From cf39537d2c763f99b3308a78d0ab5adfc4af40c0 Mon Sep 17 00:00:00 2001 From: Matt Jones <47545907+SoundMatt@users.noreply.github.com> Date: Tue, 28 Jul 2026 20:23:23 -0700 Subject: [PATCH 01/12] fix: findings emit canonical standard id + clause, SARIF driver name is c-FuSa MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit x-FuSa spec §2.4.1 (MUST): the `standard` field is a canonical lowercase id used identically everywhere in the JSON, never a display string — a clause reference belongs in the separate `clause` field. `check`/`lint`/ `analyze`/`cyber` findings were instead emitting combined display strings like "MISRA-C:2012 R15.1" or "ISO 21434 / CERT-C STR31-C" straight from the rule registry, with `clause` always empty. `cfusa_rule_t` now carries `standard_id` (canonical id) and `clause` (rule/clause reference) as two separate fields instead of one combined `standard` string, and every rule table (lint/analyze/cyber/safety) is updated to populate them. `Finding.standard`/`Finding.clause` in the JSON report, and the SARIF rule declarations' help text, are derived from the new fields. Also fixes §2.9's SARIF mapping MUST: `tool.driver.name` was emitting the binary name "cfusa" instead of the §1.1 tool name "c-FuSa" — every other JSON output already used "c-FuSa" in the common header; SARIF was the one holdout, which made GitHub Code Scanning attribute findings to "cfusa" instead of "c-FuSa". Closes #84, closes #91. Signed-off-by: Matt Jones <47545907+SoundMatt@users.noreply.github.com> --- cmd/cfusa/cmd_analyze.c | 14 ++++++------ cmd/cfusa/cmd_cyber.c | 40 +++++++++++++++++------------------ cmd/cfusa/cmd_lint.c | 20 +++++++++--------- cmd/cfusa/cmd_safety_rules.c | 41 ++++++++++++++++++------------------ include/cfusa/engine.h | 7 +++++- src/engine.c | 12 +++++++++-- src/report.c | 40 +++++++++++++++++++++++------------ tests/test_engine.c | 4 ++-- 8 files changed, 103 insertions(+), 75 deletions(-) diff --git a/cmd/cfusa/cmd_analyze.c b/cmd/cfusa/cmd_analyze.c index 40949c0..6b66e50 100644 --- a/cmd/cfusa/cmd_analyze.c +++ b/cmd/cfusa/cmd_analyze.c @@ -273,19 +273,19 @@ static int rule_a007(const char *dir, const cfusa_config_t *cfg, static const cfusa_rule_t analyze_rules[] = { {"CFUSA-A001","analyze","Unsafe string functions", - "Unbounded string operations risk overflow","CERT-C STR31-C",rule_a001}, + "Unbounded string operations risk overflow","cert-c","STR31-C",rule_a001}, {"CFUSA-A002","analyze","Unchecked allocation", - "malloc/calloc/realloc return must be checked","CERT-C MEM32-C",rule_a002}, + "malloc/calloc/realloc return must be checked","cert-c","MEM32-C",rule_a002}, {"CFUSA-A003","analyze","Signed/unsigned comparison", - "Comparison of signed and sizeof (unsigned)","CERT-C INT02-C",rule_a003}, + "Comparison of signed and sizeof (unsigned)","cert-c","INT02-C",rule_a003}, {"CFUSA-A004","analyze","Integer boundary", - "INT_MAX/MIN/UINT_MAX usage without guard","CERT-C INT30-C",rule_a004}, + "INT_MAX/MIN/UINT_MAX usage without guard","cert-c","INT30-C",rule_a004}, {"CFUSA-A005","analyze","Assert in production", - "assert() may be compiled out in release builds","CERT-C MSC11-C",rule_a005}, + "assert() may be compiled out in release builds","cert-c","MSC11-C",rule_a005}, {"CFUSA-A006","analyze","Pointer arithmetic", - "Pointer arithmetic requires bounds verification","MISRA-C:2012 R18.4",rule_a006}, + "Pointer arithmetic requires bounds verification","misra-c","R18.4",rule_a006}, {"CFUSA-A007","analyze","Unchecked system call", - "System call return values must be checked","CERT-C ERR33-C",rule_a007}, + "System call return values must be checked","cert-c","ERR33-C",rule_a007}, }; #define N_ANALYZE_RULES ((int)(sizeof(analyze_rules)/sizeof(analyze_rules[0]))) diff --git a/cmd/cfusa/cmd_cyber.c b/cmd/cfusa/cmd_cyber.c index 07ed75d..9caf20d 100644 --- a/cmd/cfusa/cmd_cyber.c +++ b/cmd/cfusa/cmd_cyber.c @@ -538,26 +538,26 @@ static int rule_cy020(const char *dir,const cfusa_config_t *cfg,cfusa_report_t * /* ---- rule table ---- */ static const cfusa_rule_t cyber_rules[] = { - {"CFUSA-CY001","cyber","Buffer copy size check","CWE-120","ISO 21434 / CERT-C STR31-C",rule_cy001}, - {"CFUSA-CY002","cyber","Format string injection","CWE-134","CERT-C FIO30-C",rule_cy002}, - {"CFUSA-CY003","cyber","OS command injection","CWE-78","ISO 21434 CAL3",rule_cy003}, - {"CFUSA-CY004","cyber","NULL pointer dereference","CWE-476","CERT-C EXP34-C",rule_cy004}, - {"CFUSA-CY005","cyber","Integer overflow in alloc","CWE-190","CERT-C INT30-C",rule_cy005}, - {"CFUSA-CY006","cyber","Use after free","CWE-416","CERT-C MEM30-C",rule_cy006}, - {"CFUSA-CY007","cyber","Double free","CWE-415","CERT-C MEM31-C",rule_cy007}, - {"CFUSA-CY008","cyber","Insecure temp file","CWE-377","CERT-C FIO21-C",rule_cy008}, - {"CFUSA-CY009","cyber","Broken crypto","CWE-327","ISO 21434 CS-7",rule_cy009}, - {"CFUSA-CY010","cyber","Dangerous function","CWE-676","CERT-C",rule_cy010}, - {"CFUSA-CY011","cyber","SSRF via curl URL variable","CWE-918","ISO 21434 CS-10",rule_cy011}, - {"CFUSA-CY012","cyber","Debug socket option exposed","CWE-489","CERT-C",rule_cy012}, - {"CFUSA-CY013","cyber","Archive path traversal (zip-slip)","CWE-23","CERT-C FIO02-C",rule_cy013}, - {"CFUSA-CY014","cyber","Weak/deprecated TLS method","CWE-326","CERT-C MSC61-C",rule_cy014}, - {"CFUSA-CY015","cyber","SQL injection via sprintf","CWE-89","CERT-C FIO30-C",rule_cy015}, - {"CFUSA-CY016","cyber","Permissive directory mode","CWE-732","CERT-C FIO06-C",rule_cy016}, - {"CFUSA-CY017","cyber","Permissive file mode","CWE-732","CERT-C FIO06-C",rule_cy017}, - {"CFUSA-CY018","cyber","Path traversal from argv/env","CWE-22","CERT-C FIO02-C",rule_cy018}, - {"CFUSA-CY019","cyber","TOCTOU race (access before open)","CWE-362","CERT-C FIO45-C",rule_cy019}, - {"CFUSA-CY020","cyber","Predictable /tmp path","CWE-377","CERT-C FIO21-C",rule_cy020}, + {"CFUSA-CY001","cyber","Buffer copy size check","CWE-120","cert-c","STR31-C",rule_cy001}, + {"CFUSA-CY002","cyber","Format string injection","CWE-134","cert-c","FIO30-C",rule_cy002}, + {"CFUSA-CY003","cyber","OS command injection","CWE-78","iso21434","CAL3",rule_cy003}, + {"CFUSA-CY004","cyber","NULL pointer dereference","CWE-476","cert-c","EXP34-C",rule_cy004}, + {"CFUSA-CY005","cyber","Integer overflow in alloc","CWE-190","cert-c","INT30-C",rule_cy005}, + {"CFUSA-CY006","cyber","Use after free","CWE-416","cert-c","MEM30-C",rule_cy006}, + {"CFUSA-CY007","cyber","Double free","CWE-415","cert-c","MEM31-C",rule_cy007}, + {"CFUSA-CY008","cyber","Insecure temp file","CWE-377","cert-c","FIO21-C",rule_cy008}, + {"CFUSA-CY009","cyber","Broken crypto","CWE-327","iso21434","CS-7",rule_cy009}, + {"CFUSA-CY010","cyber","Dangerous function","CWE-676","cert-c",NULL,rule_cy010}, + {"CFUSA-CY011","cyber","SSRF via curl URL variable","CWE-918","iso21434","CS-10",rule_cy011}, + {"CFUSA-CY012","cyber","Debug socket option exposed","CWE-489","cert-c",NULL,rule_cy012}, + {"CFUSA-CY013","cyber","Archive path traversal (zip-slip)","CWE-23","cert-c","FIO02-C",rule_cy013}, + {"CFUSA-CY014","cyber","Weak/deprecated TLS method","CWE-326","cert-c","MSC61-C",rule_cy014}, + {"CFUSA-CY015","cyber","SQL injection via sprintf","CWE-89","cert-c","FIO30-C",rule_cy015}, + {"CFUSA-CY016","cyber","Permissive directory mode","CWE-732","cert-c","FIO06-C",rule_cy016}, + {"CFUSA-CY017","cyber","Permissive file mode","CWE-732","cert-c","FIO06-C",rule_cy017}, + {"CFUSA-CY018","cyber","Path traversal from argv/env","CWE-22","cert-c","FIO02-C",rule_cy018}, + {"CFUSA-CY019","cyber","TOCTOU race (access before open)","CWE-362","cert-c","FIO45-C",rule_cy019}, + {"CFUSA-CY020","cyber","Predictable /tmp path","CWE-377","cert-c","FIO21-C",rule_cy020}, }; #define N_CYBER_RULES ((int)(sizeof(cyber_rules)/sizeof(cyber_rules[0]))) diff --git a/cmd/cfusa/cmd_lint.c b/cmd/cfusa/cmd_lint.c index f7ebe9d..e821885 100644 --- a/cmd/cfusa/cmd_lint.c +++ b/cmd/cfusa/cmd_lint.c @@ -502,25 +502,25 @@ static int rule_l010(const char *dir, const cfusa_config_t *cfg, static const cfusa_rule_t lint_rules[] = { {"CFUSA-L001","lint","Function length", - "Functions should not exceed max_function_lines","MISRA-C:2012 R15.5",rule_l001}, + "Functions should not exceed max_function_lines","misra-c","R15.5",rule_l001}, {"CFUSA-L002","lint","No goto", - "Goto statements are prohibited","MISRA-C:2012 R15.1",rule_l002}, + "Goto statements are prohibited","misra-c","R15.1",rule_l002}, {"CFUSA-L003","lint","No dynamic memory", - "malloc/calloc/realloc/free prohibited","MISRA-C:2012 R21.3",rule_l003}, + "malloc/calloc/realloc/free prohibited","misra-c","R21.3",rule_l003}, {"CFUSA-L004","lint","No recursion", - "Recursive functions are prohibited","MISRA-C:2012 R17.2",rule_l004}, + "Recursive functions are prohibited","misra-c","R17.2",rule_l004}, {"CFUSA-L005","lint","No #undef", - "#undef shall not be used","MISRA-C:2012 R20.5",rule_l005}, + "#undef shall not be used","misra-c","R20.5",rule_l005}, {"CFUSA-L006","lint","No setjmp/longjmp", - "Non-local jumps shall not be used","MISRA-C:2012 R17.4",rule_l006}, + "Non-local jumps shall not be used","misra-c","R17.4",rule_l006}, {"CFUSA-L007","lint","Mutable static variable", - "Mutable statics reduce testability","MISRA-C:2012 R8.9",rule_l007}, + "Mutable statics reduce testability","misra-c","R8.9",rule_l007}, {"CFUSA-L008","lint","Avoid void*", - "Conversions from void* should be avoided","MISRA-C:2012 R11.5",rule_l008}, + "Conversions from void* should be avoided","misra-c","R11.5",rule_l008}, {"CFUSA-L009","lint","No #pragma", - "#pragma reduces portability","MISRA-C:2012 R20.10",rule_l009}, + "#pragma reduces portability","misra-c","R20.10",rule_l009}, {"CFUSA-L010","lint","errno usage", - "errno must be zeroed before use","MISRA-C:2012 R22.8",rule_l010}, + "errno must be zeroed before use","misra-c","R22.8",rule_l010}, }; #define N_LINT_RULES ((int)(sizeof(lint_rules)/sizeof(lint_rules[0]))) diff --git a/cmd/cfusa/cmd_safety_rules.c b/cmd/cfusa/cmd_safety_rules.c index 6272d29..862eab9 100644 --- a/cmd/cfusa/cmd_safety_rules.c +++ b/cmd/cfusa/cmd_safety_rules.c @@ -918,70 +918,71 @@ static int rule_fusa005(const char *dir, const cfusa_config_t *cfg, } static const cfusa_rule_t SAFETY_RULES[] = { - /* FUSA project structure */ + /* FUSA project structure — tool-self-check, not tied to an external + * x-FuSa-spec-registry standard id (§2.4.1), so standard_id is omitted. */ {"FUSA001", "safety", ".fusa.json present", ".fusa.json project configuration required for cfusa tooling", - "x-FuSa", rule_fusa001}, + NULL, NULL, rule_fusa001}, {"FUSA002", "safety", "Build system file present", "CMakeLists.txt or equivalent required for reproducible builds", - "x-FuSa", rule_fusa002}, + NULL, NULL, rule_fusa002}, {"FUSA003", "safety", "LICENSE present", "LICENSE file required for open-source compliance", - "x-FuSa", rule_fusa003}, + NULL, NULL, rule_fusa003}, {"FUSA004", "safety", "README present", "README required for project documentation", - "x-FuSa", rule_fusa004}, + NULL, NULL, rule_fusa004}, {"FUSA005", "safety", "CI configuration present", "CI configuration required for automated safety verification", - "x-FuSa", rule_fusa005}, + NULL, NULL, rule_fusa005}, /* HARA */ {"HARA001", "safety", "HARA file present", ".fusa-hara.json must exist for ISO 26262-3 Clause 6 compliance", - "ISO 26262-3", rule_hara001}, + "iso26262", "Part 3", rule_hara001}, {"HARA002", "safety", "HARA risk ratings complete", "All hazards must have non-zero S, E, C values", - "ISO 26262-3", rule_hara002}, + "iso26262", "Part 3", rule_hara002}, {"HARA003", "safety", "HARA safety goals defined", "Every hazard must have a safety goal", - "ISO 26262-3", rule_hara003}, + "iso26262", "Part 3", rule_hara003}, {"HARA004", "safety", "HARA ASIL assigned", "Safety goals must have ASIL assigned (not TBD or empty)", - "ISO 26262-3", rule_hara004}, + "iso26262", "Part 3", rule_hara004}, {"HARA005", "safety", "HARA max ASIL within project ASIL", "Hazard ASIL must not exceed project ASIL in .fusa.json", - "ISO 26262-3", rule_hara005}, + "iso26262", "Part 3", rule_hara005}, {"HARA006", "safety", "HARA stored ASIL matches S x E x C table", "risk.asil MUST derive from severity x exposure x controllability " "(ISO 26262-3 Table 4, x-FuSa spec §1.2.5)", - "ISO 26262-3", rule_hara006}, + "iso26262", "Table 4", rule_hara006}, /* ISO 26262 */ {"ISO26262001", "safety", "ISO 26262 gap report present", "iso26262-gap-report.json should be generated and committed", - "ISO 26262", rule_iso26262001}, + "iso26262", NULL, rule_iso26262001}, {"ISO26262002", "safety", "Requirements have ASIL annotations", "All requirements in .fusa-reqs.json should have ASIL/level fields", - "ISO 26262", rule_iso26262002}, + "iso26262", NULL, rule_iso26262002}, {"ISO26262003", "safety", "Tool qualification passes", "Tool qualification report must have zero failures", - "ISO 26262-8", rule_iso26262003}, + "iso26262", "Part 8", rule_iso26262003}, /* Coupling */ {"COUP001", "analyze", "Data coupling — extern mutable vars", "Exported mutable variables create data coupling (DO-178C §6.4.4.3)", - "DO-178C", rule_coup001}, + "do178c", "6.4.4.3", rule_coup001}, {"COUP002", "analyze", "Control coupling — function pointer params", "Function pointer parameters create control coupling (DO-178C §6.4.4.3)", - "DO-178C", rule_coup002}, + "do178c", "6.4.4.3", rule_coup002}, {"COUP003", "safety", "Coupling report present", "coupling-report.json should exist as DO-178C coupling evidence", - "DO-178C", rule_coup003}, + "do178c", NULL, rule_coup003}, /* Disposition */ {"DISP001", "safety", "ERROR findings dispositioned", "All ERROR findings in check-report.json must have a disposition record", - "ISO 26262", rule_disp001}, + "iso26262", NULL, rule_disp001}, /* Complexity */ {"COMP001", "analyze", "Cyclomatic complexity within threshold", "V(G) = 1 + decision nodes; threshold varies by DAL (DO-178C §6.3.4)", - "DO-178C", rule_comp001}, + "do178c", "6.3.4", rule_comp001}, }; void cfusa_safety_register_rules(void) diff --git a/include/cfusa/engine.h b/include/cfusa/engine.h index 4240f5b..a7261f8 100644 --- a/include/cfusa/engine.h +++ b/include/cfusa/engine.h @@ -18,7 +18,12 @@ typedef struct { const char *category; const char *name; const char *description; - const char *standard; /* e.g. "MISRA-C:2012 R15.1" */ + const char *standard_id; /* canonical lowercase id (x-FuSa spec §2.4.1), + e.g. "misra-c" — NULL/"" when no external + standard applies (e.g. project-structure + self-checks) */ + const char *clause; /* clause/rule reference within standard_id, + e.g. "R15.1" — NULL/"" when not applicable */ cfusa_rule_fn run; } cfusa_rule_t; diff --git a/src/engine.c b/src/engine.c index a5a7315..517538e 100644 --- a/src/engine.c +++ b/src/engine.c @@ -39,9 +39,17 @@ void cfusa_engine_list_rules(void) "----------------------------------------", "--------"); for (int i = 0; i < g_rule_count; i++) { const cfusa_rule_t *r = g_rules[i]; + char std_disp[80]; + if (r->standard_id && r->standard_id[0]) { + if (r->clause && r->clause[0]) + snprintf(std_disp, sizeof(std_disp), "%s %s", r->standard_id, r->clause); + else + snprintf(std_disp, sizeof(std_disp), "%s", r->standard_id); + } else { + std_disp[0] = '\0'; + } printf("%-18s %-10s %-40s %s\n", - r->id, r->category, r->name, - r->standard ? r->standard : ""); + r->id, r->category, r->name, std_disp); } } diff --git a/src/report.c b/src/report.c index 4881ddd..22641dc 100644 --- a/src/report.c +++ b/src/report.c @@ -314,20 +314,23 @@ static void print_json(const cfusa_report_t *rpt, FILE *out) cfusa_str_escape_json(f->file, esc_file, sizeof(esc_file)); cfusa_str_escape_json(f->message, esc_msg, sizeof(esc_msg)); - /* Look up rule metadata for remediation/standard fields */ + /* Look up rule metadata for remediation/standard/clause fields */ const char *remediation = ""; const char *standard = ""; + const char *clause = ""; for (int j = 0; j < nrules; j++) { const cfusa_rule_t *r = cfusa_engine_get_rule(j); if (strcmp(r->id, f->rule_id) == 0) { - if (r->description) remediation = r->description; - if (r->standard) standard = r->standard; + if (r->description) remediation = r->description; + if (r->standard_id) standard = r->standard_id; + if (r->clause) clause = r->clause; break; } } - char esc_rem[256], esc_rulestd[64]; + char esc_rem[256], esc_rulestd[64], esc_clause[64]; cfusa_str_escape_json(remediation, esc_rem, sizeof(esc_rem)); cfusa_str_escape_json(standard, esc_rulestd, sizeof(esc_rulestd)); + cfusa_str_escape_json(clause, esc_clause, sizeof(esc_clause)); /* §4 location: file and line are MUST; endLine/endColumn are MAY */ if (f->end_line > 0 && f->end_column > 0) { @@ -339,12 +342,13 @@ static void print_json(const cfusa_report_t *rpt, FILE *out) " \"message\": \"%s\"," " \"fingerprint\": \"%s\"," " \"remediation\": \"%s\"," - " \"standard\": \"%s\"}%s\n", + " \"standard\": \"%s\"," + " \"clause\": \"%s\"}%s\n", f->rule_id, f->category, cfusa_severity_str(f->severity), esc_file, f->line, f->end_line, f->end_column, esc_msg, f->fingerprint, - esc_rem, esc_rulestd, + esc_rem, esc_rulestd, esc_clause, (i < rpt->count - 1) ? "," : ""); } else if (f->end_line > 0) { fprintf(out, @@ -355,12 +359,13 @@ static void print_json(const cfusa_report_t *rpt, FILE *out) " \"message\": \"%s\"," " \"fingerprint\": \"%s\"," " \"remediation\": \"%s\"," - " \"standard\": \"%s\"}%s\n", + " \"standard\": \"%s\"," + " \"clause\": \"%s\"}%s\n", f->rule_id, f->category, cfusa_severity_str(f->severity), esc_file, f->line, f->end_line, esc_msg, f->fingerprint, - esc_rem, esc_rulestd, + esc_rem, esc_rulestd, esc_clause, (i < rpt->count - 1) ? "," : ""); } else { fprintf(out, @@ -370,12 +375,13 @@ static void print_json(const cfusa_report_t *rpt, FILE *out) " \"message\": \"%s\"," " \"fingerprint\": \"%s\"," " \"remediation\": \"%s\"," - " \"standard\": \"%s\"}%s\n", + " \"standard\": \"%s\"," + " \"clause\": \"%s\"}%s\n", f->rule_id, f->category, cfusa_severity_str(f->severity), esc_file, f->line, esc_msg, f->fingerprint, - esc_rem, esc_rulestd, + esc_rem, esc_rulestd, esc_clause, (i < rpt->count - 1) ? "," : ""); } } @@ -403,7 +409,7 @@ static void print_sarif(const cfusa_report_t *rpt, FILE *out) " \"$schema\": \"https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json\",\n" " \"runs\": [{\n" " \"tool\": {\"driver\": {\n" - " \"name\": \"cfusa\",\n" + " \"name\": \"c-FuSa\",\n" " \"version\": \"%s\",\n" " \"informationUri\": \"https://github.com/SoundMatt/c-FuSa\",\n" " \"rules\": [\n", @@ -411,10 +417,18 @@ static void print_sarif(const cfusa_report_t *rpt, FILE *out) for (int i = 0; i < nrules; i++) { const cfusa_rule_t *r = cfusa_engine_get_rule(i); - char esc_name[128], esc_desc[256], esc_std[64]; + char esc_name[128], esc_desc[256], esc_std[64], std_disp[80]; cfusa_str_escape_json(r->name, esc_name, sizeof(esc_name)); cfusa_str_escape_json(r->description ? r->description : r->name, esc_desc, sizeof(esc_desc)); - cfusa_str_escape_json(r->standard ? r->standard : "", esc_std, sizeof(esc_std)); + if (r->standard_id && r->standard_id[0]) { + if (r->clause && r->clause[0]) + snprintf(std_disp, sizeof(std_disp), "%s %s", r->standard_id, r->clause); + else + snprintf(std_disp, sizeof(std_disp), "%s", r->standard_id); + } else { + std_disp[0] = '\0'; + } + cfusa_str_escape_json(std_disp, esc_std, sizeof(esc_std)); fprintf(out, " {\"id\": \"%s\"," " \"name\": \"%s\"," diff --git a/tests/test_engine.c b/tests/test_engine.c index 6052e61..675c143 100644 --- a/tests/test_engine.c +++ b/tests/test_engine.c @@ -20,7 +20,7 @@ static int dummy_rule_run(const char *dir, const cfusa_config_t *cfg, } static const cfusa_rule_t dummy_rule = { - "DUMMY-001", "test", "Dummy rule", "Test rule", "TEST", dummy_rule_run + "DUMMY-001", "test", "Dummy rule", "Test rule", "test", "T001", dummy_rule_run }; static int dummy_lint_called = 0; @@ -34,7 +34,7 @@ static int dummy_lint_run(const char *dir, const cfusa_config_t *cfg, } static const cfusa_rule_t dummy_lint_rule = { - "LINT-T001", "lint", "Lint test rule", "Test", NULL, dummy_lint_run + "LINT-T001", "lint", "Lint test rule", "Test", NULL, NULL, dummy_lint_run }; /* ---- Tests ---- */ From 3fcd0aa5ecb66c1540410600e55916a3dc284c31 Mon Sep 17 00:00:00 2001 From: Matt Jones <47545907+SoundMatt@users.noreply.github.com> Date: Tue, 28 Jul 2026 20:23:47 -0700 Subject: [PATCH 02/12] fix: CFUSA-L004 false positive when callee name has caller's name as suffix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `cfusa check`/`cfusa lint`'s CFUSA-L004 rule (MISRA-C 2012 Rule 17.2, no recursion) flagged a local function as "recursive" whenever an unrelated function it called merely had a name that *ends with* the local function's name — e.g. a static `evaluate()` calling an external `rcp_e2e_wd_evaluate()` or `helper_evaluate()`, neither of which is a self-call or forms a call cycle. The self-call scan looked for the literal substring "fn_name(" in each line of the function body with no identifier-boundary check on the left side of the match, so any callee name merely ending with the caller's name matched too. Add `l004_self_call()`, a small word-boundary-aware scanner: it still skips string/character literals, but additionally requires the character immediately before a candidate match to not be an identifier character (or be the start of line). Real recursion (a function actually calling itself) is unaffected. Closes #82. Signed-off-by: Matt Jones <47545907+SoundMatt@users.noreply.github.com> --- cmd/cfusa/cmd_lint.c | 32 +++++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/cmd/cfusa/cmd_lint.c b/cmd/cfusa/cmd_lint.c index e821885..f7c6bed 100644 --- a/cmd/cfusa/cmd_lint.c +++ b/cmd/cfusa/cmd_lint.c @@ -1,6 +1,7 @@ #include #include #include +#include #include #include "cfusa/engine.h" #include "cfusa/report.h" @@ -186,6 +187,33 @@ typedef struct { int in_block_comment; /* persists across fgets() iterations */ } l004_ctx_t; +/* Word-boundary-aware self-call detector for CFUSA-L004. Unlike the + * generic cfusa_match_outside_string() substring match (which is a + * deliberate, documented non-word-boundary checker used elsewhere for + * fixed dangerous-function-name lookups), a self-call check additionally + * requires an identifier boundary immediately before the candidate match + * — otherwise a callee whose name merely *ends with* the caller's name + * (e.g. static int evaluate(...) calling helper_evaluate(...), or + * rcp_e2e_wd_evaluate(...)) is misreported as recursion. */ +static int l004_self_call(const char *line, const char *fn_name) +{ + size_t flen = strlen(fn_name); + if (flen == 0) return 0; + int in_str = 0; + const char *p = line; + while (*p) { + if (*p == '"' && (p == line || p[-1] != '\\')) + in_str = !in_str; + if (!in_str && strncmp(p, fn_name, flen) == 0 && p[flen] == '(') { + int boundary_ok = (p == line) || + !(isalnum((unsigned char)p[-1]) || p[-1] == '_'); + if (boundary_ok) return 1; + } + p++; + } + return 0; +} + static int l004_file(const char *path, void *vctx) { l004_ctx_t *ctx = vctx; @@ -288,9 +316,7 @@ static int l004_file(const char *path, void *vctx) /* Self-call check. Skip the line where the function was first * detected: the signature always contains "fn_name(" naturally. */ if (!fn_just_detected && ctx->in_fn && ctx->fn_name[0] && brace > 0) { - char call[130]; - snprintf(call, sizeof(call), "%s(", ctx->fn_name); - if (cfusa_match_outside_string(line, call)) { + if (l004_self_call(line, ctx->fn_name)) { cfusa_report_add(ctx->rpt, "CFUSA-L004", CFUSA_CATEGORY_LINT, SEV_ERROR, path, lineno, From f88d1b3faf034885b766540a7b3af9be900265a5 Mon Sep 17 00:00:00 2001 From: Matt Jones <47545907+SoundMatt@users.noreply.github.com> Date: Tue, 28 Jul 2026 20:23:54 -0700 Subject: [PATCH 03/12] =?UTF-8?q?fix:=20hara=20init=20scaffolds=20the=20?= =?UTF-8?q?=C2=A71.2.5=20input=20schema,=20not=20a=20report=20envelope?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit x-FuSa spec §1.2.5 defines `.fusa-hara.json` as an INPUT file authored by a project (like `.fusa-reqs.json`), with a plain schema of `{project, standard, createdAt, operationalSituations, hazards, safetyGoals, attestation?}` — no `schemaVersion`/`kind`/`tool`/ `toolVersion`/`language` fields, since those §3.1 common-header fields belong to *report documents*, not this input file. §3.1's `kind` enum is also closed and does not contain a bare `"hara"` value (the corresponding report kind is `"hara-report"`, used by `hara --format json`'s separate output path). `hara init`'s scaffold wrote all five report-envelope fields (including `"kind": "hara"`, not even a member of the closed enum) straight into the input file it creates, unlike c-FuSa's own hand-authored `.fusa-hara.json` at the repo root, which correctly has none of them. Drop the envelope fields from the scaffold so a freshly-initialised file matches the §1.2.5 schema exactly. Closes #90. Signed-off-by: Matt Jones <47545907+SoundMatt@users.noreply.github.com> --- cmd/cfusa/cmd_hara.c | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/cmd/cfusa/cmd_hara.c b/cmd/cfusa/cmd_hara.c index 06e76d9..624e951 100644 --- a/cmd/cfusa/cmd_hara.c +++ b/cmd/cfusa/cmd_hara.c @@ -449,14 +449,15 @@ static int do_init(const char *dir, const char *project) cfusa_str_escape_json(project, esc_project, sizeof(esc_project)); /* x-FuSa spec §1.6 rule 1: an unanalyzed section MUST be an empty - * array, never a dummy/placeholder row. */ + * array, never a dummy/placeholder row. + * + * §1.2.5: .fusa-hara.json is an INPUT file (like .fusa-reqs.json), not + * a report document — it MUST NOT carry the §3.1 report envelope + * (schemaVersion/kind/tool/toolVersion/language). Its schema is just + * {project, standard, createdAt, operationalSituations, hazards, + * safetyGoals, attestation?}. */ fprintf(f, "{\n" - " \"schemaVersion\": \"" CFUSA_SCHEMA_VERSION "\",\n" - " \"kind\": \"hara\",\n" - " \"tool\": \"c-FuSa\",\n" - " \"toolVersion\": \"" CFUSA_VERSION_STRING "\",\n" - " \"language\": \"c\",\n" " \"project\": \"%s\",\n" " \"standard\": \"iso26262\",\n" " \"createdAt\": \"%s\",\n" From cb80b7508c1092a1e6f8765200688d4d38ba70af Mon Sep 17 00:00:00 2001 From: Matt Jones <47545907+SoundMatt@users.noreply.github.com> Date: Tue, 28 Jul 2026 20:24:06 -0700 Subject: [PATCH 04/12] fix: sas --format json (no --output) writes sas.json, not raw JSON into sas.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit x-FuSa spec §9.3: `sas --format json` writes `sas.json`, and a tool MUST also write the human-readable `sas.md` companion — `sas.json` is not a replacement for it. `--output`'s default was hardcoded to `"sas.md"` regardless of `--format`, so `cfusa sas --format json --dir .` (no `--output`) wrote the JSON body into a file literally named `sas.md`. Worse, the later "did we already write sas.md?" companion-generation guard compared only the output path, saw `out_path == "sas.md"`, and concluded the real Markdown companion had already been produced — so neither of the two files §9.3 requires ever actually existed; there was one file, named sas.md, containing JSON. Make the default `--output` path depend on `--format` when not given explicitly: "sas.json" for `--format json`, "sas.md" for md/text/default. The existing companion-generation logic then does the right thing unmodified — it writes JSON to sas.json and the real Markdown companion to sas.md. Closes #89. Signed-off-by: Matt Jones <47545907+SoundMatt@users.noreply.github.com> --- cmd/cfusa/cmd_sas.c | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/cmd/cfusa/cmd_sas.c b/cmd/cfusa/cmd_sas.c index 93f26a2..16f55c3 100644 --- a/cmd/cfusa/cmd_sas.c +++ b/cmd/cfusa/cmd_sas.c @@ -90,7 +90,7 @@ static size_t sas_canonical_content(const char *dir, char *buf, size_t bufsz) int cmd_sas(int argc, char **argv) { const char *dir = "."; - const char *output = "sas.md"; + const char *output = NULL; const char *fmt_s = "md"; const char *dal = "DAL-B"; const char *prepared_by = NULL; @@ -123,7 +123,7 @@ int cmd_sas(int argc, char **argv) case 'A': require_attestation = 1; break; case 'T': attest = optarg; break; case 'h': - printf("Usage: cfusa sas [--dir ] [--output sas.md]\n" + printf("Usage: cfusa sas [--dir ] [--output ]\n" " [--format md|text|json] [--dal DAL-A|B|C|D]\n" " [--prepared-by ]\n" " [--strict] [--require-attestation] [--attest ]\n\n" @@ -137,6 +137,15 @@ int cmd_sas(int argc, char **argv) } if (strict) require_attestation = 1; + /* Default --output path depends on --format when not given explicitly: + * "sas.json" for --format json, "sas.md" for md/text/default — a single + * hardcoded "sas.md" default regardless of format would (and previously + * did) write JSON content into a file literally named sas.md, and fool + * the "did we already write sas.md?" companion check below into + * skipping the real Markdown companion (x-FuSa spec §9.3 MUST). */ + if (!output) + output = !strcmp(fmt_s, "json") ? "sas.json" : "sas.md"; + cfusa_config_t cfg; cfusa_config_load(dir, &cfg); From 3bf9b384559e92f44adb51a2e9d93d2fc04c26ed Mon Sep 17 00:00:00 2001 From: Matt Jones <47545907+SoundMatt@users.noreply.github.com> Date: Tue, 28 Jul 2026 20:24:14 -0700 Subject: [PATCH 05/12] fix: qualify accepts --dir instead of exiting 2 for any project other than cwd MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit x-FuSa spec §2.2: `--dir ` (project root, default cwd) applies to "all" commands, and §6's own documented `qualify` CLI surface includes it explicitly. `cmd_qualify`'s `getopt_long` table had no `"dir"`/`-d` entry at all, so `cfusa qualify --dir ` failed unconditionally with exit `2` (usage error) — the flag simply didn't exist, regardless of position. Since FuSaOps orchestrates every x-FuSa tool by invoking it with `--dir ` against whatever project it is analysing (almost never its own cwd), and `qualify` is one of the §9.1 commands FuSaOps consumes, this broke `qualify` for every project other than "run cfusa from inside the target directory". Add `--dir`/`-d` to `cmd_qualify`, and resolve a relative `--binary` path against it rather than always against the process cwd. Closes #88. Signed-off-by: Matt Jones <47545907+SoundMatt@users.noreply.github.com> --- cmd/cfusa/cmd_qualify.c | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/cmd/cfusa/cmd_qualify.c b/cmd/cfusa/cmd_qualify.c index 38faea3..886c31b 100644 --- a/cmd/cfusa/cmd_qualify.c +++ b/cmd/cfusa/cmd_qualify.c @@ -471,6 +471,7 @@ enum { int cmd_qualify(int argc, char **argv) { + const char *dir = "."; const char *binary = NULL; const char *output = NULL; const char *fmt_s = "text"; @@ -489,6 +490,7 @@ int cmd_qualify(int argc, char **argv) const char *achievable_asil = NULL; static const struct option long_opts[] = { + {"dir", required_argument, NULL, 'd'}, {"binary", required_argument, NULL, 'b'}, {"output", required_argument, NULL, 'o'}, {"format", required_argument, NULL, 'f'}, @@ -511,8 +513,9 @@ int cmd_qualify(int argc, char **argv) #if defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) { extern int optreset; optreset = 1; } #endif - while ((c = getopt_long(argc, argv, "b:o:f:vh", long_opts, NULL)) != -1) { + while ((c = getopt_long(argc, argv, "d:b:o:f:vh", long_opts, NULL)) != -1) { switch (c) { + case 'd': dir = optarg; break; case 'b': binary = optarg; break; case 'o': output = optarg; break; case 'f': fmt_s = optarg; fmt_explicit = 1; break; @@ -525,7 +528,7 @@ int cmd_qualify(int argc, char **argv) case OPT_IND_TEST_EXEC: ind_test_exec = optarg; break; case OPT_ACHIEVE_ASIL: achievable_asil = optarg; break; case 'h': - printf("Usage: cfusa qualify [--binary ] [--format text|json]\n" + printf("Usage: cfusa qualify [--dir ] [--binary ] [--format text|json]\n" " [--output ] [--verbose]\n" " [--qualification-method self|independent]\n" " [--qualifier ] [--record-uri ]\n" @@ -544,7 +547,14 @@ int cmd_qualify(int argc, char **argv) if (output && !fmt_explicit) fmt_s = "json"; - /* Hash the binary if provided */ + /* Hash the binary if provided. §2.2: --dir is the project root against + * which relative paths resolve — a relative --binary is joined with it + * rather than always resolving against the process cwd. */ + char binary_resolved[512]; + if (binary && binary[0] != '/') { + cfusa_path_join(binary_resolved, sizeof(binary_resolved), dir, binary); + binary = binary_resolved; + } char bin_hash[65] = "(not provided)"; if (binary && cfusa_file_exists(binary)) cfusa_sha256_file(binary, bin_hash); From ab045375d76e75c610820bf930e762c2514d0d04 Mon Sep 17 00:00:00 2001 From: Matt Jones <47545907+SoundMatt@users.noreply.github.com> Date: Tue, 28 Jul 2026 20:24:34 -0700 Subject: [PATCH 06/12] =?UTF-8?q?fix:=20misra=20uses=20the=20canonical=20?= =?UTF-8?q?=C2=A79.3=20gap-report=20schema?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit x-FuSa spec §3.1's `kind` enum is closed — `"misra-coverage"` is not a member (the closest match is `"gap-report"`). §2.4.1 requires `standard` to be a canonical lowercase id, never a display string — `misra` emitted `"MISRA C:2012"` instead of the registry's `"misra-c"`. And §9.3 requires a standards command's JSON to use the canonical `objectives[]` + `summary{total,satisfied,partial,gaps}` shape, not an ad hoc `rules[]` array with a per-rule `"rule"` field (the same anti-pattern already fixed for the other six standards commands). `misra --format json` violated all three: `"kind": "misra-coverage"`, `"standard": "MISRA C:2012"`, and a `rules[]`/`covered`/`gaps` payload shape with no `summary` object. Closes #87. Signed-off-by: Matt Jones <47545907+SoundMatt@users.noreply.github.com> --- cmd/cfusa/cmd_misra.c | 26 ++++++++++++++------------ tests/test_cli_commands.c | 12 ++++++++---- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/cmd/cfusa/cmd_misra.c b/cmd/cfusa/cmd_misra.c index 9ff6d54..5bd17ad 100644 --- a/cmd/cfusa/cmd_misra.c +++ b/cmd/cfusa/cmd_misra.c @@ -107,34 +107,36 @@ int cmd_misra(int argc, char **argv) fprintf(out, "{\n" " \"schemaVersion\": \"" CFUSA_SCHEMA_VERSION "\",\n" - " \"kind\": \"misra-coverage\",\n" + " \"kind\": \"gap-report\",\n" " \"tool\": \"c-FuSa\",\n" " \"toolVersion\": \"" CFUSA_VERSION_STRING "\",\n" " \"language\": \"c\",\n" " \"generatedAt\": \"%s\",\n" " \"projectRoot\": \"%s\",\n" - " \"standard\": \"MISRA C:2012\",\n" + " \"standard\": \"misra-c\",\n" " \"project\": \"%s\",\n" - " \"covered\": %d,\n" - " \"gaps\": %d,\n" - " \"rules\": [\n", - ts, dir, cfg.project, covered, not_covered); + " \"objectives\": [\n", + ts, dir, cfg.project); int first = 1; for (int i = 0; MISRA_RULES[i].rule; i++) { const misra_row_t *r = &MISRA_RULES[i]; if (gaps_only && r->cfusa_rule) continue; if (!first) fprintf(out, ",\n"); - const char *status = r->cfusa_rule ? "covered" : "gap"; + const char *status = r->cfusa_rule ? "satisfied" : "gap"; fprintf(out, - " {\"id\": \"%s\", \"category\": \"%s\"," - " \"title\": \"%s\", \"rule\": %s%s%s, \"status\": \"%s\"}", - r->rule, r->category, r->title, - r->cfusa_rule ? "\"" : "", r->cfusa_rule ? r->cfusa_rule : "null", + " {\"id\": \"%s\", \"clause\": \"%s\", \"category\": \"%s\"," + " \"title\": \"%s\", \"findings\": [%s%s%s], \"status\": \"%s\"}", + r->rule, r->rule, r->category, r->title, + r->cfusa_rule ? "\"" : "", r->cfusa_rule ? r->cfusa_rule : "", r->cfusa_rule ? "\"" : "", status); first = 0; } - fprintf(out, "\n ]\n}\n"); + fprintf(out, + "\n ],\n" + " \"summary\": {\"total\": %d, \"satisfied\": %d, \"partial\": 0, \"gaps\": %d}\n" + "}\n", + covered + not_covered, covered, not_covered); } else { fprintf(out, "MISRA C:2012 Rule Coverage\n"); fprintf(out, "==========================\n\n"); diff --git a/tests/test_cli_commands.c b/tests/test_cli_commands.c index ca69c02..21ee6a6 100644 --- a/tests/test_cli_commands.c +++ b/tests/test_cli_commands.c @@ -325,13 +325,17 @@ void test_misra_json_format(void) FILE *f = fopen(out, "r"); TEST_ASSERT_NOT_NULL(f); if (f) { - char buf[4096]; size_t n = fread(buf, 1, sizeof(buf)-1, f); + char buf[8192]; size_t n = fread(buf, 1, sizeof(buf)-1, f); buf[n] = '\0'; fclose(f); TEST_ASSERT_NOT_NULL(strstr(buf, "\"schemaVersion\"")); - TEST_ASSERT_NOT_NULL(strstr(buf, "\"misra-coverage\"")); - TEST_ASSERT_NOT_NULL(strstr(buf, "\"standard\"")); + /* x-FuSa spec §9.3: standards commands MUST use the canonical + * gap-report schema — kind "gap-report", canonical §2.4.1 standard + * id "misra-c" (not "MISRA C:2012"), objectives[]/summary{}. */ + TEST_ASSERT_NOT_NULL(strstr(buf, "\"kind\": \"gap-report\"")); + TEST_ASSERT_NOT_NULL(strstr(buf, "\"standard\": \"misra-c\"")); TEST_ASSERT_NOT_NULL(strstr(buf, "\"projectRoot\"")); - TEST_ASSERT_NOT_NULL(strstr(buf, "\"rules\"")); + TEST_ASSERT_NOT_NULL(strstr(buf, "\"objectives\"")); + TEST_ASSERT_NOT_NULL(strstr(buf, "\"summary\"")); } } From a20c3cb3f99e721922dd3e4a2b34f8e540eb65c7 Mon Sep 17 00:00:00 2001 From: Matt Jones <47545907+SoundMatt@users.noreply.github.com> Date: Tue, 28 Jul 2026 20:24:58 -0700 Subject: [PATCH 07/12] fix: gap-report summary{} object across standards commands; iec62443 id MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit x-FuSa spec §9.3 (MUST): a standards command's JSON must carry a `summary{total,satisfied,partial,gaps}` object satisfying `satisfied + partial + gaps == total`. `iso26262`/`iec61508`/`do178`/ `iso21434`/`unece`/`iec62443` each emitted ad hoc top-level counters (`covered`/`gaps`[/`na`][/`mandatoryGaps`/`recommendedGaps`]) with no `summary` object at all — only `slsa` implemented the canonical shape correctly. `iso26262` additionally had a latent counting bug: its per-objective status already distinguished a mandatory "gap" from a recommended "partial", but the top-level counter combined both into one `gaps` value, so the two were inconsistent within the same document; it now tracks them the same way `iec61508`/`iec62443` already did, and exposes the recommended count as a new `recommendedGaps` field. `iso21434` and `unece` additionally have `"manual"`/`"na"` per-objective statuses for organisational evidence that isn't tool-checkable — not members of §9.3's closed `satisfied|partial|gap` enum. Per that section's own consumer fail-safe rule ("map any unrecognised status to gap"), the new summary counts them as gaps for consistency. Also, x-FuSa spec §2.4.1: `iec62443`'s gap-report emitted the non-registry command name `"iec62443"` as its `standard` id instead of the split `"iec62443-4-1"`/`"iec62443-4-2"` registry ids (issue #12 predates that split). This command only implements the Component Requirements (-4-2) part, so its `standard` is now `"iec62443-4-2"`. Closes #85, closes #86. Signed-off-by: Matt Jones <47545907+SoundMatt@users.noreply.github.com> --- cmd/cfusa/cmd_do178.c | 8 +++++++- cmd/cfusa/cmd_iec61508.c | 6 +++++- cmd/cfusa/cmd_iec62443.c | 8 ++++++-- cmd/cfusa/cmd_iso21434.c | 13 ++++++++++++- cmd/cfusa/cmd_iso26262.c | 22 +++++++++++++++------- cmd/cfusa/cmd_unece.c | 9 ++++++++- tests/test_cli_commands.c | 6 ++++-- 7 files changed, 57 insertions(+), 15 deletions(-) diff --git a/cmd/cfusa/cmd_do178.c b/cmd/cfusa/cmd_do178.c index 0a18050..775eadc 100644 --- a/cmd/cfusa/cmd_do178.c +++ b/cmd/cfusa/cmd_do178.c @@ -211,12 +211,14 @@ int cmd_do178(int argc, char **argv) " \"applicable\": %d, \"total\": %d,\n \"objectives\": [\n", ts, dir, cfg.project,(char)('A'+dal_col),applicable,total); int first=1; + int satisfied=0; for (int i=0;OBJECTIVES[i].id;i++){ int applies[4]={OBJECTIVES[i].dal_a,OBJECTIVES[i].dal_b, OBJECTIVES[i].dal_c,OBJECTIVES[i].dal_d}; if(!applies[dal_col]) continue; int ok = OBJECTIVES[i].evidence_file && do178_file_exists(dir, OBJECTIVES[i].evidence_file); + if (ok) satisfied++; const char *status = ok ? "satisfied" : "gap"; fprintf(out,"%s {\"id\":\"%s\",\"process\":\"%s\"," "\"title\":\"%s\",\"findings\":[],\"status\":\"%s\"}", @@ -225,7 +227,11 @@ int cmd_do178(int argc, char **argv) OBJECTIVES[i].objective, status); first=0; } - fprintf(out,"\n ]\n}\n"); + fprintf(out, + "\n ],\n" + " \"summary\": {\"total\": %d, \"satisfied\": %d, \"partial\": 0, \"gaps\": %d}\n" + "}\n", + applicable, satisfied, applicable - satisfied); } else { fprintf(out,"DO-178C Annex A Gap Report\n" "Project: %s v%s DAL: %c Generated: %s\n" diff --git a/cmd/cfusa/cmd_iec61508.c b/cmd/cfusa/cmd_iec61508.c index d853658..07b6925 100644 --- a/cmd/cfusa/cmd_iec61508.c +++ b/cmd/cfusa/cmd_iec61508.c @@ -176,7 +176,11 @@ int cmd_iec61508(int argc, char **argv) level_str, status); first = 0; } - fprintf(out, "\n ]\n}\n"); + fprintf(out, + "\n ],\n" + " \"summary\": {\"total\": %d, \"satisfied\": %d, \"partial\": %d, \"gaps\": %d}\n" + "}\n", + covered + gaps_r + gaps_m, covered, gaps_r, gaps_m); } else if (!strcmp(fmt_s, "text")) { fprintf(out, "IEC 61508 Parts 1-3 Gap Report — %s (target %s)\n", cfg.project, sil); fprintf(out, "=====================================================\n\n"); diff --git a/cmd/cfusa/cmd_iec62443.c b/cmd/cfusa/cmd_iec62443.c index ce8c000..7368dfc 100644 --- a/cmd/cfusa/cmd_iec62443.c +++ b/cmd/cfusa/cmd_iec62443.c @@ -160,7 +160,7 @@ int cmd_iec62443(int argc, char **argv) " \"language\": \"c\",\n" " \"generatedAt\": \"%s\",\n" " \"projectRoot\": \"%s\",\n" - " \"standard\": \"iec62443\",\n" + " \"standard\": \"iec62443-4-2\",\n" " \"project\": \"%s\",\n" " \"sl\": \"%s\",\n" " \"hasProjectConfig\": %s,\n" @@ -192,7 +192,11 @@ int cmd_iec62443(int argc, char **argv) level_str, status); first = 0; } - fprintf(out, "\n ]\n}\n"); + fprintf(out, + "\n ],\n" + " \"summary\": {\"total\": %d, \"satisfied\": %d, \"partial\": %d, \"gaps\": %d}\n" + "}\n", + covered + gaps_r + gaps_m, covered, gaps_r, gaps_m); } else if (!strcmp(fmt_s, "text")) { fprintf(out, "IEC 62443 Gap Report — %s (target %s, profile 4-2)\n", cfg.project, sl); fprintf(out, "==================================================\n\n"); diff --git a/cmd/cfusa/cmd_iso21434.c b/cmd/cfusa/cmd_iso21434.c index 6cb9c67..046c460 100644 --- a/cmd/cfusa/cmd_iso21434.c +++ b/cmd/cfusa/cmd_iso21434.c @@ -183,7 +183,18 @@ int cmd_iso21434(int argc, char **argv) o->id, o->description, status); first = 0; } - fprintf(out, "\n ]\n}\n"); + /* §9.3 canonical status enum is satisfied|partial|gap — "manual" and + * "na" (used above for objectives requiring non-tool-checkable + * organisational evidence, or not applicable at this CAL) are not + * members of that closed enum, so a spec-conformant consumer's + * fail-safe mapping treats them as "gap" (see §9.3 status note). + * The summary below counts them the same way for consistency with + * that fail-safe rule. */ + fprintf(out, + "\n ],\n" + " \"summary\": {\"total\": %d, \"satisfied\": %d, \"partial\": 0, \"gaps\": %d}\n" + "}\n", + pass + gap + manual, pass, gap + manual); } else if (!strcmp(fmt, "text")) { fprintf(out, "ISO 21434 Gap Report — %s (%s)\n", cfg.project, cal); fprintf(out, "============================================\n\n"); diff --git a/cmd/cfusa/cmd_iso26262.c b/cmd/cfusa/cmd_iso26262.c index d35ddb2..c982d8a 100644 --- a/cmd/cfusa/cmd_iso26262.c +++ b/cmd/cfusa/cmd_iso26262.c @@ -124,7 +124,7 @@ int cmd_iso26262(int argc, char **argv) return 2; } - int covered = 0, gaps = 0, na = 0; + int covered = 0, gaps = 0, gaps_r = 0, na = 0; for (int i = 0; OBJECTIVES[i].clause; i++) { const iso26262_row_t *r = &OBJECTIVES[i]; int req = (level==1) ? r->asil_a : @@ -133,7 +133,9 @@ int cmd_iso26262(int argc, char **argv) if (req == 0) { na++; continue; } int ok = r->cfusa_rule != NULL; if (!ok && r->evidence_file) ok = file_exists_in_dir(dir, r->evidence_file); - if (ok) covered++; else gaps++; + if (ok) covered++; + else if (req == 2) gaps_r++; + else gaps++; } FILE *out = stdout; @@ -155,9 +157,10 @@ int cmd_iso26262(int argc, char **argv) " \"asil\": \"%s\",\n" " \"covered\": %d,\n" " \"gaps\": %d,\n" + " \"recommendedGaps\": %d,\n" " \"na\": %d,\n" " \"objectives\": [\n", - ts, dir, cfg.project, asil, covered, gaps, na); + ts, dir, cfg.project, asil, covered, gaps, gaps_r, na); int first = 1; for (int i = 0; OBJECTIVES[i].clause; i++) { const iso26262_row_t *r = &OBJECTIVES[i]; @@ -178,7 +181,11 @@ int cmd_iso26262(int argc, char **argv) status); first = 0; } - fprintf(out, "\n ]\n}\n"); + fprintf(out, + "\n ],\n" + " \"summary\": {\"total\": %d, \"satisfied\": %d, \"partial\": %d, \"gaps\": %d}\n" + "}\n", + covered + gaps_r + gaps, covered, gaps_r, gaps); } else if (!strcmp(fmt_s, "text")) { fprintf(out, "ISO 26262 Parts 6-11 Gap Report — %s (target %s)\n", cfg.project, asil); @@ -203,9 +210,10 @@ int cmd_iso26262(int argc, char **argv) r->clause, r->title, rule_s, status); } - fprintf(out, "\nSummary: %d covered, %d gap(s), %d not applicable for %s\n", - covered, gaps, na, asil); - if (gaps > 0) + fprintf(out, "\nSummary: %d covered, %d gap(s), %d recommended gap(s), " + "%d not applicable for %s\n", + covered, gaps, gaps_r, na, asil); + if (gaps > 0 || gaps_r > 0) fprintf(out, "Review gaps and add manual evidence or custom cfusa rules.\n"); } else { if (output && out != stdout) fclose(out); diff --git a/cmd/cfusa/cmd_unece.c b/cmd/cfusa/cmd_unece.c index 2d5f8dd..72e0ebd 100644 --- a/cmd/cfusa/cmd_unece.c +++ b/cmd/cfusa/cmd_unece.c @@ -146,7 +146,14 @@ int cmd_unece(int argc, char **argv) status); first = 0; } - fprintf(out, "\n ]\n}\n"); + /* §9.3's canonical status enum is satisfied|partial|gap; "manual" + * (organisational evidence, not tool-checkable) falls back to the + * fail-safe "gap" mapping for the purposes of this summary. */ + fprintf(out, + "\n ],\n" + " \"summary\": {\"total\": %d, \"satisfied\": %d, \"partial\": 0, \"gaps\": %d}\n" + "}\n", + pass + gap + manual, pass, gap + manual); } else if (!strcmp(fmt, "text")) { fprintf(out, "UN R.155 Gap Report — %s\n", cfg.project); fprintf(out, "====================================\n\n"); diff --git a/tests/test_cli_commands.c b/tests/test_cli_commands.c index 21ee6a6..c838934 100644 --- a/tests/test_cli_commands.c +++ b/tests/test_cli_commands.c @@ -399,8 +399,10 @@ void test_iec62443_json_format(void) buf[n] = '\0'; fclose(f); TEST_ASSERT_NOT_NULL(strstr(buf, "\"schemaVersion\"")); TEST_ASSERT_NOT_NULL(strstr(buf, "\"gap-report\"")); - TEST_ASSERT_NOT_NULL(strstr(buf, "\"iec62443\"")); - TEST_ASSERT_NOT_NULL(strstr(buf, "\"standard\"")); + /* x-FuSa spec §2.4.1: canonical id is "iec62443-4-2" (this command + * only implements the Component Requirements / -4-2 part), not the + * bare, non-registry command name "iec62443". */ + TEST_ASSERT_NOT_NULL(strstr(buf, "\"standard\": \"iec62443-4-2\"")); TEST_ASSERT_NOT_NULL(strstr(buf, "\"projectRoot\"")); TEST_ASSERT_NOT_NULL(strstr(buf, "\"objectives\"")); /* REQ-IEC62443-002: canonical §9.3 objective shape (id/fr/title/ From a30786012de9b95cd432674fc3f3f01679347c61 Mon Sep 17 00:00:00 2001 From: Matt Jones <47545907+SoundMatt@users.noreply.github.com> Date: Tue, 28 Jul 2026 20:25:21 -0700 Subject: [PATCH 08/12] fix: preserve attestation as stale instead of dropping it on content change MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit x-FuSa spec §1.6.2 (MUST — carry-forward across regeneration): an artifact-producing command must not silently discard an existing `attestation` object when it regenerates the artifact — it must carry the prior attestation forward unchanged, so staleness is detected via the carried-forward `contentHash` no longer matching the fresh content (falling back to "heuristic"), rather than the review record vanishing outright. `fmea`/`tara`/`safety-case`/`sas` each read the prior attestation back correctly, but then gated *emission* of the `"attestation"` block on `cfusa_qb_attestation_valid()` (independence + non-stale hash) instead of on `attestation.present`. The moment the artifact's analysed content changed — exactly the case §1.6.2 exists for — the attestation was dropped from the regenerated document entirely instead of being carried forward and re-evaluated as stale, leaving no audit trail that a review had ever happened. Gate emission on `attestation.present` (carrying the loaded object's actual `status` forward verbatim) in all four commands; validity/ staleness stays a *consumer*-side concern, as the spec text says. Closes #83. Signed-off-by: Matt Jones <47545907+SoundMatt@users.noreply.github.com> --- cmd/cfusa/cmd_fmea.c | 13 ++++++++++--- cmd/cfusa/cmd_safety_case.c | 10 +++++++--- cmd/cfusa/cmd_sas.c | 13 ++++++++++--- cmd/cfusa/cmd_tara.c | 10 +++++++--- 4 files changed, 34 insertions(+), 12 deletions(-) diff --git a/cmd/cfusa/cmd_fmea.c b/cmd/cfusa/cmd_fmea.c index 1391e08..3e1f637 100644 --- a/cmd/cfusa/cmd_fmea.c +++ b/cmd/cfusa/cmd_fmea.c @@ -373,7 +373,6 @@ int cmd_fmea(int argc, char **argv) } int qb_gate = run_quality_bar(dir, fresh_hash, &attestation, require_attestation); - int attestation_valid = cfusa_qb_attestation_valid(&attestation, fresh_hash); int cov_gate = 0; if (min_coverage > 0 && coverage_pct < min_coverage) { @@ -382,15 +381,23 @@ int cmd_fmea(int argc, char **argv) cov_gate = 1; } +/* x-FuSa spec §1.6.2 MUST (carry-forward across regeneration): a prior + * attestation is carried forward onto the regenerated document verbatim + * whenever one was read back, not only when it is still hash-valid — + * staleness is then automatic (a consumer recomputes contentHash and + * falls back to "heuristic" on mismatch), so gating *emission* on + * attestation_valid would silently erase a real prior review the moment + * content changes. */ #define WRITE_ATTESTATION(fp) do { \ - if (attestation_valid) { \ + if (attestation.present) { \ fprintf((fp), ",\n \"attestation\": {\n" \ - " \"status\": \"reviewed\",\n" \ + " \"status\": \"%s\",\n" \ " \"implementationAuthor\": \"%s\",\n" \ " \"independentReviewer\": \"%s\",\n" \ " \"reviewedAt\": \"%s\",\n" \ " \"contentHash\": \"%s\"\n" \ " }\n", \ + attestation.status[0] ? attestation.status : "heuristic", \ attestation.implementation_author, attestation.independent_reviewer, \ attestation.reviewed_at, attestation.content_hash); \ } else { \ diff --git a/cmd/cfusa/cmd_safety_case.c b/cmd/cfusa/cmd_safety_case.c index cdf3474..9405ec3 100644 --- a/cmd/cfusa/cmd_safety_case.c +++ b/cmd/cfusa/cmd_safety_case.c @@ -229,7 +229,6 @@ int cmd_safety_case(int argc, char **argv) fprintf(stderr, "cfusa safety-case: %s: node text shows low distinct-value ratio%s\n", CFUSA_QB_RULE_B, reviewed ? " (suppressed by a valid attestation)" : ""); } - int attestation_valid = cfusa_qb_attestation_valid(&attestation, fresh_hash); int qb_gate = (rule_a_hits > 0 && !rule_a_disposed) ? 1 : (require_attestation && rule_b && !reviewed) ? 1 : 0; @@ -275,15 +274,20 @@ int cmd_safety_case(int argc, char **argv) " \"totalGoals\": %d, \"goalsWithEvidence\": %d, \"undeveloped\": %d\n" " }", total_goals, goals_with_evidence, undeveloped); - if (attestation_valid) { + /* x-FuSa spec §1.6.2 MUST (carry-forward across regeneration): carry + * a prior attestation forward verbatim whenever one was read back, + * not only when it is still hash-valid — see cmd_fmea.c's + * WRITE_ATTESTATION for the full rationale. */ + if (attestation.present) { fprintf(f, ",\n \"attestation\": {\n" - " \"status\": \"reviewed\",\n" + " \"status\": \"%s\",\n" " \"implementationAuthor\": \"%s\",\n" " \"independentReviewer\": \"%s\",\n" " \"reviewedAt\": \"%s\",\n" " \"contentHash\": \"%s\"\n" " }\n", + attestation.status[0] ? attestation.status : "heuristic", attestation.implementation_author, attestation.independent_reviewer, attestation.reviewed_at, attestation.content_hash); } else { diff --git a/cmd/cfusa/cmd_sas.c b/cmd/cfusa/cmd_sas.c index 16f55c3..49448d9 100644 --- a/cmd/cfusa/cmd_sas.c +++ b/cmd/cfusa/cmd_sas.c @@ -193,7 +193,6 @@ int cmd_sas(int argc, char **argv) fprintf(stderr, "cfusa sas: %s: checklist item text shows low distinct-value ratio%s\n", CFUSA_QB_RULE_B, reviewed ? " (suppressed by a valid attestation)" : ""); } - int attestation_valid = cfusa_qb_attestation_valid(&attestation, fresh_hash); int qb_gate = (rule_a_hits > 0 && !rule_a_disposed) ? 1 : (require_attestation && rule_b && !reviewed) ? 1 : 0; @@ -245,15 +244,23 @@ int cmd_sas(int argc, char **argv) SAS_ITEMS[i+1].id ? "," : ""); } fprintf(f," ],\n \"summary\": {\"total\": %d, \"present\": %d}", total, present_count); - if (attestation_valid) { + /* x-FuSa spec §1.6.2 MUST (carry-forward across regeneration): a + * prior attestation is carried forward onto the regenerated + * document verbatim whenever one was read back, not only when it + * is still hash-valid — staleness is then automatic (a consumer + * recomputes contentHash and falls back to "heuristic" on + * mismatch), so gating *emission* on attestation_valid would + * silently erase a real prior review the moment content changes. */ + if (attestation.present) { fprintf(f, ",\n \"attestation\": {\n" - " \"status\": \"reviewed\",\n" + " \"status\": \"%s\",\n" " \"implementationAuthor\": \"%s\",\n" " \"independentReviewer\": \"%s\",\n" " \"reviewedAt\": \"%s\",\n" " \"contentHash\": \"%s\"\n" " }\n", + attestation.status[0] ? attestation.status : "heuristic", attestation.implementation_author, attestation.independent_reviewer, attestation.reviewed_at, attestation.content_hash); } else { diff --git a/cmd/cfusa/cmd_tara.c b/cmd/cfusa/cmd_tara.c index 37cb87a..693431b 100644 --- a/cmd/cfusa/cmd_tara.c +++ b/cmd/cfusa/cmd_tara.c @@ -428,7 +428,6 @@ int cmd_tara(int argc, char **argv) } int qb_gate = run_quality_bar(dir, fresh_hash, &attestation, require_attestation); - int attestation_valid = cfusa_qb_attestation_valid(&attestation, fresh_hash); int cov_gate = 0; if (min_coverage > 0 && coverage_pct < min_coverage) { @@ -460,14 +459,19 @@ int cmd_tara(int argc, char **argv) "inventory.\"\n" \ " }", \ g_asset_count, g_total_found, coverage_pct); \ - if (attestation_valid) { \ + /* x-FuSa spec §1.6.2 MUST (carry-forward across regeneration): carry a \ + * prior attestation forward verbatim whenever one was read back, not \ + * only when it is still hash-valid — see cmd_fmea.c's WRITE_ATTESTATION \ + * for the full rationale. */ \ + if (attestation.present) { \ fprintf((fp), ",\n \"attestation\": {\n" \ - " \"status\": \"reviewed\",\n" \ + " \"status\": \"%s\",\n" \ " \"implementationAuthor\": \"%s\",\n" \ " \"independentReviewer\": \"%s\",\n" \ " \"reviewedAt\": \"%s\",\n" \ " \"contentHash\": \"%s\"\n" \ " }\n", \ + attestation.status[0] ? attestation.status : "heuristic", \ attestation.implementation_author, attestation.independent_reviewer, \ attestation.reviewed_at, attestation.content_hash); \ } else { \ From 3933afab9c7a4bc27cbb9b99e7b252945d9829da Mon Sep 17 00:00:00 2001 From: Matt Jones <47545907+SoundMatt@users.noreply.github.com> Date: Tue, 28 Jul 2026 20:26:06 -0700 Subject: [PATCH 09/12] test: regression coverage for the 2026-07-28 deep-audit bug-fix sprint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds tests/test_xfusa_audit_20260728.c, one (or a small group of) test(s) per issue fixed in this sprint (#82-#91): the CFUSA-L004 suffix false positive and its real-recursion negative case, fmea's attestation carry-forward-as-stale across a content change, the canonical standard/clause fields on a lint finding plus the SARIF driver name, summary{} presence and the satisfied+partial+gaps==total invariant across all six standards commands (iso26262/iec61508/do178/iso21434/unece plus iec62443's canonical id), misra's canonical gap-report schema, qualify accepting --dir, sas writing both sas.json and a real sas.md companion, and hara init's envelope-free §1.2.5 scaffold. Each test reproduces its issue's failure mode directly against the library code (not by shelling out to the built binary), and was verified to fail against the pre-fix code before the corresponding fix commit. Signed-off-by: Matt Jones <47545907+SoundMatt@users.noreply.github.com> --- CMakeLists.txt | 1 + tests/test_xfusa_audit_20260728.c | 502 ++++++++++++++++++++++++++++++ 2 files changed, 503 insertions(+) create mode 100644 tests/test_xfusa_audit_20260728.c diff --git a/CMakeLists.txt b/CMakeLists.txt index cd27470..0d100f6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -160,6 +160,7 @@ cfusa_test(test_qualify_vv) cfusa_test(test_mcdc) cfusa_test(test_gap_coverage) cfusa_test(test_xfusa_v114) +cfusa_test(test_xfusa_audit_20260728) # ---- Installation ---- include(GNUInstallDirs) diff --git a/tests/test_xfusa_audit_20260728.c b/tests/test_xfusa_audit_20260728.c new file mode 100644 index 0000000..89cf4b5 --- /dev/null +++ b/tests/test_xfusa_audit_20260728.c @@ -0,0 +1,502 @@ +/* + * Regression tests for the 2026-07-28/29 deep-audit bug-fix sprint + * (issues #82-#91 on SoundMatt/c-FuSa): + * + * #82 CFUSA-L004 false positive on suffix-matching callee names + * #83 Attestation carry-forward dropped (not preserved-as-stale) when + * fmea/tara/safety-case/sas content changes (§1.6.2 MUST) + * #84 check/lint/analyze/cyber findings: Finding.standard is now a + * canonical id (§2.4.1) with a separate clause field, not a + * combined display string + * #85 iso26262/iec61508/do178/iso21434/unece/iec62443 gap-report JSON + * now carries a summary{total,satisfied,partial,gaps} object + * (§9.3 MUST) satisfying the total invariant + * #86 iec62443 gap-report standard id is "iec62443-4-2" (§2.4.1 MUST) + * #87 misra command uses the canonical §9.3 gap-report schema + * (kind "gap-report", standard "misra-c", objectives[]/summary{}) + * #88 qualify accepts --dir (§2.2: applies to all commands) + * #89 sas --format json (no --output) writes sas.json + sas.md + * companion, not raw JSON into a file named sas.md + * #90 hara init scaffolds .fusa-hara.json per the §1.2.5 INPUT schema + * (no report-envelope fields, no "kind": "hara") + * #91 SARIF tool.driver.name is "c-FuSa" (the §1.1 tool name), not the + * binary name "cfusa" (§2.9 MUST) + */ +#include +#include +#include +#include +#include "../vendor/unity/unity.h" +#include "cfusa/report.h" +#include "cfusa/engine.h" +#include "cfusa/config.h" + +extern int cmd_qualify(int argc, char **argv); +extern int cmd_misra(int argc, char **argv); +extern int cmd_iec62443(int argc, char **argv); +extern int cmd_iso26262(int argc, char **argv); +extern int cmd_iec61508(int argc, char **argv); +extern int cmd_do178(int argc, char **argv); +extern int cmd_iso21434(int argc, char **argv); +extern int cmd_unece(int argc, char **argv); +extern int cmd_hara(int argc, char **argv); +extern int cmd_sas(int argc, char **argv); +extern int cmd_fmea(int argc, char **argv); +extern int cmd_lint(int argc, char **argv); + +extern void cfusa_lint_register_rules(void); + +#define AUDIT_DIR "/tmp/cfusa_audit_20260728_testdir" + +void setUp(void) { (void)mkdir(AUDIT_DIR, 0700); } +void tearDown(void) {} + +static void write_file(const char *name, const char *body) +{ + char path[512]; + snprintf(path, sizeof(path), "%s/%s", AUDIT_DIR, name); + FILE *f = fopen(path, "w"); + if (f) { fputs(body, f); fclose(f); } +} + +static int file_exists(const char *name) +{ + char path[512]; + snprintf(path, sizeof(path), "%s/%s", AUDIT_DIR, name); + FILE *f = fopen(path, "r"); + if (f) { fclose(f); return 1; } + return 0; +} + +static char *slurp(const char *name, size_t *len_out) +{ + char path[512]; + snprintf(path, sizeof(path), "%s/%s", AUDIT_DIR, name); + FILE *f = fopen(path, "r"); + if (!f) return NULL; + static char buf[65536]; + size_t n = fread(buf, 1, sizeof(buf) - 1, f); + buf[n] = '\0'; + fclose(f); + if (len_out) *len_out = n; + return buf; +} + +/* ================================================================== */ +/* #82 — CFUSA-L004 suffix false positive */ +/* ================================================================== */ + +#define L004_DIR "/tmp/cfusa_audit_20260728_l004" + +/* Each of these two cases gets its own isolated, freshly-emptied + * directory — lint scans every .c file under --dir, so sharing AUDIT_DIR + * with other tests (or leftover files from a prior run of this same + * binary) would make one case's fixture pollute the other's result. */ +static void l004_reset_dir(const char *sub) +{ + char dir[512]; snprintf(dir, sizeof(dir), "%s/%s", L004_DIR, sub); + char rmcmd[600]; snprintf(rmcmd, sizeof(rmcmd), "rm -rf '%s'", dir); + (void)system(rmcmd); /* test-only cleanup of a fixed /tmp path — not + * project source under analysis */ + mkdir(L004_DIR, 0700); + mkdir(dir, 0700); +} + +//cfusa:test REQ-LINT004 +void test_l004_no_false_positive_on_suffix_match(void) +{ + char dir[512]; snprintf(dir, sizeof(dir), "%s/pos", L004_DIR); + l004_reset_dir("pos"); + char path[512]; snprintf(path, sizeof(path), "%s/l004_suffix.c", dir); + FILE *f = fopen(path, "w"); + TEST_ASSERT_NOT_NULL(f); + fputs( + "int helper_evaluate(int x) {\n" + " return x + 1;\n" + "}\n" + "\n" + "static int evaluate(int x) {\n" + " return helper_evaluate(x);\n" + "}\n", f); + fclose(f); + + cfusa_engine_reset(); + cfusa_lint_register_rules(); + cfusa_config_t cfg; cfusa_config_defaults(&cfg); + cfusa_report_t rpt; cfusa_report_init(&rpt); + cfusa_engine_run_category(CFUSA_CATEGORY_LINT, dir, &cfg, &rpt); + + int l004_count = 0; + for (int i = 0; i < rpt.count; i++) + if (!strcmp(rpt.findings[i].rule_id, "CFUSA-L004")) l004_count++; + TEST_ASSERT_EQUAL(0, l004_count); + + cfusa_report_free(&rpt); + cfusa_engine_reset(); +} + +//cfusa:test REQ-LINT004 +void test_l004_still_detects_real_recursion(void) +{ + char dir[512]; snprintf(dir, sizeof(dir), "%s/neg", L004_DIR); + l004_reset_dir("neg"); + char path[512]; snprintf(path, sizeof(path), "%s/l004_real.c", dir); + FILE *f = fopen(path, "w"); + TEST_ASSERT_NOT_NULL(f); + fputs( + "static int factorial(int n) {\n" + " if (n <= 1) return 1;\n" + " return n * factorial(n - 1);\n" + "}\n", f); + fclose(f); + + cfusa_engine_reset(); + cfusa_lint_register_rules(); + cfusa_config_t cfg; cfusa_config_defaults(&cfg); + cfusa_report_t rpt; cfusa_report_init(&rpt); + cfusa_engine_run_category(CFUSA_CATEGORY_LINT, dir, &cfg, &rpt); + + int l004_count = 0; + for (int i = 0; i < rpt.count; i++) + if (!strcmp(rpt.findings[i].rule_id, "CFUSA-L004")) l004_count++; + TEST_ASSERT_TRUE(l004_count > 0); + + cfusa_report_free(&rpt); + cfusa_engine_reset(); +} + +/* ================================================================== */ +/* #83 — attestation carry-forward preserved-as-stale */ +/* ================================================================== */ + +//cfusa:test REQ-QB001 +void test_fmea_attestation_carried_forward_when_content_changes(void) +{ + write_file(".fusa.json", + "{\"project\":\"demo\",\"standard\":\"iso26262\"}\n"); + write_file("src_main.c", + "//fusa:req REQ-DEMO001\n" + "int add(int a, int b) { return a + b; }\n"); + + char dir[512]; snprintf(dir, sizeof(dir), "%s", AUDIT_DIR); + + char *argv1[] = {"cfusa", "--dir", dir, "--format", "json", + "--attest", "Jane Doe ", NULL}; + cmd_fmea(6, argv1); + + size_t len1 = 0; + char *buf1 = slurp("fmea.json", &len1); + TEST_ASSERT_NOT_NULL(buf1); + TEST_ASSERT_NOT_NULL(strstr(buf1, "\"attestation\"")); + TEST_ASSERT_NOT_NULL(strstr(buf1, "jane@example.com")); + + /* Change the analyzed content and regenerate WITHOUT --attest. */ + write_file("src_main2.c", + "//fusa:req REQ-DEMO001\n" + "int subtract(int a, int b) { return a - b; }\n"); + + char *argv2[] = {"cfusa", "--dir", dir, "--format", "json", NULL}; + cmd_fmea(4, argv2); + + size_t len2 = 0; + char *buf2 = slurp("fmea.json", &len2); + TEST_ASSERT_NOT_NULL(buf2); + /* The prior attestation MUST still be present (carried forward, + * unchanged) — not silently dropped just because its contentHash + * is now stale relative to the freshly-generated content. */ + TEST_ASSERT_NOT_NULL(strstr(buf2, "\"attestation\"")); + TEST_ASSERT_NOT_NULL(strstr(buf2, "jane@example.com")); +} + +/* ================================================================== */ +/* #84 — Finding.standard is canonical id + clause */ +/* ================================================================== */ + +//cfusa:test REQ-LINT001 +void test_lint_finding_standard_is_canonical_id_with_clause(void) +{ + write_file("l001_long.c", + "int fn(void) {\n" + " goto done;\n" + "done:\n" + " return 0;\n" + "}\n"); + + cfusa_engine_reset(); + cfusa_lint_register_rules(); + cfusa_config_t cfg; cfusa_config_defaults(&cfg); + cfusa_report_t rpt; cfusa_report_init(&rpt); + strncpy(rpt.project_root, AUDIT_DIR, sizeof(rpt.project_root) - 1); + cfusa_engine_run_category(CFUSA_CATEGORY_LINT, AUDIT_DIR, &cfg, &rpt); + + char path[512]; + snprintf(path, sizeof(path), "%s/check.json", AUDIT_DIR); + TEST_ASSERT_TRUE(cfusa_report_write(&rpt, path, FMT_JSON) == 0); + + size_t len = 0; + char *buf = slurp("check.json", &len); + TEST_ASSERT_NOT_NULL(buf); + /* x-FuSa spec §2.4.1: standard is a canonical lowercase id, never a + * display string like "MISRA-C:2012 R15.1"; the clause carries the + * rule reference separately. */ + TEST_ASSERT_NOT_NULL(strstr(buf, "\"standard\": \"misra-c\"")); + TEST_ASSERT_NOT_NULL(strstr(buf, "\"clause\": \"R15.1\"")); + TEST_ASSERT_NULL(strstr(buf, "MISRA-C:2012")); + + cfusa_report_free(&rpt); + cfusa_engine_reset(); +} + +//cfusa:test REQ-RPT001 +void test_sarif_rule_declarations_use_canonical_standard_id(void) +{ + write_file("l002_goto.c", + "int fn(int x) {\n" + " if (x) goto done;\n" + "done:\n" + " return 0;\n" + "}\n"); + + cfusa_engine_reset(); + cfusa_lint_register_rules(); + cfusa_config_t cfg; cfusa_config_defaults(&cfg); + cfusa_report_t rpt; cfusa_report_init(&rpt); + strncpy(rpt.project_root, AUDIT_DIR, sizeof(rpt.project_root) - 1); + cfusa_engine_run_category(CFUSA_CATEGORY_LINT, AUDIT_DIR, &cfg, &rpt); + + char path[512]; + snprintf(path, sizeof(path), "%s/check.sarif", AUDIT_DIR); + TEST_ASSERT_TRUE(cfusa_report_write(&rpt, path, FMT_SARIF) == 0); + + size_t len = 0; + char *buf = slurp("check.sarif", &len); + TEST_ASSERT_NOT_NULL(buf); + /* #91: tool.driver.name is the §1.1 tool name "c-FuSa", not the + * binary name "cfusa". */ + TEST_ASSERT_NOT_NULL(strstr(buf, "\"name\": \"c-FuSa\"")); + + cfusa_report_free(&rpt); + cfusa_engine_reset(); +} + +/* ================================================================== */ +/* #85 — gap-report summary{} object across all six standards commands */ +/* ================================================================== */ + +static void assert_gap_report_has_valid_summary(const char *buf) +{ + TEST_ASSERT_NOT_NULL(strstr(buf, "\"summary\"")); + const char *s = strstr(buf, "\"summary\""); + TEST_ASSERT_NOT_NULL(s); + int total = -1, satisfied = -1, partial = -1, gaps = -1; + /* Cheap, order-independent extraction — good enough for a regression + * test that mainly wants to confirm the object's presence and the + * satisfied+partial+gaps == total invariant. */ + sscanf(strstr(s, "\"total\":"), "\"total\": %d", &total); + sscanf(strstr(s, "\"satisfied\":"), "\"satisfied\": %d", &satisfied); + sscanf(strstr(s, "\"partial\":"), "\"partial\": %d", &partial); + sscanf(strstr(s, "\"gaps\":"), "\"gaps\": %d", &gaps); + TEST_ASSERT_TRUE(total >= 0); + TEST_ASSERT_EQUAL(total, satisfied + partial + gaps); +} + +//cfusa:test REQ-ISO26262 +void test_iso26262_gap_report_has_summary(void) +{ + char dir[512]; snprintf(dir, sizeof(dir), "%s", AUDIT_DIR); + char *argv[] = {"cfusa", "--dir", dir, "--format", "json", NULL}; + cmd_iso26262(5, argv); + /* stdout capture is awkward in-process; write via --output instead. */ + char out[256]; snprintf(out, sizeof(out), "%s/iso26262.json", AUDIT_DIR); + char *argv2[] = {"cfusa", "--dir", dir, "--format", "json", "--output", out, NULL}; + cmd_iso26262(7, argv2); + char *buf = slurp("iso26262.json", NULL); + TEST_ASSERT_NOT_NULL(buf); + assert_gap_report_has_valid_summary(buf); +} + +//cfusa:test REQ-IEC61508 +void test_iec61508_gap_report_has_summary(void) +{ + char dir[512]; snprintf(dir, sizeof(dir), "%s", AUDIT_DIR); + char out[256]; snprintf(out, sizeof(out), "%s/iec61508.json", AUDIT_DIR); + char *argv[] = {"cfusa", "--dir", dir, "--format", "json", "--output", out, NULL}; + cmd_iec61508(7, argv); + char *buf = slurp("iec61508.json", NULL); + TEST_ASSERT_NOT_NULL(buf); + assert_gap_report_has_valid_summary(buf); +} + +//cfusa:test REQ-DO178 +void test_do178_gap_report_has_summary(void) +{ + char dir[512]; snprintf(dir, sizeof(dir), "%s", AUDIT_DIR); + char out[256]; snprintf(out, sizeof(out), "%s/do178.json", AUDIT_DIR); + char *argv[] = {"cfusa", "--dir", dir, "--format", "json", "--output", out, NULL}; + cmd_do178(7, argv); + char *buf = slurp("do178.json", NULL); + TEST_ASSERT_NOT_NULL(buf); + assert_gap_report_has_valid_summary(buf); +} + +//cfusa:test REQ-ISO21434-001 +void test_iso21434_gap_report_has_summary(void) +{ + char dir[512]; snprintf(dir, sizeof(dir), "%s", AUDIT_DIR); + char out[256]; snprintf(out, sizeof(out), "%s/iso21434.json", AUDIT_DIR); + char *argv[] = {"cfusa", "--dir", dir, "--format", "json", "--output", out, NULL}; + cmd_iso21434(7, argv); + char *buf = slurp("iso21434.json", NULL); + TEST_ASSERT_NOT_NULL(buf); + assert_gap_report_has_valid_summary(buf); +} + +//cfusa:test REQ-UNECE-OUT001 +void test_unece_gap_report_has_summary(void) +{ + char dir[512]; snprintf(dir, sizeof(dir), "%s", AUDIT_DIR); + char out[256]; snprintf(out, sizeof(out), "%s/unece.json", AUDIT_DIR); + char *argv[] = {"cfusa", "--dir", dir, "--format", "json", "--output", out, NULL}; + cmd_unece(7, argv); + char *buf = slurp("unece.json", NULL); + TEST_ASSERT_NOT_NULL(buf); + assert_gap_report_has_valid_summary(buf); +} + +/* ================================================================== */ +/* #86 — iec62443 canonical standard id + summary (also covers #85) */ +/* ================================================================== */ + +//cfusa:test REQ-IEC62443-001 +void test_iec62443_standard_id_is_4_2_and_has_summary(void) +{ + char dir[512]; snprintf(dir, sizeof(dir), "%s", AUDIT_DIR); + char out[256]; snprintf(out, sizeof(out), "%s/iec62443.json", AUDIT_DIR); + char *argv[] = {"cfusa", "--dir", dir, "--format", "json", "--output", out, NULL}; + cmd_iec62443(7, argv); + char *buf = slurp("iec62443.json", NULL); + TEST_ASSERT_NOT_NULL(buf); + TEST_ASSERT_NOT_NULL(strstr(buf, "\"standard\": \"iec62443-4-2\"")); + assert_gap_report_has_valid_summary(buf); +} + +/* ================================================================== */ +/* #87 — misra canonical gap-report schema */ +/* ================================================================== */ + +//cfusa:test REQ-MISRA +void test_misra_uses_canonical_gap_report_schema(void) +{ + char dir[512]; snprintf(dir, sizeof(dir), "%s", AUDIT_DIR); + char out[256]; snprintf(out, sizeof(out), "%s/misra.json", AUDIT_DIR); + char *argv[] = {"cfusa", "--dir", dir, "--format", "json", "--output", out, NULL}; + cmd_misra(7, argv); + char *buf = slurp("misra.json", NULL); + TEST_ASSERT_NOT_NULL(buf); + TEST_ASSERT_NOT_NULL(strstr(buf, "\"kind\": \"gap-report\"")); + TEST_ASSERT_NOT_NULL(strstr(buf, "\"standard\": \"misra-c\"")); + TEST_ASSERT_NOT_NULL(strstr(buf, "\"objectives\"")); + TEST_ASSERT_NULL(strstr(buf, "\"misra-coverage\"")); + TEST_ASSERT_NULL(strstr(buf, "\"rules\":")); + assert_gap_report_has_valid_summary(buf); +} + +/* ================================================================== */ +/* #88 — qualify accepts --dir */ +/* ================================================================== */ + +//cfusa:test REQ-QUAL006 +void test_qualify_accepts_dir_flag(void) +{ + char dir[512]; snprintf(dir, sizeof(dir), "%s", AUDIT_DIR); + char *argv[] = {"cfusa", "--dir", dir, "--format", "json", NULL}; + int rc = cmd_qualify(5, argv); + /* §2.2: --dir applies to all commands — this MUST NOT be a usage + * error (exit 2) the way it was before the fix. */ + TEST_ASSERT_TRUE(rc != 2); +} + +/* ================================================================== */ +/* #89 — sas --format json writes sas.json + sas.md companion */ +/* ================================================================== */ + +//cfusa:test REQ-SAS001 +void test_sas_json_format_writes_sas_json_and_md_companion(void) +{ + char dir[512]; snprintf(dir, sizeof(dir), "%s", AUDIT_DIR); + char *argv[] = {"cfusa", "--dir", dir, "--format", "json", NULL}; + cmd_sas(4, argv); + + TEST_ASSERT_TRUE(file_exists("sas.json")); + TEST_ASSERT_TRUE(file_exists("sas.md")); + + size_t jlen = 0; + char *jbuf = slurp("sas.json", &jlen); + TEST_ASSERT_NOT_NULL(jbuf); + TEST_ASSERT_NOT_NULL(strstr(jbuf, "\"schemaVersion\"")); + TEST_ASSERT_NOT_NULL(strstr(jbuf, "\"kind\": \"sas\"")); + + size_t mlen = 0; + char *mbuf = slurp("sas.md", &mlen); + TEST_ASSERT_NOT_NULL(mbuf); + /* sas.md must be real Markdown, not the JSON body from a stale + * "did we already write sas.md?" check. */ + TEST_ASSERT_NOT_NULL(strstr(mbuf, "# Software Accomplishment Summary")); + TEST_ASSERT_NULL(strstr(mbuf, "\"schemaVersion\"")); +} + +/* ================================================================== */ +/* #90 — hara init scaffolds the §1.2.5 INPUT schema (no envelope) */ +/* ================================================================== */ + +//cfusa:test REQ-HARA001 +void test_hara_init_scaffold_has_no_report_envelope_fields(void) +{ + char dir[512]; snprintf(dir, sizeof(dir), "%s", AUDIT_DIR); + char *argv[] = {"cfusa", "init", "--dir", dir, NULL}; + cmd_hara(4, argv); + + TEST_ASSERT_TRUE(file_exists(".fusa-hara.json")); + char *buf = slurp(".fusa-hara.json", NULL); + TEST_ASSERT_NOT_NULL(buf); + /* §1.2.5: an INPUT file, not a report document — no §3.1 envelope. */ + TEST_ASSERT_NULL(strstr(buf, "\"schemaVersion\"")); + TEST_ASSERT_NULL(strstr(buf, "\"kind\"")); + TEST_ASSERT_NULL(strstr(buf, "\"toolVersion\"")); + TEST_ASSERT_NOT_NULL(strstr(buf, "\"operationalSituations\"")); + TEST_ASSERT_NOT_NULL(strstr(buf, "\"hazards\"")); + TEST_ASSERT_NOT_NULL(strstr(buf, "\"safetyGoals\"")); + + char path[512]; snprintf(path, sizeof(path), "%s/.fusa-hara.json", AUDIT_DIR); + (void)remove(path); +} + +int main(void) +{ + UNITY_BEGIN(); + RUN_TEST(test_l004_no_false_positive_on_suffix_match); + RUN_TEST(test_l004_still_detects_real_recursion); + + RUN_TEST(test_fmea_attestation_carried_forward_when_content_changes); + + RUN_TEST(test_lint_finding_standard_is_canonical_id_with_clause); + RUN_TEST(test_sarif_rule_declarations_use_canonical_standard_id); + + RUN_TEST(test_iso26262_gap_report_has_summary); + RUN_TEST(test_iec61508_gap_report_has_summary); + RUN_TEST(test_do178_gap_report_has_summary); + RUN_TEST(test_iso21434_gap_report_has_summary); + RUN_TEST(test_unece_gap_report_has_summary); + + RUN_TEST(test_iec62443_standard_id_is_4_2_and_has_summary); + + RUN_TEST(test_misra_uses_canonical_gap_report_schema); + + RUN_TEST(test_qualify_accepts_dir_flag); + + RUN_TEST(test_sas_json_format_writes_sas_json_and_md_companion); + + RUN_TEST(test_hara_init_scaffold_has_no_report_envelope_fields); + + return UNITY_END(); +} From de6334af2f80f3ca73f113f5d76ae789e38bf183 Mon Sep 17 00:00:00 2001 From: Matt Jones <47545907+SoundMatt@users.noreply.github.com> Date: Tue, 28 Jul 2026 20:26:19 -0700 Subject: [PATCH 10/12] chore: bump version to 0.5.48 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 2026-07-28/29 deep-audit bug-fix sprint (issues #82-#91) — see include/cfusa/version.h for the itemised change list. Signed-off-by: Matt Jones <47545907+SoundMatt@users.noreply.github.com> --- CMakeLists.txt | 2 +- include/cfusa/version.h | 22 +++++++++++++++++++--- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 0d100f6..e98e80d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.16) project(cfusa - VERSION 0.5.47 + VERSION 0.5.48 DESCRIPTION "C functional safety toolkit" LANGUAGES C ) diff --git a/include/cfusa/version.h b/include/cfusa/version.h index 546ce72..efd2c9f 100644 --- a/include/cfusa/version.h +++ b/include/cfusa/version.h @@ -3,8 +3,8 @@ #define CFUSA_VERSION_MAJOR 0 #define CFUSA_VERSION_MINOR 5 -#define CFUSA_VERSION_PATCH 47 -#define CFUSA_VERSION_STRING "0.5.47" +#define CFUSA_VERSION_PATCH 48 +#define CFUSA_VERSION_STRING "0.5.48" #define CFUSA_SCHEMA_VERSION "1.15.0" /* Bumped from 1.14.0 to 1.15.0: adopts the x-FuSa master spec's v1.15.0 * attestation-carry-forward MUST (already conformant for fmea/tara/ @@ -24,7 +24,23 @@ * path; HARA's stored `risk.asil` is now cross-checked against the S x E x * C table both in `hara --format json`'s completeness block and as a new * `check` engine rule (HARA006), not just a text-mode warning. See issues - * #73-80. */ + * #73-80. + * + * v0.5.48 — 2026-07-28/29 deep-audit bug-fix sprint (issues #82-91): + * SARIF tool.driver.name now "c-FuSa" (§2.9); hara init's scaffold matches + * the §1.2.5 INPUT schema (no report envelope, no "kind": "hara"); sas + * --format json now writes real sas.json + sas.md companion instead of raw + * JSON into a file named sas.md; qualify accepts --dir (§2.2); misra now + * uses the canonical §9.3 gap-report schema; iec62443's standard id is + * "iec62443-4-2" (§2.4.1); iso26262/iec61508/do178/iso21434/unece/iec62443 + * gap-report JSON now carries a summary{total,satisfied,partial,gaps} + * object (§9.3 MUST); check/lint/analyze/cyber findings emit a canonical + * `standard` id with a separate `clause` field instead of a combined + * display string (§2.4.1); fmea/tara/safety-case/sas now carry a prior + * attestation forward verbatim (preserved-as-stale) instead of dropping it + * the moment content changes (§1.6.2 MUST); CFUSA-L004 no longer + * false-positives when a callee's name merely has the caller's name as a + * suffix. */ #define CFUSA_SPEC_VERSION "1.15.0" #endif /* CFUSA_VERSION_H */ From 9b7a3a8c03479e0ab3a72e1b0d46e8c2005b0e0d Mon Sep 17 00:00:00 2001 From: Matt Jones <47545907+SoundMatt@users.noreply.github.com> Date: Tue, 28 Jul 2026 20:32:13 -0700 Subject: [PATCH 11/12] fix: correct off-by-one argc in three test_xfusa_audit_20260728.c cases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three synthetic argv[] arrays were passed to cmd_fmea()/cmd_sas() with argc one less than the array's actual element count (excluding the NULL terminator) — e.g. a 7-element argv (including "--attest" and its value) called with argc=6. This under-count wasn't visible to getopt_long on macOS/BSD libc, but on glibc (the CI Linux runners) it made getopt_long report "--attest requires an argument" and left the command in an inconsistent state that went on to segfault, failing the Linux legs of CI while passing locally. Verified against a real Ubuntu 22.04 container (matching the CI image) with both gcc and the system toolchain: all 40 suites now pass there, where test_xfusa_audit_20260728 previously segfaulted. Signed-off-by: Matt Jones <47545907+SoundMatt@users.noreply.github.com> --- tests/test_xfusa_audit_20260728.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_xfusa_audit_20260728.c b/tests/test_xfusa_audit_20260728.c index 89cf4b5..af4d5c1 100644 --- a/tests/test_xfusa_audit_20260728.c +++ b/tests/test_xfusa_audit_20260728.c @@ -182,7 +182,7 @@ void test_fmea_attestation_carried_forward_when_content_changes(void) char *argv1[] = {"cfusa", "--dir", dir, "--format", "json", "--attest", "Jane Doe ", NULL}; - cmd_fmea(6, argv1); + cmd_fmea(7, argv1); size_t len1 = 0; char *buf1 = slurp("fmea.json", &len1); @@ -196,7 +196,7 @@ void test_fmea_attestation_carried_forward_when_content_changes(void) "int subtract(int a, int b) { return a - b; }\n"); char *argv2[] = {"cfusa", "--dir", dir, "--format", "json", NULL}; - cmd_fmea(4, argv2); + cmd_fmea(5, argv2); size_t len2 = 0; char *buf2 = slurp("fmea.json", &len2); @@ -425,7 +425,7 @@ void test_sas_json_format_writes_sas_json_and_md_companion(void) { char dir[512]; snprintf(dir, sizeof(dir), "%s", AUDIT_DIR); char *argv[] = {"cfusa", "--dir", dir, "--format", "json", NULL}; - cmd_sas(4, argv); + cmd_sas(5, argv); TEST_ASSERT_TRUE(file_exists("sas.json")); TEST_ASSERT_TRUE(file_exists("sas.md")); From d1328f1330e700f3aaf235f7d7319d217ad04cf5 Mon Sep 17 00:00:00 2001 From: Matt Jones <47545907+SoundMatt@users.noreply.github.com> Date: Tue, 28 Jul 2026 20:39:29 -0700 Subject: [PATCH 12/12] fix: address CodeQL/self-check findings in the new audit regression test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CodeQL (3 high-severity) flagged three fopen(path, "w") call sites in tests/test_xfusa_audit_20260728.c as creating world-writable files — the same class of finding the repo already fixed elsewhere by routing file creation through cfusa_fopen_write() (restrictive mode + O_EXCL-ish semantics) instead of raw fopen(). Switch all three sites to it. c-FuSa's own self-check (CFUSA-CY003 / CWE-78) flagged l004_reset_dir()'s system("rm -rf ...") cleanup helper as OS command execution with a non-literal argument. The helper never needed shell semantics in the first place — each fixture subdirectory only ever holds one specific, statically-known file — so replace it with direct remove()/rmdir() calls instead of shelling out. Signed-off-by: Matt Jones <47545907+SoundMatt@users.noreply.github.com> --- tests/test_xfusa_audit_20260728.c | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/tests/test_xfusa_audit_20260728.c b/tests/test_xfusa_audit_20260728.c index af4d5c1..ef85b46 100644 --- a/tests/test_xfusa_audit_20260728.c +++ b/tests/test_xfusa_audit_20260728.c @@ -26,10 +26,12 @@ #include #include #include +#include #include "../vendor/unity/unity.h" #include "cfusa/report.h" #include "cfusa/engine.h" #include "cfusa/config.h" +#include "cfusa/utils.h" extern int cmd_qualify(int argc, char **argv); extern int cmd_misra(int argc, char **argv); @@ -55,7 +57,7 @@ static void write_file(const char *name, const char *body) { char path[512]; snprintf(path, sizeof(path), "%s/%s", AUDIT_DIR, name); - FILE *f = fopen(path, "w"); + FILE *f = cfusa_fopen_write(path); if (f) { fputs(body, f); fclose(f); } } @@ -91,13 +93,18 @@ static char *slurp(const char *name, size_t *len_out) /* Each of these two cases gets its own isolated, freshly-emptied * directory — lint scans every .c file under --dir, so sharing AUDIT_DIR * with other tests (or leftover files from a prior run of this same - * binary) would make one case's fixture pollute the other's result. */ + * binary) would make one case's fixture pollute the other's result. + * Each subdirectory only ever holds one known fixture file, so cleanup + * unlinks it directly (no shelling out via system() for what is really + * just "remove these two known paths"). */ static void l004_reset_dir(const char *sub) { char dir[512]; snprintf(dir, sizeof(dir), "%s/%s", L004_DIR, sub); - char rmcmd[600]; snprintf(rmcmd, sizeof(rmcmd), "rm -rf '%s'", dir); - (void)system(rmcmd); /* test-only cleanup of a fixed /tmp path — not - * project source under analysis */ + char f1[600]; snprintf(f1, sizeof(f1), "%s/l004_suffix.c", dir); + char f2[600]; snprintf(f2, sizeof(f2), "%s/l004_real.c", dir); + (void)remove(f1); + (void)remove(f2); + (void)rmdir(dir); mkdir(L004_DIR, 0700); mkdir(dir, 0700); } @@ -108,7 +115,7 @@ void test_l004_no_false_positive_on_suffix_match(void) char dir[512]; snprintf(dir, sizeof(dir), "%s/pos", L004_DIR); l004_reset_dir("pos"); char path[512]; snprintf(path, sizeof(path), "%s/l004_suffix.c", dir); - FILE *f = fopen(path, "w"); + FILE *f = cfusa_fopen_write(path); TEST_ASSERT_NOT_NULL(f); fputs( "int helper_evaluate(int x) {\n" @@ -141,7 +148,7 @@ void test_l004_still_detects_real_recursion(void) char dir[512]; snprintf(dir, sizeof(dir), "%s/neg", L004_DIR); l004_reset_dir("neg"); char path[512]; snprintf(path, sizeof(path), "%s/l004_real.c", dir); - FILE *f = fopen(path, "w"); + FILE *f = cfusa_fopen_write(path); TEST_ASSERT_NOT_NULL(f); fputs( "static int factorial(int n) {\n"