Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .fusa-reqs.json
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,9 @@
{"id":"REQ-FIX005","title":"cfusa fix provides remediation guidance for CFUSA-CY006 (free-without-NULL, CWE-416/CERT-C MEM30-C): set the pointer to NULL immediately after free()","standard":"c-FuSa CLI","level":"ASIL-A"},
{"id":"REQ-FIX006","title":"cfusa fix --dry-run/--apply auto-fixes CFUSA-CY006 by inserting '<lvalue> = NULL;' after a bare free(<lvalue>); call, skipping insertion when already NULL'd on the next line, and never writing files under --dry-run","standard":"c-FuSa CLI","level":"ASIL-A"},
{"id":"REQ-FIX007","title":"cfusa fix provides remediation guidance for every registered lint/analyze/cyber rule that has a well-defined recommended fix","standard":"c-FuSa CLI","level":"ASIL-A"},
{"id":"REQ-EXPLAIN001","title":"cfusa explain <RULE-ID> prints the rule's name, category, standard/clause citation, description, and fix guidance (or a clear note when none exists)","standard":"c-FuSa CLI","level":"ASIL-A"},
{"id":"REQ-EXPLAIN002","title":"cfusa explain returns a usage error (2) with no RULE-ID and a not-found error (1) for an unknown rule id","standard":"c-FuSa CLI","level":"ASIL-A"},
{"id":"REQ-EXPLAIN003","title":"cfusa explain accepts a rule id case-insensitively and without requiring the 'CFUSA-' prefix","standard":"c-FuSa CLI","level":"ASIL-A"},
{"id":"REQ-FMEA003","title":"cfusa fmea --help returns zero","standard":"IEC 60812","level":"ASIL-A"},
{"id":"REQ-FMEA004","title":"cfusa fmea --format json writes fmea.json","standard":"IEC 60812","level":"ASIL-A"},
{"id":"REQ-FMEA005","title":"cfusa fmea --format csv writes fmea.csv","standard":"IEC 60812","level":"ASIL-A"},
Expand Down
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ add_library(cfusa_cmds STATIC
cmd/cfusa/cmd_badge.c
cmd/cfusa/cmd_req.c
cmd/cfusa/cmd_fix.c
cmd/cfusa/cmd_explain.c
cmd/cfusa/cmd_hooks.c
cmd/cfusa/cmd_sign.c
cmd/cfusa/cmd_do178.c
Expand Down Expand Up @@ -172,6 +173,7 @@ cfusa_test(test_xfusa_audit_20260728)
cfusa_test(test_severity)
cfusa_test(test_coverage_asil)
cfusa_test(test_disposition_enforcement)
cfusa_test(test_explain)

# ---- Installation ----
include(GNUInstallDirs)
Expand Down
2 changes: 1 addition & 1 deletion cmd/cfusa/cmd_capabilities.c
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ int cmd_capabilities(int argc, char **argv)
" \"coupling\", \"cyber\", \"vuln\", \"boundary\", \"coverage\", \"diff\", \"badge\",\n"
" \"iso26262\", \"iec61508\", \"iec62443\", \"slsa\", \"do178\", \"iso21434\", \"unece\",\n"
" \"lint\", \"analyze\", \"disposition\", \"baseline\", \"sign\", \"sas\", \"sci\", \"pr\", \"req\",\n"
" \"fix\", \"hooks\", \"impact\", \"metrics\", \"comp\", \"template\", \"misra\"\n"
" \"fix\", \"explain\", \"hooks\", \"impact\", \"metrics\", \"comp\", \"template\", \"misra\"\n"
" ],\n"
" \"formats\": {\n"
" \"check\": [\"text\", \"json\", \"sarif\", \"html\", \"md\"],\n"
Expand Down
1 change: 1 addition & 0 deletions cmd/cfusa/cmd_dispatch.c
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const cfusa_command_t CFUSA_COMMANDS[] = {
{"badge", "SVG status badge from report", cmd_badge},
{"req", "Show source locations for a requirement ID", cmd_req},
{"fix", "Apply mechanical auto-fixes", cmd_fix},
{"explain", "Show a rule's description, standard, and fix guidance", cmd_explain},
{"hooks", "Install / remove git pre-commit hook", cmd_hooks},
{"sign", "HMAC-SHA256 file signing and verification", cmd_sign},
{"do178", "DO-178C Annex A objective gap report", cmd_do178},
Expand Down
168 changes: 168 additions & 0 deletions cmd/cfusa/cmd_explain.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
//cfusa:req REQ-EXPLAIN001 REQ-EXPLAIN002 REQ-EXPLAIN003

/*
* cfusa explain <RULE-ID> — issue #212: onboarding aid. Prints a rule's
* full metadata (name, standard/clause citation, description) plus its
* remediation guidance from `cfusa fix`'s table, in one place, so a team
* new to the tool doesn't have to go read the rule's source to understand
* a finding.
*
* Deliberately does NOT introduce a second, separately-authored writeup
* table: cfusa_rule_t already carries id/category/name/description/
* standard_id/clause for every registered rule (both whole-file rules and
* the line-rule metadata stubs cfusa_engine_register_line_rule() creates
* — see engine.h), and cfusa_fix_lookup() (cfusa/fix.h) already carries
* remediation guidance for every rule that has one. This command is a
* thin, hand-authoring-free presentation layer over both.
*/
#if defined(__linux__) || defined(__unix__)
# define _GNU_SOURCE
#endif
#include <ctype.h>
#include <stdio.h>
#include <string.h>
#include "cfusa/engine.h"
#include "cfusa/fix.h"

/* Registers every rule set so the full metadata table is populated,
* regardless of which category the requested rule id belongs to. Mirrors
* cmd_check.c/cmd_fix.c's own registration calls. */
static void register_all_rules(void)
{
cfusa_engine_reset();
cfusa_lint_register_rules();
cfusa_analyze_register_rules();
cfusa_cyber_register_rules();
cfusa_safety_register_rules();
}

/* Skips a leading "CFUSA-"/"CFUSA_" prefix (case-insensitive), if present.
* Rule ids outside the lint/analyze/cyber families (COMP001, HARA001, ...)
* never carry the prefix to begin with, so this is a no-op for them. */
static const char *skip_cfusa_prefix(const char *id)
{
if ((id[0] == 'c' || id[0] == 'C') && (id[1] == 'f' || id[1] == 'F') &&
(id[2] == 'u' || id[2] == 'U') && (id[3] == 's' || id[3] == 'S') &&
(id[4] == 'a' || id[4] == 'A') && (id[5] == '-' || id[5] == '_'))
Comment on lines +44 to +46
return id + 6;
return id;
}

/* Findings/rule ids are case-sensitive elsewhere in the tool, but a human
* typing `cfusa explain cy006` at a shell prompt shouldn't have to
* remember the exact "CFUSA-" prefix and casing — strip that prefix from
* both sides, then compare case-insensitively (also ignoring any stray
* '-'/'_' separator so "cy-006" and "cy_006" match too). */
static int ids_match_loosely(const char *a, const char *b)
{
a = skip_cfusa_prefix(a);
b = skip_cfusa_prefix(b);
while (*a || *b) {
while (*a == '-' || *a == '_') a++;
while (*b == '-' || *b == '_') b++;
if (!*a || !*b) break;
if (tolower((unsigned char)*a) != tolower((unsigned char)*b)) return 0;
a++; b++;
}
return *a == '\0' && *b == '\0';
}

static const cfusa_rule_t *find_rule_loosely(const char *want)
{
int n = cfusa_engine_rule_count();
/* Exact match first, so an id that happens to be a substring/loose
* match of a different rule's id never shadows the real one. */
for (int i = 0; i < n; i++) {
const cfusa_rule_t *r = cfusa_engine_get_rule(i);
if (!strcmp(r->id, want)) return r;
}
for (int i = 0; i < n; i++) {
const cfusa_rule_t *r = cfusa_engine_get_rule(i);
if (ids_match_loosely(r->id, want)) return r;
}
return NULL;
}

static void print_wrapped_guidance(const char *g)
{
char line_buf[256];
while (*g) {
const char *nl = strchr(g, '\n');
size_t len = nl ? (size_t)(nl - g) : strlen(g);
if (len >= sizeof(line_buf)) len = sizeof(line_buf) - 1;
memcpy(line_buf, g, len); line_buf[len] = '\0';
printf(" %s\n", line_buf);
if (!nl) break;
g = nl + 1;
}
}

static void print_rule_ids(void)
{
int n = cfusa_engine_rule_count();
const char *last_category = "";
for (int i = 0; i < n; i++) {
const cfusa_rule_t *r = cfusa_engine_get_rule(i);
if (strcmp(r->category, last_category) != 0) {
printf("\n%s:\n", r->category);
last_category = r->category;
}
printf(" %-16s %s\n", r->id, r->name);
}
}

int cmd_explain(int argc, char **argv)
{
if (argc >= 2 && (!strcmp(argv[1], "--help") || !strcmp(argv[1], "-h"))) {
printf("Usage: cfusa explain <RULE-ID>\n"
" cfusa explain --list\n\n"
"Prints a rule's full description, standard/clause citation, and\n"
"remediation guidance (when available) in one place.\n"
" --list List every registered rule id, grouped by category\n");
return 0;
}

register_all_rules();

if (argc >= 2 && (!strcmp(argv[1], "--list") || !strcmp(argv[1], "-l"))) {
print_rule_ids();
return 0;
}

if (argc < 2) {
fprintf(stderr,
"cfusa explain: missing RULE-ID\n"
"Usage: cfusa explain <RULE-ID> (or: cfusa explain --list)\n");
return 2;
}

const char *want = argv[1];
const cfusa_rule_t *r = find_rule_loosely(want);
if (!r) {
fprintf(stderr, "cfusa explain: unknown rule '%s'\n", want);
fprintf(stderr, "Run 'cfusa explain --list' to see every registered rule id.\n");
return 1;
}

printf("%s — %s\n", r->id, r->name);
printf("Category: %s\n", r->category);
if (r->standard_id && r->standard_id[0]) {
if (r->clause && r->clause[0])
printf("Standard: %s %s\n", r->standard_id, r->clause);
else
printf("Standard: %s\n", r->standard_id);
}
if (r->description && r->description[0])
printf("\n%s\n", r->description);

const cfusa_fix_entry_t *fix = cfusa_fix_lookup(r->id);
if (fix) {
printf("\nFix: %s\n", fix->summary);
printf("Guidance:\n");
print_wrapped_guidance(fix->guidance);
} else {
printf("\nNo automated remediation guidance for this rule yet —\n"
"see the description above and the standard clause it maps to.\n");
}
return 0;
}
14 changes: 9 additions & 5 deletions cmd/cfusa/cmd_fix.c
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,9 @@
#include "cfusa/engine.h"
#include "cfusa/report.h"
#include "cfusa/utils.h"
#include "cfusa/fix.h"

typedef struct {
const char *rule_id;
const char *summary;
const char *guidance;
} fix_entry_t;
typedef cfusa_fix_entry_t fix_entry_t;

static const fix_entry_t FIXES[] = {
/* ---- LINT ---- */
Expand Down Expand Up @@ -241,6 +238,13 @@ static const fix_entry_t *lookup_fix(const char *rule_id)
return NULL;
}

/* Public accessor (cfusa/fix.h) — issue #212: lets `cfusa explain` show
* the same guidance without a second copy of FIXES[]. */
const cfusa_fix_entry_t *cfusa_fix_lookup(const char *rule_id)
{
return lookup_fix(rule_id);
}

/* ---- Real autofix: CFUSA-CY006 (free() without a following NULL-out) ----
*
* issue #210: the only rewrite in this file that is genuinely mechanical
Expand Down
1 change: 1 addition & 0 deletions include/cfusa/commands.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ int cmd_diff(int argc, char **argv);
int cmd_badge(int argc, char **argv);
int cmd_req(int argc, char **argv);
int cmd_fix(int argc, char **argv);
int cmd_explain(int argc, char **argv);
int cmd_hooks(int argc, char **argv);
int cmd_sign(int argc, char **argv);
int cmd_do178(int argc, char **argv);
Expand Down
20 changes: 20 additions & 0 deletions include/cfusa/fix.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#ifndef CFUSA_FIX_H
#define CFUSA_FIX_H

/* issue #212: cmd_fix.c's remediation-guidance table is the same data
* `cfusa explain <RULE-ID>` wants to show alongside a rule's description
* and standard citation -- expose a read-only lookup instead of a second,
* inevitably-drifting copy of the same ~39 entries. */

typedef struct {
const char *rule_id;
const char *summary;
const char *guidance;
} cfusa_fix_entry_t;

/* Returns the FIXES[] entry for `rule_id`, or NULL if that rule has no
* remediation guidance (yet). Owned by cmd_fix.c; the returned pointer is
* valid for the process lifetime (points into a static const table). */
const cfusa_fix_entry_t *cfusa_fix_lookup(const char *rule_id);

#endif /* CFUSA_FIX_H */
Loading
Loading