Skip to content
Open
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
4 changes: 4 additions & 0 deletions sdk/typescript/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,10 @@ checkout so a fixed vulnerability can be checked again.
cause; `scans match --all` matches all completed scans of the current repository,
including other worktrees and clones. Saved matches appear in `scans show` and
are reused unless `--force` is passed. Scans without sealed artifacts are skipped.
Automatic history matching loads at most 64 scan pairs per workbench page and
32 findings or 512 KiB per finding page. Each model comparison receives one
bounded page from each scan; cross-page matches are reconciled before the pair
is saved. Match results larger than 1 MiB are rejected with an actionable error.
Comment thread
mldangelo-oai marked this conversation as resolved.

`scans compare BEFORE_SCAN_ID AFTER_SCAN_ID` automatically matches findings by
root cause, reuses saved matches, and reports findings as new, persisting,
Expand Down
5 changes: 5 additions & 0 deletions sdk/typescript/_bundled_plugin/references/final-report.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ When there are no reportable findings, include a short `No findings` section tha
When there are reportable findings, render them as readable markdown findings rather than raw JSON or a dumped schema object.
Order findings from highest severity to lowest severity: `critical`, then `high`, then `medium`, then `low`.

`informational` is outside the reportable severity set, so those findings are not
detailed in the report. They are still sealed into `findings.json` and included in
the SARIF and CSV exports, so the report records how many were held back rather
than omitting them silently.

Use a separate finding entry for each independently attackable source/control/sink instance. Do not combine sibling routes, templates, query builders, parser operations, auth/object-access endpoints, or shared-helper callers into one representative finding solely for readability; if grouping helps, add a short grouped summary after the individual finding entries.

If validation or attack-path analysis provides a broad family row with multiple independently triggerable sink, parser, helper, API-mode, or protected-action lines, split it into child final findings before writing the report. Multiple affected lines inside one finding are appropriate for one inseparable proof tuple, such as a wrapper plus its shared sink, but not as a substitute for separate findings when sibling operations can be triggered independently.
Expand Down
26 changes: 26 additions & 0 deletions sdk/typescript/_bundled_plugin/scripts/report_projection.py
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,23 @@ def _code_evidence_lines(evidence: list[dict[str, Any]]) -> list[str]:
return lines


def _unreported_findings_note(findings: list[dict[str, Any]]) -> str:
"""Describe findings held back from this report by the reportable-severity gate.

They remain sealed in findings.json and exported to SARIF and CSV, so without
this line the three artifacts of one scan contradict each other and the one a
human reads is the one missing data.
"""
count = len(findings)
subject = "finding is" if count == 1 else "findings are"
pronoun = "It remains" if count == 1 else "They remain"
return (
f"{count} {subject} outside the reportable severity set "
f"({_severity_mix(findings)}) and not detailed here. "
f"{pronoun} recorded in `findings.json` and in the SARIF and CSV exports."
)


