-
Notifications
You must be signed in to change notification settings - Fork 1.1k
fix(cli): baseline suppression in recursive multi-skill scans (#201) #205
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -277,7 +277,16 @@ def scan( | |
| if recursive and resolved_path.is_dir(): | ||
| detection = detect_skills(resolved_path) | ||
| if detection.is_multi_skill: | ||
| _scan_multi_skill(detection, format, output, no_llm, yara_rules_dir, verbose) | ||
| _scan_multi_skill( | ||
| detection, | ||
| format, | ||
| output, | ||
| no_llm, | ||
| yara_rules_dir, | ||
| verbose, | ||
| baseline=baseline, | ||
| show_suppressed=show_suppressed, | ||
| ) | ||
| return | ||
| if not detection.has_root_skill and len(detection.skills) == 0: | ||
| console.print( | ||
|
|
@@ -352,13 +361,39 @@ def _build_trace_config(input_path: str, format: FormatChoice, no_llm: bool) -> | |
| } | ||
|
|
||
|
|
||
| def _result_finding_count(result: dict[str, object]) -> int: | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good fix for the falsy empty-list fallback. Note the same |
||
| """Count active (post-suppression) findings for the multi-skill summary. | ||
|
|
||
| The report node returns ``filtered_findings`` as the full LLM-filtered set | ||
| (kept plus baseline-suppressed) and the suppressed subset separately under | ||
| ``suppressed_findings`` (see ``nodes/report.py``). ``partition_findings`` | ||
| guarantees ``kept + suppressed == filtered_findings``, so the active count is | ||
| ``len(filtered_findings) - len(suppressed_findings)`` and is never negative for | ||
| real report output; the ``max(..., 0)`` is only a display-safety floor against | ||
| a malformed result. Subtracting is done solely on this branch: the raw | ||
| ``findings`` fallback (reached only when ``filtered_findings`` is absent or not | ||
| a list, e.g. a malformed or error result, never real report output) must not | ||
| subtract, since raw findings are not the population that produced | ||
| ``suppressed_findings``. | ||
| """ | ||
| filtered = result.get("filtered_findings") | ||
| if isinstance(filtered, list): | ||
| suppressed = result.get("suppressed_findings") | ||
| suppressed_count = len(suppressed) if isinstance(suppressed, list) else 0 | ||
| return max(len(filtered) - suppressed_count, 0) | ||
| findings = result.get("findings") | ||
| return len(findings) if isinstance(findings, list) else 0 | ||
|
|
||
|
|
||
| def _scan_multi_skill( | ||
| detection: MultiSkillDetectionResult, | ||
| format: FormatChoice, | ||
| output: Path | None, | ||
| no_llm: bool, | ||
| yara_rules_dir: Path | None, | ||
| verbose: bool, | ||
| baseline: Path | None = None, | ||
| show_suppressed: bool = False, | ||
| ) -> None: | ||
| """Scan each detected sub-skill independently and produce a combined report.""" | ||
| skills = detection.skills | ||
|
|
@@ -372,7 +407,14 @@ def _scan_multi_skill( | |
| f" [{i}/{len(skills)}] Scanning [bold]{skill.name}[/bold] ({skill.relative_path}/)" | ||
| ) | ||
| yara_dir = str(yara_rules_dir.resolve()) if yara_rules_dir else None | ||
| state = _scan_state(str(skill.path), format, no_llm, yara_rules_dir=yara_dir) | ||
| state = _scan_state( | ||
| str(skill.path), | ||
| format, | ||
| no_llm, | ||
| yara_rules_dir=yara_dir, | ||
| baseline=baseline, | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| show_suppressed=show_suppressed, | ||
| ) | ||
| trace_config = _build_trace_config(str(skill.path), format, no_llm) | ||
|
|
||
| try: | ||
|
|
@@ -397,8 +439,7 @@ def _scan_multi_skill( | |
| continue | ||
| score = result.get("risk_score", 0) | ||
| severity = result.get("risk_severity", "LOW") | ||
| filtered = result.get("filtered_findings") or result.get("findings") | ||
| finding_count = len(filtered) if isinstance(filtered, list) else 0 | ||
| finding_count = _result_finding_count(result) | ||
| console.print(f" {skill.name:<30} {score:<8} {severity:<12} {finding_count:<10}") | ||
|
|
||
| console.print("") | ||
|
|
@@ -420,9 +461,7 @@ def _scan_multi_skill( | |
| "path": skill.relative_path, | ||
| "risk_score": result.get("risk_score", 0), | ||
| "risk_severity": result.get("risk_severity", "LOW"), | ||
| "finding_count": len( | ||
| result.get("filtered_findings") or result.get("findings") or [] | ||
| ), | ||
| "finding_count": _result_finding_count(result), | ||
| } | ||
| ) | ||
| Path(output).write_text(json.dumps(combined, indent=2), encoding="utf-8") | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Blocker: this dispatch happens before the
try/exceptinscan(), andload_baseline()inside_scan_state()can raiseFileNotFoundError/ValueError. Verified:scan <dir> -r --baseline /nonexistent.yamlemits a raw traceback and exits 1 (the "risk found" code), while the single-skill path prints a clean error and exits 2. Please load the baseline once here inside the existing try (or wrap this call in equivalent handling) so recursive mode keeps the documented exit-code contract.