Skip to content

Commit db71e64

Browse files
authored
fix: resolve 14 bugs with TDD regression tests (#273)
* fix: resolve 14 verified production bugs with TDD regression tests Bugs fixed (identified via semi-formal code reasoning analysis with 41 agents across 3 rounds + devil's advocate review): Critical: - C2: --output-formats CLI flag now propagates through orchestrator to execution engine to report phase (was silently ignored) - C5: hasattr-on-dict bugs in sarif_utils.py and scanner_statistics_calculator.py replaced with proper dict key checks, restoring issue_severity extraction from Bandit High: - H2: FlatJSON reporter no longer injects phantom test finding on clean scans (test scaffolding removed from production) - H3: Bedrock severity grouping uses "level" key (was "severity") - H4: SecurityHub model_post_init__ renamed to model_post_init - H5: Trivy scanner uses --severity with inclusion list mapping (was --severity-threshold which does not exist in Trivy CLI) - H8: report_id strftime uses %m month (was %M minute) - H-NEW: GitLabSASTReporter and UnusedSuppressionsReporter added to ASH_REPORTERS discovery list Medium: - M1: cancel_scan logs warning when process_id is None - M3: EnginePhase uses AshEventType enum lookup (was string) - M6: find_executable checks all command variants (was early return) - M7: glob uses recursive=True for nested .gitignore discovery - M8: Syft scanner preserves exclude paths after args rebuild - M9: OCSF suppression counter uses StatusId.integer_3 (was 4) Cleanup: - Removed debug print statements from mcp_server.py - Fixed split["."] syntax error in inspect_findings_app.py - Deleted 409-line dead code file models/uv_tool_installation.py - Deleted empty placeholder scan_tracking_modified.py 46 new regression tests added across 9 test files. 1016 tests pass, 0 failures, 0 regressions. * fix: Windows CI compatibility for glob and find_executable tests - Glob recursive tests now check both forward and backslash path separators - find_executable test allows multiple which() calls (now iterates all variants) * fix: replace hardcoded /tmp/ paths with tempfile.gettempdir() in tests Resolves Bandit B108 findings flagged by ASH scan on test files. * refactor(tests): organize regression tests by domain, not severity Redistribute tests from test_low_fixes.py and test_medium_fixes.py into domain-appropriate locations: - M1 (cancel_scan) -> core/resource_management/test_cancel_scan_warning.py - M3 (engine_phase) -> core/test_engine_phase_events.py - M6 (find_executable) -> utils/test_find_executable_fix.py - M7 (glob recursive) -> utils/test_glob_recursive_fix.py - L1 (mcp debug) -> cli/test_mcp_debug_prints_fix.py - L2 (inspect split) -> cli/test_inspect_split_fix.py - Dead code -> test_dead_code_removal.py No behavior change. Same 1016 tests pass. * fix(syft): create target results directory before subprocess execution The target_results_dir (e.g., /out/scanners/syft/source/) was not created before calling syft, causing [Errno 2] when syft tried to write its output file. Moved the mkdir call before _run_subprocess and removed the redundant late mkdir that ran after the file read. * fix(syft): use --exclude flag instead of --skip-path Syft uses --exclude for path exclusion, not --skip-path (which is silently ignored, causing syft to produce no output). Same class of bug as H5 (Trivy wrong flag name). Previously masked by M8 bug (exclude paths were discarded before reaching the command line). * fix: proper exit code handling across all scanners Two fixes for scanner exit code handling: 1. executionSuccessful in SARIF Invocation now reflects actual exit codes instead of being hardcoded True. Exit 0 and 1 (findings found) are treated as successful for most tools. Grype uses inverted semantics (exit 1=error, 2=findings) and is handled separately. 8 scanners fixed, 35 new tests. 2. Multi-subprocess scanners (cfn_nag, npm_audit) now retain the worst exit code across loop iterations instead of only the last. Uses max(self.exit_code, new_code) in plugin_base.py. 5 new tests. Research verified exit code semantics from official documentation for all 13 external tools ASH wraps: Bandit, Checkov, Grype, Syft, Semgrep, OpenGrep, npm/yarn/pnpm audit, cfn_nag, CDK Nag, Trivy, Snyk, detect-secrets, and Ferret. * fix(syft): stop passing global_ignore_paths as --exclude to syft global_ignore_paths (patterns like **/.venv/**) are designed for SARIF-level finding suppression, not for syft's --exclude flag. Passing them caused syft to produce empty output in Docker containers, failing the container scan tests. Syft generates SBOMs (inventory), not security findings, so SARIF ignore paths are irrelevant. Scanner-specific exclude paths from config still work via _process_config_options. * chore: bump version to 3.3.0
1 parent f4a061d commit db71e64

59 files changed

Lines changed: 1716 additions & 574 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -91,18 +91,18 @@ ASH v3 integrates multiple open-source security tools as scanners. Tools like Ba
9191
curl -sSfL https://astral.sh/uv/install.sh | sh
9292

9393
# Create an alias for ASH
94-
alias ash="uvx git+https://github.com/awslabs/automated-security-helper.git@v3.2.7"
94+
alias ash="uvx git+https://github.com/awslabs/automated-security-helper.git@v3.3.0"
9595
```
9696

9797
```powershell
9898
# Install uv on Windows with PowerShell if it isn't installed already
9999
irm https://astral.sh/uv/install.ps1 | iex
100100
101101
# Create a function for ASH
102-
function ash { uvx git+https://github.com/awslabs/automated-security-helper.git@v3.2.7 $args }
102+
function ash { uvx git+https://github.com/awslabs/automated-security-helper.git@v3.3.0 $args }
103103
```
104104

105-
> **Floating tag `v3`**: We also maintain a `v3` floating tag that always points to the latest stable v3.x release. You can use `@v3` instead of `@v3.2.7` to stay up to date automatically. Pin a specific version (e.g., `@v3.2.7`) when you need reproducible builds.
105+
> **Floating tag `v3`**: We also maintain a `v3` floating tag that always points to the latest stable v3.x release. You can use `@v3` instead of `@v3.3.0` to stay up to date automatically. Pin a specific version (e.g., `@v3.3.0`) when you need reproducible builds.
106106
107107
### Other Installation Methods
108108

@@ -122,13 +122,13 @@ ash --help
122122
#### Using `pip`
123123

124124
```bash
125-
pip install git+https://github.com/awslabs/automated-security-helper.git@v3.2.7
125+
pip install git+https://github.com/awslabs/automated-security-helper.git@v3.3.0
126126
```
127127

128128
#### Clone the Repository
129129

130130
```bash
131-
git clone https://github.com/awslabs/automated-security-helper.git --branch v3.2.7
131+
git clone https://github.com/awslabs/automated-security-helper.git --branch v3.3.0
132132
cd automated-security-helper
133133
pip install .
134134
```

automated_security_helper/base/engine_phase.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from automated_security_helper.core.enums import ExecutionPhase
1111
from automated_security_helper.models.asharp_model import AshAggregatedResults
1212
from automated_security_helper.plugins import ash_plugin_manager
13+
from automated_security_helper.plugins.events import AshEventType
1314
from automated_security_helper.utils.log import ASH_LOGGER
1415

1516

@@ -87,7 +88,14 @@ def execute(
8788
self.initialize_progress()
8889

8990
# Notify phase start
90-
start_event = f"{self.phase_name.upper()}_PHASE_START"
91+
start_event_name = f"{self.phase_name.upper()}_PHASE_START"
92+
try:
93+
start_event = AshEventType[start_event_name]
94+
except KeyError:
95+
ASH_LOGGER.warning(
96+
f"No AshEventType member '{start_event_name}'; falling back to string"
97+
)
98+
start_event = start_event_name
9199
ASH_LOGGER.debug(f"EnginePhase.execute: Notifying {start_event} event")
92100
self.notify_event(start_event, **kwargs)
93101

@@ -126,7 +134,14 @@ def execute(
126134
self._update_summary_stats()
127135

128136
# Notify phase complete
129-
complete_event = f"{self.phase_name.upper()}_PHASE_COMPLETE"
137+
complete_event_name = f"{self.phase_name.upper()}_PHASE_COMPLETE"
138+
try:
139+
complete_event = AshEventType[complete_event_name]
140+
except KeyError:
141+
ASH_LOGGER.warning(
142+
f"No AshEventType member '{complete_event_name}'; falling back to string"
143+
)
144+
complete_event = complete_event_name
130145
ASH_LOGGER.debug(f"EnginePhase.execute: Notifying {complete_event} event")
131146
self.notify_event(complete_event, results=results, **kwargs)
132147

@@ -213,7 +228,11 @@ def update_progress(self, completed: int, description: str = None) -> None:
213228
)
214229

215230
# Notify progress event
216-
progress_event = f"{self.phase_name.upper()}_PHASE_PROGRESS"
231+
progress_event_name = f"{self.phase_name.upper()}_PHASE_PROGRESS"
232+
try:
233+
progress_event = AshEventType[progress_event_name]
234+
except KeyError:
235+
progress_event = progress_event_name
217236
self.notify_event(
218237
progress_event, completed=completed, description=description
219238
)

automated_security_helper/base/plugin_base.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -288,8 +288,9 @@ def _run_subprocess(
288288
if "stderr" in response and response["stderr"]:
289289
self.errors.extend(response["stderr"].splitlines())
290290

291-
# Set exit code (default to 1 if not available)
292-
self.exit_code = response.get("returncode", 1)
291+
# Accumulate worst exit code across multiple subprocess calls
292+
new_code = response.get("returncode", 1)
293+
self.exit_code = max(self.exit_code, new_code)
293294

294295
return response
295296

@@ -385,8 +386,8 @@ def _try_uv_tool_execution(
385386
if response["stderr"]:
386387
self.errors.extend(response["stderr"].splitlines())
387388

388-
# Set exit code
389-
self.exit_code = response["returncode"]
389+
# Accumulate worst exit code across multiple subprocess calls
390+
self.exit_code = max(self.exit_code, response["returncode"])
390391

391392
self._plugin_log(
392393
f"UV tool execution completed for {self.command} with exit code {response['returncode']}",

automated_security_helper/cli/inspect/inspect_findings_app.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ def compose(self) -> ComposeResult:
111111
if self.finding.get("code_snippet"):
112112
md_text += f"""\n\n## Code
113113
114-
```{Path(self.finding["file"]).name.split["."][-1]}
114+
```{Path(self.finding["file"]).name.split(".")[-1]}
115115
{self.finding.get("code_snippet")}
116116
```
117117
"""

automated_security_helper/cli/mcp_server.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -985,12 +985,6 @@ async def get_scan_summary(
985985
Args:
986986
output_dir: Path to the scan output directory (absolute path recommended)
987987
"""
988-
import sys
989-
990-
print("=" * 80, file=sys.stderr)
991-
print("DEBUG: get_scan_summary FUNCTION CALLED!", file=sys.stderr)
992-
print("=" * 80, file=sys.stderr)
993-
994988
try:
995989
# Ensure output_dir is an absolute path
996990
if not Path(output_dir).is_absolute():

automated_security_helper/core/execution_engine.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ def __init__(
6363
python_based_plugins_only: bool = False,
6464
simple_mode: bool = False,
6565
ash_plugin_modules: List[str] = [],
66+
output_formats: List[str] = [],
6667
):
6768
"""Initialize the execution engine.
6869
@@ -130,6 +131,7 @@ def __init__(
130131
python_based_plugins_only # Store the python_based_plugins_only flag
131132
)
132133
self._simple_mode = simple_mode # Store the simple_mode flag
134+
self._output_formats = output_formats # CLI --output-formats
133135

134136
# Get debug and verbose flags from environment or config
135137
debug_flag = debug or os.environ.get("ASH_DEBUG", "").lower() in [
@@ -510,11 +512,7 @@ def execute_phases(
510512
# )
511513
self._results = report_phase.execute(
512514
report_dir=self._context.output_dir.joinpath("reports"),
513-
cli_output_formats=(
514-
self._context.config.output_formats
515-
if hasattr(self._context.config, "output_formats")
516-
else None
517-
),
515+
cli_output_formats=self._output_formats or None,
518516
aggregated_results=self._results,
519517
python_based_plugins_only=self._python_only,
520518
)

automated_security_helper/core/orchestrator.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,7 @@ def model_post_init(self, context):
234234
debug=self.debug,
235235
python_based_plugins_only=self.python_based_plugins_only,
236236
ash_plugin_modules=self.ash_plugin_modules, # Pass the ash_plugin_modules to the execution engine
237+
output_formats=self.output_formats,
237238
**exec_engine_params,
238239
)
239240

automated_security_helper/core/resource_management/scan_registry.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -383,7 +383,12 @@ def cancel_scan(self, scan_id: str) -> bool:
383383
return False
384384

385385
# Try to terminate the process if we have a process ID
386-
if entry.process_id:
386+
if entry.process_id is None:
387+
self._logger.warning(
388+
f"Scan {scan_id} has no process ID (thread-based scan); "
389+
"cannot send termination signal"
390+
)
391+
elif entry.process_id:
387392
try:
388393
os.kill(entry.process_id, signal.SIGTERM)
389394
self._logger.info(

automated_security_helper/core/resource_management/scan_tracking_modified.py

Whitespace-only changes.

automated_security_helper/core/scanner_statistics_calculator.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
from automated_security_helper.config.default_config import get_default_config
3333
from automated_security_helper.core.constants import ASH_DEFAULT_SEVERITY_LEVEL
3434
from automated_security_helper.models.asharp_model import AshAggregatedResults
35+
from automated_security_helper.schemas.sarif_schema_model import PropertyBag
3536
from automated_security_helper.utils.log import ASH_LOGGER
3637

3738

@@ -372,9 +373,15 @@ def extract_sarif_counts_for_scanner(
372373
if not is_suppressed:
373374
# Check if scanner provides issue_severity in properties (e.g., Bandit)
374375
properties = result.properties or {}
376+
if isinstance(properties, PropertyBag):
377+
properties = properties.model_dump(
378+
mode="json",
379+
exclude_unset=True,
380+
exclude_none=True,
381+
)
375382
issue_severity = (
376383
properties.get("issue_severity", "").upper()
377-
if hasattr(properties, "get")
384+
if isinstance(properties, dict)
378385
else ""
379386
)
380387

0 commit comments

Comments
 (0)