def _severity_mix(findings: list[dict[str, Any]]) -> str:
counts = Counter(finding["severity"]["level"] for finding in findings)
return (
Expand Down Expand Up @@ -588,6 +605,11 @@ def build_report_markdown(
),
key=_finding_sort_key,
)
unreported = [
finding
for finding in findings_document["findings"]
if finding["severity"]["level"] not in REPORTABLE_SEVERITIES
]
writeup_paths = [_writeup_report_path(finding) for finding in findings]
duplicate_writeup_paths = sorted(
path
Expand Down Expand Up @@ -719,6 +741,8 @@ def build_report_markdown(
f"| {finding_link} | {finding['severity']['level']} "
f"| {finding['confidence']['level']} | {writeup_link} |"
)
if unreported:
lines.extend(["", _unreported_findings_note(unreported)])
lines.extend(
[
"",
Expand Down Expand Up @@ -746,6 +770,8 @@ def build_report_markdown(
"No reportable findings survived the canonical discovery, validation, and reportability gates.",
]
)
if unreported:
lines.extend(["", _unreported_findings_note(unreported)])
if hardening_portfolio_path is not None:
lines.extend(
[
Expand Down
11 changes: 10 additions & 1 deletion sdk/typescript/_bundled_plugin/scripts/workbench_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,12 @@ def parse_args(description: str) -> argparse.Namespace:
list_unmatched_scan_pairs = subparsers.add_parser("list-unmatched-scan-pairs")
list_unmatched_scan_pairs.add_argument("--repository", required=True)
list_unmatched_scan_pairs.add_argument("--force", action="store_true")
list_unmatched_scan_pairs.add_argument("--offset", type=non_negative_int, default=0)
list_unmatched_scan_pairs.add_argument("--completed-before")

get_scan_matching_inputs = subparsers.add_parser("get-scan-matching-inputs")
get_scan_matching_inputs.add_argument("--scan-id", required=True)
get_scan_matching_inputs.add_argument("--offset", type=non_negative_int, default=0)

register_cli_scan = subparsers.add_parser("register-cli-scan")
register_cli_scan.add_argument("--scan-dir", required=True)
Expand All @@ -168,12 +174,15 @@ def parse_args(description: str) -> argparse.Namespace:
compare_scans.add_argument("--before-scan-id", required=True)
compare_scans.add_argument("--after-scan-id", required=True)
compare_scans.add_argument("--include-matching-inputs", action="store_true")
compare_scans.add_argument("--include-matching-status", action="store_true")
compare_scans.add_argument("--require-matches", action="store_true")

save_scan_comparison = subparsers.add_parser("save-scan-comparison")
save_scan_comparison.add_argument("--before-scan-id", required=True)
save_scan_comparison.add_argument("--after-scan-id", required=True)
save_scan_comparison.add_argument("--matches-json", required=True)
matches = save_scan_comparison.add_mutually_exclusive_group(required=True)
matches.add_argument("--matches-json")
matches.add_argument("--matches-file")

list_global_findings = subparsers.add_parser("list-global-findings")
list_global_findings.add_argument("--query")
Expand Down
3 changes: 3 additions & 0 deletions sdk/typescript/_bundled_plugin/scripts/workbench_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@
PATCH_ARTIFACT_MAX_BYTES = 2 * 1024 * 1024
FINDINGS_RESULT_LIMIT = 20
FINDINGS_PAGE_MAX = 20
MATCHING_PAIR_PAGE_MAX = 64
MATCHING_FINDING_PAGE_MAX = 32
MATCHING_INPUT_PAGE_BYTES = 512 * 1024
FINDING_DETAILS_PREVIEW_BYTES = 16_000
FINDING_ROOT_CAUSE_PREVIEW_BYTES = 2_000
FINDING_VALIDATION_PREVIEW_BYTES = 3_000
Expand Down
10 changes: 9 additions & 1 deletion sdk/typescript/_bundled_plugin/scripts/workbench_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -3568,9 +3568,16 @@ def main() -> None:
result = scan_history.list_unmatched_scan_pairs(
connection,
args,
backfill_finding_details=backfill_legacy_finding_details,
read_coverage=coverage_for_comparison,
)
elif args.command == "get-scan-matching-inputs":
result = scan_history.get_scan_matching_inputs(
connection,
args,
require_scan=require_scan,
read_coverage=coverage_for_comparison,
backfill_finding_details=backfill_legacy_finding_details,
)
elif args.command == "register-cli-scan":
result = register_cli_scan(connection, args)
elif args.command == "get-scan-recipe":
Expand All @@ -3583,6 +3590,7 @@ def main() -> None:
read_coverage=coverage_for_comparison,
backfill_finding_details=backfill_legacy_finding_details,
include_matching_inputs=args.include_matching_inputs,
include_matching_status=args.include_matching_status,
require_matches=args.require_matches,
)
elif args.command == "save-scan-comparison":
Expand Down
Loading
Loading