Skip to content

Commit 9bd7bd5

Browse files
committed
feat(cli): add fail-on-findings option
Signed-off-by: Deepak Jain <deepujain@gmail.com>
1 parent 21d0452 commit 9bd7bd5

2 files changed

Lines changed: 64 additions & 0 deletions

File tree

‎src/skillspector/cli.py‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,13 @@ def scan(
494494
help="Exit 1 when relevant analysis is partial or incomplete.",
495495
),
496496
] = False,
497+
fail_on_findings: Annotated[
498+
bool,
499+
typer.Option(
500+
"--fail-on-findings",
501+
help="Exit 1 when the scan reports one or more active findings.",
502+
),
503+
] = False,
497504
verbose: Annotated[
498505
bool,
499506
typer.Option(
@@ -628,6 +635,7 @@ def scan(
628635
yara_dir=yara_dir,
629636
verbose=verbose,
630637
fail_on_incomplete=fail_on_incomplete,
638+
fail_on_findings=fail_on_findings,
631639
)
632640
return
633641
if detection.complete and not detection.has_root_skill and len(detection.skills) == 0:
@@ -702,6 +710,8 @@ def scan(
702710
)
703711
if fail_on_incomplete and not is_complete:
704712
raise typer.Exit(code=1)
713+
if fail_on_findings and effective_findings(result):
714+
raise typer.Exit(code=1)
705715
if (result.get("risk_score") or 0) > RISK_THRESHOLD:
706716
raise typer.Exit(code=1)
707717
except typer.Exit:
@@ -2077,6 +2087,7 @@ def _scan_multi_skill(
20772087
yara_dir: str | None = None,
20782088
verbose: bool = False,
20792089
fail_on_incomplete: bool = False,
2090+
fail_on_findings: bool = False,
20802091
**legacy_kwargs: object,
20812092
) -> None:
20822093
"""Scan each detected sub-skill independently and produce a combined report."""
@@ -2102,6 +2113,7 @@ def _scan_multi_skill(
21022113
transitive_finding_count = 0
21032114
transitive_sources: set[str] = set()
21042115
analysis_incomplete = not detection.complete
2116+
has_findings = False
21052117
aggregate_limitations = [
21062118
f"recursive discovery {limitation.resource} limit reached"
21072119
for limitation in detection.limitations[:256]
@@ -2157,6 +2169,7 @@ def _scan_multi_skill(
21572169
result_body = _result_body(result)
21582170
result_characters = len(result_body)
21592171
result_records = _multi_skill_public_record_count(result)
2172+
has_findings = has_findings or bool(effective_findings(result))
21602173
if (
21612174
retained_public_records + result_records > _MULTI_SKILL_MAX_PUBLIC_RECORDS
21622175
or retained_report_characters + result_characters
@@ -2406,6 +2419,8 @@ def _scan_multi_skill(
24062419
raise typer.Exit(code=2)
24072420
if fail_on_incomplete and analysis_incomplete:
24082421
raise typer.Exit(code=1)
2422+
if fail_on_findings and has_findings:
2423+
raise typer.Exit(code=1)
24092424
if max_score > RISK_THRESHOLD:
24102425
raise typer.Exit(code=1)
24112426

‎tests/unit/test_cli.py‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,33 @@ def test_cli_fail_on_incomplete_exits_one_after_writing_report(
220220
assert output.exists()
221221

222222

223+
def test_cli_fail_on_findings_exits_one_below_risk_threshold(
224+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
225+
) -> None:
226+
"""Active findings can gate automation even when aggregate risk remains low."""
227+
(tmp_path / "SKILL.md").write_text("# Safe", encoding="utf-8")
228+
output = tmp_path / "report.json"
229+
monkeypatch.setattr(
230+
"skillspector.cli.graph.invoke",
231+
lambda state, config: {
232+
"report_body": '{"issues": []}',
233+
"execution_successful": True,
234+
"analysis_completeness": {"is_complete": True},
235+
"risk_score": 0,
236+
"findings": [_finding("T1", "active finding")],
237+
"filtered_findings": [_finding("T1", "active finding")],
238+
},
239+
)
240+
241+
result = runner.invoke(
242+
app,
243+
["scan", str(tmp_path), "-f", "json", "-o", str(output), "--fail-on-findings"],
244+
)
245+
246+
assert result.exit_code == 1
247+
assert output.exists()
248+
249+
223250
def test_recursive_scan_exits_two_after_writing_all_child_reports(tmp_path: Path) -> None:
224251
"""Recursive mode aggregates child execution failures after producing output."""
225252
s1 = SkillDirectory(path=tmp_path / "one", name="one", relative_path="one")
@@ -1066,6 +1093,28 @@ def _bounded_recursive_result(label: str, *, finding_count: int = 1) -> dict[str
10661093
}
10671094

10681095

1096+
def test_recursive_fail_on_findings_exits_one_below_risk_threshold(
1097+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
1098+
) -> None:
1099+
"""Recursive scans gate on active child findings, not only aggregate score."""
1100+
skill = SkillDirectory(tmp_path / "one", "one", "one")
1101+
detection = MultiSkillDetectionResult(is_multi_skill=True, skills=[skill])
1102+
monkeypatch.setattr(
1103+
cli.graph, "invoke", lambda *_args, **_kwargs: _bounded_recursive_result("one")
1104+
)
1105+
1106+
with pytest.raises(typer.Exit) as exit_info:
1107+
_scan_multi_skill(
1108+
detection,
1109+
FormatChoice.json,
1110+
None,
1111+
no_llm=True,
1112+
fail_on_findings=True,
1113+
)
1114+
1115+
assert exit_info.value.exit_code == 1
1116+
1117+
10691118
def test_recursive_json_uses_one_global_public_record_budget(
10701119
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
10711120
) -> None:

0 commit comments

Comments
 (0)