Skip to content

feat(cli): Implement report baseline comparison and suppression features - #324

Draft
misonijnik wants to merge 14 commits into
mainfrom
misonijnik/supressions
Draft

feat(cli): Implement report baseline comparison and suppression features#324
misonijnik wants to merge 14 commits into
mainfrom
misonijnik/supressions

Conversation

@misonijnik

Copy link
Copy Markdown
Member

No description provided.

@misonijnik
misonijnik force-pushed the misonijnik/supressions branch from 82d35fc to 5cbbc0c Compare July 30, 2026 11:17
@misonijnik
misonijnik marked this pull request as draft July 30, 2026 11:18
@misonijnik
misonijnik force-pushed the misonijnik/supressions branch from 5cbbc0c to e8b01aa Compare July 30, 2026 21:25
Adds the report-level engine for baselines and suppressions:

- identity.go: fingerprint-key resolution (source/sink hash by default),
  identity lookup and git-style prefix resolution
- baseline.go: new/unchanged/updated/absent classification, baselineState
  and baselineGuid application, run GUID stamping
- suppress.go: the read rule for suppression status, accept/defer/unsuppress,
  verbatim inheritance from a baseline, and summary counters
- save.go: atomic report writing

PropertyBag now preserves unknown keys as raw JSON. It previously modelled
only "tags", so any read-modify-write of a report silently dropped the rest
of every property bag — which the round-trip test caught.
internal/triage applies one pass of baseline inheritance, accept/defer
decisions and baseline comparison over a report, and is the single
implementation behind the triage command, scan's annotation step and
summary's read-only view. Gate decides whether findings fail the build:
suppressed findings never count, and with a baseline only new (or
uncomparable) ones do.

The summary grows Baseline and Suppressions subsections, rendered only
when they apply, and the finding listing hides suppressed results unless
asked for them.
- opentaint triage: compare a report against a baseline, record accept
  ("won't fix") and defer ("not fixing for now") decisions by fingerprint
  prefix, and write the annotated report
- scan: --baseline, --baseline-state, --error-on-findings,
  --error-on-severity, --fingerprint-key; the report the analyzer wrote is
  rewritten only when triage actually changed it
- summary: --baseline, --baseline-state, --suppressed; runs the same engine
  read-only so the file is never touched

Read-only mode still annotates the in-memory report, otherwise
--baseline-state could not filter on a state nobody had written.
Rule lists are scan-time rule selection, not suppression: an excluded
rule
never loads, so it produces nothing to suppress. The analyzer only
supports
inclusion, so exclusion is implemented by enumerating every rule id in
the
ruleset and subtracting, then re-expanding join refs so that excluding a
library rule cannot silently break a rule that joins against it.

Resolution happens before the --dry-run bail-out, so a list that selects
nothing is reported by --dry-run rather than after a full compile. The
--rule-id flag continues to win over the config file.
Adds the opentaint triage reference, the baseline/gating flags on scan
and
summary, a Baselines and suppressions section explaining that presence
in a
baseline is not acceptance, and the rules.only/rules.exclude
configuration
keys. Suggestion builders learn the new flags so rebuilt commands keep
them.
- rename TestGateCountsUpdatedFindingsAsNew: the body asserts the
opposite
  (updated findings do not trip the gate)
- Gate.inScope reuses sarif.MatchesSeverity (exported) instead of
  re-implementing the case-insensitive level match
- fingerprintValue delegates to Identity instead of duplicating the
lookup
- scan loads the baseline once, before compile, and passes it through to
  the triage step instead of reading the file a second time
Rule exclusion was config-only (rules.exclude); this adds the flag
analogue
on scan. It overrides the configured exclude list, and composes with
--rule-id — both were asked for explicitly, so exclusion subtracts from
the
selection instead of one flag silently winning. Emptying the selection
is
an error either way, caught by --dry-run before any compile.
…matcher

matchesPattern in rules/select.go had invented a fourth match form,
globbing
the bare leaf name, which summary's --rule-id filter does not support.
Export
the string-level matcher as sarif.MatchesRuleID and delegate rule
selection
(rules.only/rules.exclude, --exclude-rule-id) to it, so one grammar
holds
everywhere: exact full path.yaml:id, exact bare name, or a doublestar
glob
over the full id only. Docs examples updated off the removed leaf-glob
form.
Verifying that --exclude-rule-id reaches the analyzer as concrete
--semgrep-rule-id args surfaced a silence: excluding a typo'd rule id
matched nothing and looked effective. Selection.Unmatched reports
patterns
that select no rule, and scan warns for each — on both the config-list
path
and the --rule-id/--exclude-rule-id flag path. The configuration example
also used a rule id that does not exist in the shipped ruleset; replaced
with a real one.
…-exclude

An exclusion-only selection lowers to just the excluded concrete ids, passed
via --semgrep-rule-id-exclude, instead of expanding into the ~150-rule
inclusion complement. rules.Select now returns Resolved{Include, Exclude};
the allow-list path keeps CLI-side subtraction and join-ref re-expansion,
since inclusion must be exact ids anyway. --rule-id composed with
--exclude-rule-id still subtracts CLI-side (tiny explicit lists).

Requires the analyzer's --semgrep-rule-id-exclude option (separate core
branch); older jars reject it, so exclusion fails fast until the analyzer
version is bumped.
… filter

The single --baseline-state name carried two grammars: a boolean on
scan/triage that persisted result.baselineState into the file, and a
value
list on summary that filtered the listing. Rename the scan/triage switch
to
--write-baseline-state (matching its WriteBaselineState field and its
intent),
leaving --baseline-state <state> exclusively as the summary filter. The
filter
help now spells out the accepted values (new | unchanged | updated |
absent).
No behavior change beyond the flag name; --error-on-severity is
unchanged.
The flag validated each repeatable value as a single token, so the
natural
--error-on-severity error,warning was rejected with "invalid severity".
Split
each value on commas so the comma form and the repeated-flag form are
equivalent. Purely CLI-side (the gate never reaches the analyzer); the
docs
already used the comma form, which now works.
A dedicated task-oriented guide for the new capabilities: the two-axis
mental
model (baseline state vs. suppression), the full lifecycle from
establishing a
baseline through triage inheritance to gating, and reference tables for
baseline
states, suppression statuses, fingerprint identity, the failure gate +
exit
codes, and rule selection. Includes copy-paste GitHub Actions
(actions/cache to
carry the baseline) and GitLab recipes, plus the SARIF 2.1.0 conformance
notes.

Linked from the docs index (Guides + CI/CD) and the usage guide, whose
long
inline section is trimmed to a concise pointer keeping the flag tables.
@misonijnik
misonijnik force-pushed the misonijnik/supressions branch from e8b01aa to 4e8e975 Compare August 1, 2026 21:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant