Python API Reference
automated_security_helper.interactions.run_ash_scan
ScanOptions
Bases: BaseModel
All parameters for a single run_ash_scan invocation.
Source code in automated_security_helper/interactions/run_ash_scan.py
105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 | |
ScanIncompleteness
dataclass
Every reason a finished scan exits 1 for coverage rather than for a crash.
One field per arm of the exit-1 half of _compute_exit_code, which reads its
verdict from this object rather than from its own calls to the primitives. That
is what makes it safe for anything else to act on: the MCP runner reports a scan
incomplete instead of failed from this object, and if the two read the
question separately, the status a client sees and the exit code the CLI returns
could disagree on the same results.
Truthy when any field is set. Empty for results is None, which also exits
1 but means no results exist. That is a crash, and it does not belong in here.
Source code in automated_security_helper/interactions/run_ash_scan.py
962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 | |
to_payload()
The structured form an MCP client reads. JSON-serializable.
Source code in automated_security_helper/interactions/run_ash_scan.py
ScanIncompleteExit
Bases: SystemExit
sys.exit(1) for a scan that finished with partial coverage.
A SystemExit with code 1, so the CLI's exit code and every caller that
catches SystemExit see what they saw before. It adds the structured reason.
Exit 1 also means a crash, and an in-process caller that has only the code
cannot tell the two apart. Its other choices are parsing the log or re-reading
a results file that a crash after the SCAN phase leaves behind looking just
like a finished scan's.
Raised only where _compute_exit_code returned 1 with results in hand. That
function returns 1 with results only from the coverage arms, so the
exception's presence is the signal and incompleteness is what it was.
Source code in automated_security_helper/interactions/run_ash_scan.py
scanner_statuses(results)
(name, status) for every scanner in results, in scanner-name order.
What :func:no_scanner_ran reads, and read through
get_unified_scanner_metrics for the same reason
:func:incomplete_scanners does: that function is what every reporter and the
metrics table already use, so the set-level gate answers from the statuses the
operator was shown rather than from a second, independently-derived read of
results.scanner_results.
Deliberately just the pairs. An earlier form of this shared one pass with
incomplete_scanners by filtering these pairs, which stopped being possible
when that function grew a second arm reading the per-metric target counters --
a shortfall is not visible in a (name, status) pair. Keeping this narrow is
what makes the two gates independently correct; the cost is one extra pass over
the metrics, which _compute_exit_code already takes for the findings count.
Source code in automated_security_helper/interactions/run_ash_scan.py
no_scanner_ran(observed, expected=None)
True when this run reached no verdict about the target.
Two conditions, and the second exists because the first could not express it.
-
observed is non-empty and none of its scanners reached a verdict. SKIPPED has to be tolerated one entry at a time -- it is how sharding and
--exclude-scannersrecord work a run was never meant to do -- so a file in which every entry is SKIPPED clears the per-scanner pass while having measured nothing. -
observed is empty AND expected is not. An empty scanner set used to be exempt unconditionally, on the reasoning that it is reachable from a legitimate
--phases convertrun. That reasoning is sound but covers two different states, and a boolean overobservedalone cannot separate them: "the scan phase was not requested", which is benign, and "the scan phase ran and had nothing to run", which is the silent-zero case this gate exists for.
expected is what separates them, and it needs no new state to do it.
ScanPhase is what records metadata.expected_scanners, so a recorded
roster means the scan phase ran; no roster and no scanners means it never did.
Defaults to None so a results file written by a version that recorded no roster
-- which ash merge reads, from whatever ASH produced each shard -- keeps the
old benign reading rather than becoming a failure on upgrade.
Source code in automated_security_helper/interactions/run_ash_scan.py
incomplete_scanners(results)
(name, status) for every scanner that was selected and did not complete.
"Did not complete" covers two distinct failures, and it did not always cover the second:
- The scanner never produced a result -- status ERROR or MISSING.
- The scanner ran, reported a status, and could not evaluate some of the targets it was given.
Only (1) was selected on, because that is a status test and (2) does not move
a target's status. ScanResultsContainer.determine_status returns ERROR only
once targets_failed >= targets_attempted, so a scanner that lost some of
its input keeps whatever the severity gate gave it and the gate could not see
it. Measured on this repository under its own config: cdk-nag attempts 10
targets and fails 4, its per-target container still reports PASSED, and the
gate exited 0. Two of those four are real CloudFormation templates that went
unscanned.
Do not read "(2) does not change the status" as "nothing in this change
touches a status". A sibling commit ORs any_target_errored into the
error flag, so a target tree that lost ALL of its targets -- which
determine_status does report as ERROR -- now reaches the rolled-up status
where previously only the "source" report was consulted, and even that only
when the scanner was absent from scanner_results. That is a status change,
it is not opted in, and it is the thing the CHANGELOG's "Behavior changes"
entry describes. The two arms are separable: partial loss stays out of the
status and is expressed here; total loss on any tree is a status.
Case (2) is expressed HERE and not as a status, and not by widening
_INCOMPLETE_SCANNER_STATUSES. The reason is a caller rather than taste.
cli.merge._completed keys on status against that same set to answer a
different question -- whether a shard's scanner ran at all -- and
_verify_shard_contributions refuses a merge outright where a shard
completed none of the scanners it owned. A scanner that lost one target of ten
ran, so any status-shaped expression of partial coverage would propagate into
shard refusal and start rejecting healthy shards, failing the merge far from
the code that caused it. That is the concrete cost of adding a
ScannerStatus member for this, and the reason none was added.
Worth being precise about the residual risk, because two earlier versions of
this comment got it wrong in opposite directions. One said _completed
inspects a ScannerTargetStatusInfo, which declares no target counters, so
the protection is structural rather than conventional. That is false, and
measurably so: the model sets extra="allow", so counters written into
scanner_results land in model_extra and a getattr for them
succeeds. Nothing structural stops _completed reading coverage; what stops
it is that it does not, which is a behavior and therefore something a test can
hold. tests/unit/interactions/test_fail_on_partial_target_coverage.py
holds it, and mutating _completed to consult the counters reddens it.
The other claimed no reporter and no summary table sees anything new, and this
change is the reason both do. ScannerMetrics gained targets_attempted
and targets_failed; the console table, the markdown report and
ash.flat.json all carry them, and the first two grew an "Incomplete
coverage" section. Measured on this repository, ash.summary.md gained
### Incomplete coverage and ash.flat.json gained the two keys. What is
genuinely untouched is narrower and worth naming exactly: _completed, and
the DEFAULT exit code, which reaches this function only once
_resolve_fail_on_incomplete_scanners returns true.
tests/unit/cli/test_merge.py pins the boundary from the merge side.
Precedence between the two arms is on TOTALITY, not on status. Total loss
satisfies the coverage condition too -- failed >= attempted implies
failed > 0 -- and appending counts there would give an ERROR row a
parenthetical it never had while saying nothing the status does not already
say, so total loss reports the bare status.
A PARTIAL shortfall reports its counts whatever the status is, and that is a
correction rather than a preference. Selecting the bare-status arm on status
alone made this function unable to deliver what it exists for in the case it
was written for: an ERROR scanner that is only partly incomplete took the bare
arm and printed cdk-nag: ERROR, never cdk-nag: ERROR (4 of 10 targets
unevaluated), so the counts the operator needs to tell "the tool is absent"
from "the tool ran and skipped four templates" were dropped by the routing.
An ERROR with no counters available still falls to the bare form, because there
is no honest denominator to print.
Read through get_unified_scanner_metrics rather than off
results.scanner_results directly, so the gate and the report cannot
disagree: that function is what every reporter and the metrics table already
use, and it is where excluded-versus-missing precedence is decided. The target
counters are read from the same rows for the same reason -- ScannerMetrics
is what the summary table prints, so the gate fails on exactly the numbers the
operator was shown rather than on a second, independently-derived count.
An allowlist narrowing -- --scanners bandit -- does not trip this, because
the scanners it leaves out are recorded SKIPPED. That was not always true: the
scan phase used to validate a scanner's dependencies before checking whether it
had been selected, so on a host without cfn-nag, grype and syft a
--scanners bandit run reported those three MISSING while the six
tool-present scanners it left out reported SKIPPED. Which status an unselected
scanner got therefore depended on whether its tool happened to be installed.
See core/phases/scan_phase.py for the ordering that fixed it.
Filtering here against opts.scanners was the alternative and is rejected:
it would make the exit code disagree with the status the report prints for the
same scanner, and it has no counterpart in ash merge, which has no scanner
selection to consult. Fixing the recorded status instead makes both agree.
Tested against _COMPLETE_SCANNER_STATUSES and not against
_INCOMPLETE_SCANNER_STATUSES, though the two are complements over the enum.
metric.status is a plain string that may have come from a results file this
version did not write, and only the allowlist form treats a status outside the
enum entirely as incomplete rather than as a scanner that ran.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
results
|
Optional[AshAggregatedResults]
|
The aggregated results, or None when the scan produced none. |
required |
Returns:
| Type | Description |
|---|---|
List[tuple[str, str]]
|
Pairs in scanner-name order, empty when every selected scanner completed. |
List[tuple[str, str]]
|
The second element is the scanner's own status for a status-based |
List[tuple[str, str]]
|
incompleteness, and that status followed by the unevaluated-target counts |
List[tuple[str, str]]
|
for a coverage-based one. It is a display string, not a status token: |
List[tuple[str, str]]
|
both callers interpolate it into a message and neither parses it. |
Source code in automated_security_helper/interactions/run_ash_scan.py
417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 | |
incomplete_converters(results)
(name, reason) for every converter that was meant to run and did not.
Conversion produces the second set of targets the scanners are given: notebooks become Python, archives become their contents. A converter that crashed or whose tool was absent therefore costs scan coverage, and it did so with no effect on any verdict -- the scanners that ran reported PASSED on the targets they were handed, and nothing asked whether the targets that should have existed did.
Two conditions, read off the recorded row rather than re-derived:
failureis set -- the converter raised, or was dropped by the plugin filter for a reason its own dependency check did not explain. ConvertPhase records both.dependencies_satisfiedis False -- its external tool was not available.
excluded is checked first and wins over both. It is the converter-side
counterpart of a SKIPPED scanner: work the run was never meant to do, which is
what a config-disabled converter and one dropped by
--python-based-plugins-only are. A gate that failed on those would fail every
run that turns conversion off, which is a supported configuration.
Read with getattr because a results file written by an older version carries
rows without failure, and ash merge reads shard results from whatever
ASH produced each one.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
results
|
Optional[AshAggregatedResults]
|
The aggregated results, or None when the scan produced none. |
required |
Returns:
| Type | Description |
|---|---|
List[tuple[str, str]]
|
Pairs in the order the rows were recorded, empty when every converter either |
List[tuple[str, str]]
|
ran or was excluded. The second element is a display string, not a token; |
List[tuple[str, str]]
|
the caller interpolates it into a message and does not parse it. |
Source code in automated_security_helper/interactions/run_ash_scan.py
print_incompleteness_message(incomplete_scanner_rows, incomplete_converter_rows, stale_database_records=())
Explain an exit code of 1 in terms of what actually caused it.
WHY THIS IS A FUNCTION RATHER THAN THREE BRANCHES INLINE
It was inline, and the converter arm had no branch of its own, so it fell through to
"Exiting due to exception during ASH scan" -- which is false. Nothing raises on that
path: _compute_exit_code returns 1 from the converter gate after a
logger.error. Both Nix CI legs exited that way, and the wording sent a reader
through 1572 log lines hunting a traceback that was never written, with the real
cause recorded only in a log line seventy lines earlier.
Inline it also could not be tested. The enclosing function runs a whole scan, so no
unit test could assert on the wording, which is how a message this wrong survived.
Extracted, the arm selection is a pure function of two lists and
tests/unit/interactions/test_fail_on_incomplete_converters.py asserts it
directly.
THE SCANNER ARM WINS WHEN BOTH ARE NON-EMPTY, deliberately. A scanner that did not run is the more specific and more serious finding, and conversion shortfalls often follow from the same absent environment. The converter arm is still reachable on its own, which is the case Nix mode produces.
STALE CONTENT DATABASES ARE PRINTED FIRST AND ALONGSIDE, not as a third arm of
the precedence above. Their advice is different (refresh the database, or opt out
with --allow-stale-content-db), and that arm is not behind the completeness
gate, so a run can trip it together with either of the others. A scanner listed
only for its stale database never reaches incomplete_scanner_rows:
scan_incompleteness carries it in the stale records instead, so the
install-the-tool advice below is not given for a tool that ran.
Source code in automated_security_helper/interactions/run_ash_scan.py
670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 | |
unevaluated_rules(results)
Every rule a scanner reported, at run level, that it could not evaluate.
WHY THIS IS A SECOND FUNCTION RATHER THAN PART OF incomplete_scanners
incomplete_scanners answers "which scanners lost whole targets", and it
answers it from the target counters. A rule that raised mid-evaluation loses
neither a scanner nor a target: the scanner ran, the target was read, most of
the rules reached a verdict, and one did not. Counting it as a lost target
would overstate in a way that is measurable rather than theoretical --
ScanResultsContainer.determine_status returns ERROR once
targets_failed >= targets_attempted, so a rule that raises on every
template (which is the normal case for one that cannot resolve an intrinsic)
would report the scanner as having evaluated nothing, and the cdk-nag
scanner's own log line would read "No rules were evaluated" about a run that
evaluated all but one of them.
So the granularity is the rule, and the fact is read from where SARIF already puts it.
WHAT IT READS, AND WHY THAT IS THE RIGHT CHANNEL
invocation.toolExecutionNotifications is, in the schema's own words, "A
list of runtime conditions detected by the tool during the analysis", and
notification.associatedRule is "A reference used to locate the rule
descriptor associated with this notification". A rule raising instead of
returning a verdict is a runtime condition, and the rule it happened to is
what to associate it with. The cdk-nag scanner already writes exactly that.
Reading it here rather than inventing a counter is what makes this gate scanner-agnostic: any scanner -- or any externally-produced SARIF that ASH ingests -- which reports an error-level runtime condition is reporting that part of its analysis did not run, and that is the thing being gated on.
ONLY level == "error". warning is the field's default and a tool may
use it for conditions that cost no coverage, so gating on it would fail scans
for notes. The cdk-nag scanner sets error deliberately and says so.
.value RATHER THAN str() ON THE LEVEL. Level is a str-mixin enum,
so Enum.__str__ still wins and str(Level.error) renders
"Level.error", which matches nothing. Comparing the raw member against
"error" works because of the str mixin, but only for a model built
in-process; a model round-tripped through JSON carries a plain string. Both
shapes are handled by taking .value when it is there.
Read from the in-memory model rather than re-reading reports/ash.sarif.
Verified end to end against a real cdk-nag run: the notifications survive
sanitize_sarif_paths, apply_suppressions_to_sarif,
attach_scanner_details, merge_sarif_report, and a second scanner
merging into the same aggregate. Nothing in that chain rewrites them, which
is why no disk read is needed to see them.
SUPPRESSIONS ARE HONORED, AND THAT IS NOT A CONVENIENCE
A notification carries no suppression of its own -- SARIF puts suppressions on
results -- so read naively this gate would be unsuppressable, and an operator
who has already reviewed a rule's failure and accepted not knowing its verdict
would have no way to say so. That is not hypothetical: this repository's own
.ash/.ash.yaml carries fifteen such entries under the heading "rules that
threw and never ran", each with a reviewed reason, and its own note calls
naming them "the only way to keep the exit code honest". A gate that ignored
them would fail ASH's own default scan with no escape hatch, which is a worse
defect than the one being fixed.
So a rule is reported only when at least one of its not-evaluated results is unsuppressed. The results are what suppression applies to, and consulting them is what lets the existing mechanism reach a fact recorded somewhere it cannot be attached.
kind is the filter rather than the cdk-nag property bag, so this stays
generic to SARIF. Restricting to not-evaluated rows matters: one rule can throw
on one resource while reaching a verdict on another, and counting an ordinary
unsuppressed finding as evidence would report a rule whose only failure was
suppressed.
A rule with an error-level notification and NO matching result is reported. Absence of a result is not evidence of suppression, and defaulting to silence there would reintroduce the silent pass through the one shape nothing checks.
Returns:
| Type | Description |
|---|---|
List[str]
|
Rule ids in sorted order, deduplicated, with the notification's message |
List[str]
|
substituted for a notification that names no rule so a condition is never |
List[str]
|
silently dropped for lacking an id. Empty when every rule was evaluated, |
List[str]
|
which is the case for every scanner that reports no such condition at all. |
Source code in automated_security_helper/interactions/run_ash_scan.py
755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 | |
incomplete_scanner_reason(status)
The reason token for a scanner incomplete_scanners listed, from its status.
A scanner listed with a status the per-scanner pass accepts (PASSED, FAILED, SKIPPED) is there for the targets it lost, not for its status.
Source code in automated_security_helper/interactions/run_ash_scan.py
scan_incompleteness(results, *, gate, one_shard_of_a_split=False)
Ask the exit-1 coverage questions of results, under the gate as resolved.
gate is _resolve_fail_on_incomplete_scanners's answer. When it is off,
only the unevaluated-rule arm is asked, because that arm is the only one
_compute_exit_code asks with the gate off. Pass gate=True to get the
facts whatever the gate says, which is what a report that states coverage
needs.
one_shard_of_a_split exempts the set-level arm for the reason
_compute_exit_code gives at that arm.
A stale content database is reported in stale_content_databases whatever the
gate says, and ONLY there. incomplete_scanners also lists its scanner, with
the database named in the status, so that the workspace layer and ash merge
see it; a row that is there for nothing else is dropped from scanners here.
Otherwise the one condition would be reported twice with the gate on -- once as
an incomplete scanner whose reason reads partial_coverage and whose advice is
to install a tool that ran, and once as a stale database -- and once with it off,
so the MCP payload and the console message would change shape with a gate the
staleness check does not consult. A scanner that is stale AND lost targets or
did not run keeps its row: that part is a coverage gap in its own right.
Source code in automated_security_helper/interactions/run_ash_scan.py
build_project_scan_settings(opts)
Build the per-project settings record a workspace run scans from.
Module-level and public because there are two callers, not one: the CLI's
_run_workspace_mode and the MCP surface in
automated_security_helper.cli.mcp.workspace. They assemble their
ScanOptions differently -- one from typer arguments, one from MCP tool
parameters -- but the record handed to execute_workspace has to come from
one construction.
Why it is extracted rather than written twice
ProjectScanSettings has 24 fields and every one of them is optional with a
plausible default, so a second construction that omits a field produces a
valid record and a scan that runs to completion with a setting the caller
never chose. Nothing raises. The two worst omissions are config_overrides,
where dropping it silently scans with different configuration, and
ignore_suppressions, where the default is the lenient direction.
What it owns, and why the boundary is here
Both derived inputs are computed inside: the workspace execution config, via
:func:_resolve_workspace_execution_config, which supplies
max_parallel_projects and project_timeout; and the phases list,
which is the only field with branching behind it. A builder that took either
as an argument would push part of the construction back out to its callers,
which is where the duplication started.
Note what it does not own. Setting ASH_OFFLINE stays with the caller:
it mutates process state and has to be unset in a finally, which a
builder returning a value cannot do.
Failure modes
An unreadable ASH config at the workspace root does not raise here.
_resolve_workspace_execution_config warns and falls back to the defaults,
because these are scheduling knobs -- refusing the whole scan over a typo in
one would be a poor trade, and on the MCP path it would surface as an
internal error for what is really an operator's config file.
Source code in automated_security_helper/interactions/run_ash_scan.py
2094 2095 2096 2097 2098 2099 2100 2101 2102 2103 2104 2105 2106 2107 2108 2109 2110 2111 2112 2113 2114 2115 2116 2117 2118 2119 2120 2121 2122 2123 2124 2125 2126 2127 2128 2129 2130 2131 2132 2133 2134 2135 2136 2137 2138 2139 2140 2141 2142 2143 2144 2145 2146 2147 2148 2149 2150 2151 2152 2153 2154 2155 2156 2157 2158 2159 2160 2161 2162 2163 2164 2165 2166 2167 2168 2169 2170 2171 2172 2173 2174 2175 2176 2177 2178 2179 2180 2181 2182 2183 2184 2185 2186 2187 2188 2189 2190 2191 2192 2193 2194 2195 2196 2197 2198 2199 2200 2201 2202 2203 2204 2205 2206 2207 2208 2209 2210 2211 2212 2213 2214 2215 2216 | |
run_ash_scan(source_dir=None, output_dir=None, config=None, config_overrides=None, offline=False, strategy=ExecutionStrategy.PARALLEL, scanners=None, exclude_scanners=None, progress=True, output_formats=None, cleanup=False, phases=None, inspect=False, existing_results=None, python_based_plugins_only=False, quiet=False, simple=False, verbose=False, debug=False, color=True, fail_on_findings=None, fail_on_incomplete_scanners=None, ignore_suppressions=False, min_severity='low', changed_files_only=False, base_ref='origin/main', shard_index=None, shard_count=None, mode=RunMode.local, show_summary=True, log_level=AshLogLevel.INFO, build=True, run=True, force=False, oci_runner=None, build_target=None, offline_semgrep_rulesets='p/ci', container_uid=None, container_gid=None, ash_revision_to_install=None, custom_containerfile=None, custom_build_arg=None, ash_plugin_modules=None, container_network='bridge', workspace_plan=None, allow_missing_projects=False, config_base_gate=None, untrusted_config=False, trusted_config_path=None, *args, **kwargs)
Run an ASH scan against source_dir, outputting results to output_dir.
When workspace_plan is given, source_dir is the workspace root and each
project in the plan is scanned in its own scope. See
:mod:automated_security_helper.workspace.execution.
Source code in automated_security_helper/interactions/run_ash_scan.py
2871 2872 2873 2874 2875 2876 2877 2878 2879 2880 2881 2882 2883 2884 2885 2886 2887 2888 2889 2890 2891 2892 2893 2894 2895 2896 2897 2898 2899 2900 2901 2902 2903 2904 2905 2906 2907 2908 2909 2910 2911 2912 2913 2914 2915 2916 2917 2918 2919 2920 2921 2922 2923 2924 2925 2926 2927 2928 2929 2930 2931 2932 2933 2934 2935 2936 2937 2938 2939 2940 2941 2942 2943 2944 2945 2946 2947 2948 2949 2950 2951 2952 2953 2954 2955 2956 2957 2958 2959 2960 2961 2962 2963 2964 2965 2966 2967 2968 2969 2970 2971 2972 2973 2974 2975 2976 2977 2978 2979 2980 2981 2982 2983 2984 2985 2986 2987 2988 2989 2990 2991 2992 2993 2994 2995 2996 2997 2998 2999 3000 3001 3002 3003 3004 3005 3006 3007 3008 3009 3010 3011 3012 3013 3014 3015 3016 3017 3018 3019 3020 3021 3022 3023 3024 3025 3026 3027 3028 3029 3030 3031 3032 3033 3034 3035 3036 3037 3038 3039 3040 3041 3042 3043 3044 3045 3046 3047 3048 3049 3050 3051 3052 3053 3054 3055 3056 3057 3058 3059 3060 3061 3062 3063 3064 3065 3066 3067 3068 3069 3070 3071 3072 3073 3074 3075 3076 3077 3078 3079 3080 3081 3082 3083 3084 3085 3086 3087 3088 3089 3090 3091 3092 3093 3094 3095 3096 3097 3098 3099 3100 3101 3102 3103 3104 3105 3106 3107 3108 3109 3110 3111 3112 3113 3114 3115 3116 3117 3118 3119 3120 3121 3122 3123 3124 3125 3126 3127 3128 3129 3130 3131 3132 3133 3134 3135 3136 3137 3138 3139 3140 3141 3142 | |
automated_security_helper.models.core
Core models for security findings.
IgnorePathWithReason
Bases: BaseModel
Represents a path exclusion entry.
Source code in automated_security_helper/models/core.py
matches_path(file_path)
Return True if file_path matches this entry's path pattern.
Supports exact matches, simple globs (*.py), and recursive globs
(tests/**/*.py). Matching is case-insensitive for OS portability.
Source code in automated_security_helper/models/core.py
ToolArgs
Bases: BaseModel
Base class for tool argument dictionaries.
Source code in automated_security_helper/models/core.py
AshSuppression
Bases: IgnorePathWithReason
Represents a finding suppression rule.
Source code in automated_security_helper/models/core.py
90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 | |
id
property
Stable identifier derived from path|rule_id|line_start|line_end.
Unspecified rule_id is rendered as *. When line_end is None,
line_start is reused to match how suppressions are indexed elsewhere
in the codebase.
A suppression that sets any package field gets a fifth part,
name@version@path with * for each unset piece, so two entries
that differ only by package do not share an id. Entries without
package fields keep the four-part id they always had.
A suppression that sets symbol gets that fifth part (*@*@*
when no package field is set) and the symbol as a sixth. Entries
without a symbol keep the id they had before the field existed.
is_expired
property
Return True if this suppression has a past expiration date.
days_until_expiry
property
Days from today until expiration; None if no expiration is set.
A negative value indicates the suppression has already expired.
validate_line_range(v, values)
classmethod
Validate that line_end is greater than or equal to line_start if both are provided.
Source code in automated_security_helper/models/core.py
validate_package_field_not_blank(v)
classmethod
Reject a blank package field: it reads as "any" but would match nothing.
Source code in automated_security_helper/models/core.py
validate_symbol(v)
classmethod
Reject a symbol that is not a dotted qualified name.
A malformed name could never equal a parsed definition's name, so the entry would silently match nothing.
Source code in automated_security_helper/models/core.py
validate_expiration_date(v)
classmethod
Validate that expiration date is in YYYY-MM-DD format.
Past dates are accepted; use is_expired to check whether the suppression has expired at runtime.
Source code in automated_security_helper/models/core.py
matches(finding, symbol_resolver=None)
Return True if finding is covered by this suppression rule.
Checks rule_id (exact or glob), path (supports **), optional line
range overlap, the optional package fields, and the optional symbol.
Expired suppressions never match.
Each package field that is set must match the finding's corresponding field. A finding that does not carry that field does not match: the scanner could not say which package it is about, so a package-scoped suppression must not assume it is the one intended.
A symbol is checked last, so a file is parsed only for a finding that
every other field already matches. It needs symbol_resolver, which
reads the scanned source; without one a symbol-scoped suppression
matches nothing, and so does a finding with no line number or in a
file the resolver cannot parse.
Source code in automated_security_helper/models/core.py
suppression_id(suppression)
Identifier for a suppression given as a dict of its fields.
Shared by AshSuppression.id and the config linter, which reads raw YAML
dicts, so the unused-suppressions report and the linter's fixer agree.