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
1 change: 1 addition & 0 deletions .fusa-reqs.json
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@
{"id":"REQ-DISP-FP001","title":"cfusa disposition add accepts --fingerprint <sha256:...> (in addition to --rule, which remains for display/filtering only); the fingerprint is the key cfusa_report_apply_dispositions() matches on","standard":"ISO 26262","level":"ASIL-B"},
{"id":"REQ-BASELINE001","title":"cfusa baseline snapshots every current, undispositioned finding's fingerprint into .fusa-baseline.json; cfusa check/lint load and apply it via cfusa_report_apply_dispositions(), excluding baselined findings from the exit-code gate the same way an accepted disposition is, tagged with the distinct 'baseline' action","standard":"c-FuSa core","level":"ASIL-B"},
{"id":"REQ-BASELINE002","title":"cfusa_report_apply_dispositions() is idempotent across multiple calls against the same report with different lists -- a finding already carrying a disposition_id from an earlier call is skipped, so applying dispositions then baseline never double-decrements error_count/warning_count/dispositioned_count","standard":"c-FuSa core","level":"ASIL-B"},
{"id":"REQ-GITDIFF001","title":"cfusa check --changed-since <ref> filters the report to only findings on lines changed since <ref> (via git diff --unified=0), validates the ref against argument-injection the same way cmd_impact.c's validate_git_ref() does, and fails loudly (exit 2) rather than silently reporting against an unfiltered diff when the ref is invalid or the directory isn't a git repository","standard":"c-FuSa core","level":"ASIL-B"},
{"id":"REQ-MET-SUBCMD001","title":"cfusa metrics returns exit 2 when no subcommand given; parity with go-FuSa TestRunMetrics_NoSubcmd","standard":"go-FuSa parity","level":"ASIL-A"},
{"id":"REQ-MET-SUBCMD002","title":"cfusa metrics returns exit 2 for unknown subcommand; parity with go-FuSa TestRunMetrics_UnknownSubcmd","standard":"go-FuSa parity","level":"ASIL-A"},
{"id":"REQ-MET-REC001","title":"cfusa metrics record output contains 'Metrics recorded'; parity with go-FuSa TestRunMetricsRecord_EmptyDir","standard":"go-FuSa parity","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 @@ -42,6 +42,7 @@ add_library(cfusa_core STATIC
src/severity.c
src/disposition.c
src/lex.c
src/gitdiff.c
)
target_include_directories(cfusa_core PUBLIC include)
set_target_properties(cfusa_core PROPERTIES
Expand Down Expand Up @@ -135,6 +136,7 @@ cfusa_test(test_analyze_rules)
cfusa_test(test_hara)
cfusa_test(test_match_outside_string)
cfusa_test(test_lex)
cfusa_test(test_gitdiff)
cfusa_test(test_asil_table)
cfusa_test(test_report_formats)
cfusa_test(test_impact)
Expand Down
67 changes: 49 additions & 18 deletions cmd/cfusa/cmd_check.c
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,26 @@
#include "cfusa/config.h"
#include "cfusa/utils.h"
#include "cfusa/disposition.h"
#include "cfusa/gitdiff.h"

//cfusa:req REQ-CLI003 REQ-NOSUMMARY001
int cmd_check(int argc, char **argv)
{
const char *dir = ".";
const char *fmt_s = "text";
const char *output = NULL;
const char *dir = ".";
const char *fmt_s = "text";
const char *output = NULL;
const char *changed_since = NULL;
int strict = 0, no_summary = 0;

static const struct option long_opts[] = {
{"dir", required_argument, NULL, 'd'},
{"format", required_argument, NULL, 'f'},
{"output", required_argument, NULL, 'o'},
{"strict", no_argument, NULL, 's'},
{"no-color", no_argument, NULL, 'C'},
{"no-summary", no_argument, NULL, 'S'},
{"help", no_argument, NULL, 'h'},
{"dir", required_argument, NULL, 'd'},
{"format", required_argument, NULL, 'f'},
{"output", required_argument, NULL, 'o'},
{"strict", no_argument, NULL, 's'},
{"no-color", no_argument, NULL, 'C'},
{"no-summary", no_argument, NULL, 'S'},
{"changed-since", required_argument, NULL, 'g'},
{"help", no_argument, NULL, 'h'},
{NULL,0,NULL,0}
};

Expand All @@ -37,20 +40,26 @@ int cmd_check(int argc, char **argv)
#elif defined(__linux__)
optind = 0; /* glibc: reset nextchar so stale argv pointer is not followed */
#endif
while ((c = getopt_long(argc, argv, "d:f:o:sCSh", long_opts, NULL)) != -1) {
while ((c = getopt_long(argc, argv, "d:f:o:sCSg:h", long_opts, NULL)) != -1) {
switch (c) {
case 'd': dir = optarg; break;
case 'f': fmt_s = optarg; break;
case 'o': output = optarg; break;
case 's': strict = 1; break;
case 'd': dir = optarg; break;
case 'f': fmt_s = optarg; break;
case 'o': output = optarg; break;
case 's': strict = 1; break;
case 'C': break; /* no-color: text output has no ANSI codes; accepted for spec compliance */
case 'S': no_summary = 1; break;
case 'S': no_summary = 1; break;
case 'g': changed_since = optarg; break;
case 'h':
printf("Usage: cfusa check [--dir <path>] [--format text|json|sarif|html|md]\n"
" [--output <file>] [--strict] [--no-color] [--no-summary]\n\n"
" [--output <file>] [--strict] [--no-color] [--no-summary]\n"
" [--changed-since <git-ref>]\n\n"
"Runs all lint + analyze + cyber rules.\n"
"Exits 1 on any ERROR; with --strict, exits 1 on any WARNING too.\n"
"--no-summary suppresses the per-category and top-rules summary block.\n");
"--no-summary suppresses the per-category and top-rules summary block.\n"
"--changed-since <ref>: only keep findings on lines changed since <ref>\n"
" (via `git diff --unified=0 <ref>`). Independent of, and composes with,\n"
" .fusa-baseline.json (`cfusa baseline`) -- use either or both to adopt\n"
" c-FuSa on an existing codebase without gating on its full backlog.\n");
return 0;
default: return 2;
}
Expand Down Expand Up @@ -92,6 +101,28 @@ int cmd_check(int argc, char **argv)
rpt.no_summary = no_summary;
cfusa_engine_run_all(dir, &cfg, &rpt);

//cfusa:req REQ-GITDIFF001
/* issue #209: --changed-since scopes the report down to only
* findings on lines actually touched since <ref> -- applied FIRST,
* before dispositions/baseline, so those lookups never need to
* consider a finding this already dropped. Independent of, and
* composable with, dispositions/baseline: a project can use either,
* both, or neither. */
if (changed_since) {
cfusa_changed_lines_t changed;
if (!cfusa_git_changed_lines_load(dir, changed_since, &changed)) {
fprintf(stderr,
"cfusa check: ERROR: --changed-since '%s' failed — not a "
"git repository, unknown ref, or git not found; refusing "
"to silently report against an unfiltered/unknown diff\n",
changed_since);
cfusa_report_free(&rpt);
return 2;
}
cfusa_report_filter_to_changed_lines(&rpt, &changed);
cfusa_git_changed_lines_free(&changed);
}

//cfusa:req REQ-DISP-ENFORCE003
/* issue #122: cross-reference findings against .fusa-dispositions.json
* by fingerprint — an accept/mitigate-action disposition suppresses
Expand Down
75 changes: 75 additions & 0 deletions include/cfusa/gitdiff.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
#ifndef CFUSA_GITDIFF_H
#define CFUSA_GITDIFF_H

#include "cfusa/report.h"

/*
* issue #209: `cfusa check --changed-since <ref>` scopes analysis to
* lines actually touched since `ref` -- so adopting c-FuSa on an
* existing codebase can gate CI on only the NEW findings a change
* introduces, without needing #208's baseline mechanism (the two are
* independent; a project can use either, or both).
*/

typedef struct {
char file[CFUSA_FINDING_FILE_MAX]; /* same project-relative convention
as cfusa_finding_t.file */
int start;
int end; /* inclusive */
} cfusa_changed_range_t;

typedef struct {
cfusa_changed_range_t *items;
int count;
int cap;
} cfusa_changed_lines_t;

/* Runs `git -C dir diff --unified=0 <ref> -- .` with no shell (pipe() +
* fork() + execvp("git", ...), the same safe subprocess pattern
* cmd_impact.c already established for git invocation — argv entries
* are passed separately, never concatenated into a shell command
* string, so this can never be interpreted as shell syntax, CWE-78) and
* parses the unified-diff hunk headers into a list of per-file NEW-side
* changed line ranges.
*
* `ref` is validated (rejects a leading '-', which git would otherwise
* parse as an option — the same argument-injection class cmd_impact.c's
* validate_git_ref() already guards against — and any character outside
* a conservative git-ref-name allowlist) before it ever reaches git.
*
* git diff's own paths are always relative to the repository root, not
* to `dir` — `git -C dir rev-parse --show-prefix` supplies the
* repo-root-to-`dir` prefix, which is stripped off every path so a
* `--dir` pointing at a subdirectory of a larger repo still lines up
* with cfusa_finding_t.file's `dir`-relative convention.
*
* Returns 1 on success (*out populated, possibly with 0 ranges — an
* empty diff is not an error), 0 on failure (bad ref, not a git repo,
* git not found, ...) — *out is always safe to pass to
* cfusa_git_changed_lines_free() either way. */
int cfusa_git_changed_lines_load(const char *dir, const char *ref,
cfusa_changed_lines_t *out);

/* Releases the array cfusa_git_changed_lines_load() allocated. Safe on a
* zeroed/already-freed list. */
void cfusa_git_changed_lines_free(cfusa_changed_lines_t *list);

/* Removes every finding in `rpt` whose (file, line) does not fall
* within any range in `changed`, then recomputes error_count/
* warning_count/info_count/dispositioned_count from what remains.
* Unlike disposition/baseline suppression (cfusa_report_apply_
* dispositions(), which keeps a finding visible but excludes it from
* the gate), this genuinely filters the report down to only what's
* relevant to the diff — composes with, but is independent of, that
* mechanism; apply this first if using both, so a disposition/baseline
* lookup never has to look at a finding this already dropped.
*
* A finding with no line information (line <= 0) is always kept — line
* 0 is used project-wide for directory/file-level findings that don't
* correspond to any specific changed line. Safe to call with an empty
* `changed` list (removes every findable-line finding — an empty diff
* means nothing changed). */
void cfusa_report_filter_to_changed_lines(cfusa_report_t *rpt,
const cfusa_changed_lines_t *changed);

#endif /* CFUSA_GITDIFF_H */
Loading
Loading