Summary
Suppressions silently fail to match when the SARIF URI emitted by a scanner is an absolute path whose prefix differs from source_dir.resolve(), even though both refer to the same file. This happens reliably on macOS for any scan whose source-dir is under /tmp (because /tmp is a symlink to /private/tmp), and would happen any time the source directory contains symlinks anywhere in its path.
The result: the finding is reported as actionable AND the suppression is reported as "unused" by the unused-suppressions reporter.
Reproduction (macOS)
mkdir -p /tmp/ash-bug-repro/src/.ash
mkdir -p /tmp/ash-bug-repro/rules/problem-based-packs/insecure-transport/js-node
cat > /tmp/ash-bug-repro/src/insecure.js <<'JS'
process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = 0;
const https = require('https');
const agent = new https.Agent({rejectUnauthorized: false});
JS
curl -sL https://raw.githubusercontent.com/opengrep/opengrep-rules/main/problem-based-packs/insecure-transport/js-node/bypass-tls-verification.yaml \
-o /tmp/ash-bug-repro/rules/problem-based-packs/insecure-transport/js-node/bypass-tls-verification.yaml
cat > /tmp/ash-bug-repro/.ash/.ash.yaml <<'YAML'
project_name: ash-bug-repro
fail_on_findings: false
global_settings:
severity_threshold: LOW
suppressions:
- rule_id: "rules.problem-based-packs.insecure-transport.js-node.bypass-tls-verification"
path: "src/insecure.js"
reason: "Test"
scanners:
opengrep:
enabled: true
options:
config: rules
YAML
ash --source-dir /tmp/ash-bug-repro --mode local
Observed
- Opengrep produces 2 findings, both reported as actionable.
reports/ash.unused-suppressions.json reports the suppression as unused.
ash_aggregated_results.json shows the SARIF URI as /tmp/ash-bug-repro/src/insecure.js (absolute), and used_suppressions is empty.
Expected
- Both findings suppressed.
used_suppressions contains the suppression id.
- Unused-suppressions report shows it as used.
Verification that this is the symlink issue
Re-running the same repro under a non-symlinked path (e.g. /Users/<me>/ash-bug-repro) produces the correct behavior: SARIF URIs get normalized to src/insecure.js, both findings are suppressed, and used_suppressions contains the entry. Only the /tmp (or any other symlinked) path fails.
Root cause
In automated_security_helper/utils/sarif_utils.py::apply_suppressions_to_sarif, the URI normalization computes:
_source_dir_prefix = str(plugin_context.source_dir.resolve()).replace("\\", "/") + "/"
and then strips this prefix from each SARIF URI. On macOS, Path("/tmp/x").resolve() returns /private/tmp/x, so the prefix becomes /private/tmp/x/ while the SARIF URI emitted by opengrep is /tmp/x/src/insecure.js. The prefix never matches, and the URI is left as an absolute path, which then fails to match the suppression's relative path.
The same issue affects:
- the
ignore_paths evaluation in the same function (lines ~478)
- any scanner whose SARIF URIs use the unresolved path the user invoked ASH with
The function already handles a few Windows-specific variants (/D:/..., drive-letter-stripped paths) but does not handle the symlink case.
Suggested fix
Resolve symlinks on the SARIF URI before prefix-stripping, or compute multiple prefix candidates (e.g. both source_dir and source_dir.resolve()), or fall back to comparing Path(uri).resolve() against source_dir.resolve(). Care needed for offline opengrep paths (<basename>/...) that the existing code already handles separately.
Impact
- High user-visible: suppressions silently no-op when the source directory contains symlinks anywhere in its path. Users see findings they thought were suppressed AND see their suppression flagged as "unused" — the symptom looks like a config bug, not a path-normalization bug.
- Affects any macOS user scanning under
/tmp (common in CI scripts, smoke tests, sandboxes).
- Affects any environment where the source directory traverses a symlink (mount points, container bind mounts, etc.).
Environment
- ASH version: 3.5.2 (verified locally)
- macOS (any version where
/tmp is symlinked to /private/tmp)
- Scanner: opengrep (but the bug is in shared SARIF post-processing, so any scanner emitting absolute paths is affected)
Summary
Suppressions silently fail to match when the SARIF URI emitted by a scanner is an absolute path whose prefix differs from
source_dir.resolve(), even though both refer to the same file. This happens reliably on macOS for any scan whosesource-diris under/tmp(because/tmpis a symlink to/private/tmp), and would happen any time the source directory contains symlinks anywhere in its path.The result: the finding is reported as actionable AND the suppression is reported as "unused" by the unused-suppressions reporter.
Reproduction (macOS)
Observed
reports/ash.unused-suppressions.jsonreports the suppression as unused.ash_aggregated_results.jsonshows the SARIF URI as/tmp/ash-bug-repro/src/insecure.js(absolute), andused_suppressionsis empty.Expected
used_suppressionscontains the suppression id.Verification that this is the symlink issue
Re-running the same repro under a non-symlinked path (e.g.
/Users/<me>/ash-bug-repro) produces the correct behavior: SARIF URIs get normalized tosrc/insecure.js, both findings are suppressed, andused_suppressionscontains the entry. Only the/tmp(or any other symlinked) path fails.Root cause
In
automated_security_helper/utils/sarif_utils.py::apply_suppressions_to_sarif, the URI normalization computes:and then strips this prefix from each SARIF URI. On macOS,
Path("/tmp/x").resolve()returns/private/tmp/x, so the prefix becomes/private/tmp/x/while the SARIF URI emitted by opengrep is/tmp/x/src/insecure.js. The prefix never matches, and the URI is left as an absolute path, which then fails to match the suppression's relativepath.The same issue affects:
ignore_pathsevaluation in the same function (lines ~478)The function already handles a few Windows-specific variants (
/D:/..., drive-letter-stripped paths) but does not handle the symlink case.Suggested fix
Resolve symlinks on the SARIF URI before prefix-stripping, or compute multiple prefix candidates (e.g. both
source_dirandsource_dir.resolve()), or fall back to comparingPath(uri).resolve()againstsource_dir.resolve(). Care needed for offline opengrep paths (<basename>/...) that the existing code already handles separately.Impact
/tmp(common in CI scripts, smoke tests, sandboxes).Environment
/tmpis symlinked to/private/tmp)