Subscribe to scan events¶
An event subscriber is a function that ASH calls at a point in a scan: when a phase starts, when a scanner finishes, or when the whole scan ends. Use one to log progress somewhere ASH does not, to time scans, or to start work that depends on a scanner's result. This page builds a module with two subscribers, one that prints a line as each scanner finishes and one that prints the scan's duration at the end.
Prerequisites:
- ASH installed (Install ASH);
- a repository to scan, with your shell in its top directory. To keep the steps short, this page saves the module in an
ash-pluginsdirectory there; a module you keep belongs outside the repositories you scan (Package a third-party plugin).
Plugins explains how ASH loads plugin modules and what a loaded module can do.
1. Write the subscribers¶
Save this as ash-plugins/acme_ash_plugins/__init__.py:
from automated_security_helper.plugins.decorators import event_subscriber
from automated_security_helper.plugins.events import AshEventType
@event_subscriber(AshEventType.SCAN_COMPLETE)
def scanner_finished(**kwargs):
print(f"acme: {kwargs['scanner']} finished, {kwargs['remaining_count']} left")
@event_subscriber(AshEventType.EXECUTION_COMPLETE)
def run_finished(**kwargs):
print(f"acme: run finished in {kwargs['duration']:.1f} s")
2. Scan with them¶
export PYTHONPATH="$PWD/ash-plugins"
ash scan --ash-plugin-modules acme_ash_plugins --scanners detect-secrets --offline
Among the scan's output are the two lines the subscribers print:
ash plugin list does not show event subscribers.
How subscribing works¶
@event_subscriber(event_type) subscribes the function when ASH imports the module, which happens when a module list names it (Load a plugin module). ASH calls each subscriber with keyword arguments only, so declare **kwargs and read the arguments you need; the table below lists them. Return values are ignored. Subscribing the same function to the same event twice has no effect.
A module can instead list its subscribers in a dict at module level, which ASH reads when it loads a module from a module list:
from automated_security_helper.plugins.events import AshEventType
def scanner_finished(**kwargs):
print(kwargs["scanner"])
ASH_EVENT_HANDLERS = {AshEventType.SCAN_COMPLETE: [scanner_finished]}
Subscribers to one event run in the order they were subscribed. The order between modules is not fixed, so do not make one module's subscriber depend on another's.
Events¶
EXECUTION_START comes before the first phase and EXECUTION_COMPLETE after the last. Phases run in the order convert, scan, report, and inspect; each emits its _PHASE_START event, then its per-plugin events, then its _PHASE_COMPLETE event. Events emitted during a phase also receive phase and plugin_context.
| Event | When | Keyword arguments |
|---|---|---|
EXECUTION_START |
once, before the first phase | phases, plugin_context, source_dir, output_dir, enabled_scanners, excluded_scanners, python_based_plugins_only, strategy, message |
CONVERT_PHASE_START, SCAN_PHASE_START, REPORT_PHASE_START, INSPECT_PHASE_START |
when each phase starts; inspect runs only with --inspect |
the scan phase adds enabled_scanners, excluded_scanners, parallel, max_workers, global_ignore_paths, shard_index, and shard_count; the report phase adds report_dir and cli_output_formats |
CONVERT_START |
before each converter runs | converter, converter_class, message |
CONVERT_COMPLETE |
after each converter that did not raise | the same, plus converted_files and file_count |
SCAN_START |
before each scanner, when scanners run one at a time: with --strategy sequential, or when only one scanner runs |
scanner, scanner_class, scan_targets, message |
SCAN_COMPLETE |
after each scanner, including one that failed | scanner, completed_count, total_count, remaining_count, remaining_scanners, message |
REPORT_START |
before each reporter | reporter, reporter_class, message |
REPORT_COMPLETE |
after each reporter that returned a string; when the string was empty, no file was written, and output_file and output_filename are None |
the same, plus output_file and output_filename |
CONVERT_PHASE_COMPLETE, SCAN_PHASE_COMPLETE, REPORT_PHASE_COMPLETE, INSPECT_PHASE_COMPLETE |
when each phase ends | the phase's start arguments, plus results |
EXECUTION_COMPLETE |
once, at the end, also when the scan failed | phases, results, duration (seconds), completed_scanners, message |
ERROR, WARNING, INFO |
when ASH records a problem or a validation checkpoint | message, and details that depend on the source |
AshEventType also has EXECUTION_PROGRESS, CONVERT_PHASE_PROGRESS, SCAN_PHASE_PROGRESS, REPORT_PHASE_PROGRESS, INSPECT_PHASE_PROGRESS, INSPECT_START, and INSPECT_COMPLETE. ASH never emits them. When two or more scanners run under the default parallel strategy, SCAN_START is not emitted either; use SCAN_PHASE_START to learn which scanners will run.
When a subscriber fails¶
A subscriber runs in ASH's own process, in the thread that emitted the event, and ASH waits for it before it continues. Keep subscribers fast: one that calls a network service delays the scan by that call.
If a subscriber raises an exception, the subscribers after it for that event do not run. What else happens depends on the event:
- For a
_PHASE_STARTor_PHASE_COMPLETEevent, the scan stops. It exits with code 1 and writes no reports. - For any other event, ASH logs the error and the scan continues.
Catch exceptions inside your subscriber unless you mean it to stop the scan.
Built-in subscribers¶
ASH subscribes two functions of its own. At SCAN_COMPLETE one logs which scanners are still running, as in Scanner bandit completed. 2 remaining: semgrep, checkov, and All scanners completed! after the last one. At EXECUTION_START the other checks the expiration dates in the config (Expiration). It logs, in this order, one warning that names every suppression and ignore path that has already expired, one that expires today included; a warning for the ignore paths that expire within the next 30 days; and a warning for the suppressions that do. Under --ignore-suppressions it leaves the suppressions out of these warnings and still names the ignore paths.