diff --git a/CMakeLists.txt b/CMakeLists.txt index cd27470..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 ) @@ -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/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_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_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_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" 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_lint.c b/cmd/cfusa/cmd_lint.c index f7ebe9d..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, @@ -502,25 +528,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_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/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); 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_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/cmd/cfusa/cmd_sas.c b/cmd/cfusa/cmd_sas.c index 93f26a2..49448d9 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); @@ -184,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; @@ -236,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 { \ 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/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/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 */ 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_cli_commands.c b/tests/test_cli_commands.c index ca69c02..c838934 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\"")); } } @@ -395,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/ 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 ---- */ diff --git a/tests/test_xfusa_audit_20260728.c b/tests/test_xfusa_audit_20260728.c new file mode 100644 index 0000000..ef85b46 --- /dev/null +++ b/tests/test_xfusa_audit_20260728.c @@ -0,0 +1,509 @@ +/* + * 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 +#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); +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 = cfusa_fopen_write(path); + 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. + * 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 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); +} + +//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 = cfusa_fopen_write(path); + 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 = cfusa_fopen_write(path); + 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(7, 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(5, 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(5, 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(); +}