Testland
Browse all skills & agents

language-native-sast

Language-native SAST linters - the first-party "linter as SAST" family that runs inside each ecosystem's standard toolchain with no separate scanner server: Bandit (Python, 60+ B-rules, severity x confidence filtering), gosec (Go, 40+ G-rules, AST + SSA taint tracking, golangci-lint integration), eslint-plugin-security + eslint-plugin-no-unsanitized (JS/TS, 14 detect-* rules + DOM-sink XSS), and PMD's Apex security ruleset (Salesforce, ApexSOQLInjection / ApexCRUDViolation / ApexSharingViolations). Covers the shared adoption pattern - install as a dev dependency, first scan, suppression-with-justification discipline, baseline-diff adoption for legacy code, SARIF output + CI gating - with per-tool depth in references. Use when a repo needs in-toolchain security linting for Python, Go, JavaScript/TypeScript, or Apex; for cross-language or cross-file taint analysis use semgrep-rules / codeql-queries instead.

Install with skills.sh (any agent)

npx skills add testland/qa --skill language-native-sast
View source

language-native-sast

Overview

Every major ecosystem ships a first-party security linter that runs inside the toolchain developers already use - no scanner server, no new binary in the inner loop. This family trades depth for adoption cost: rules are single-file and pattern-based, but the feedback lands in the editor and on every commit, which is where shallow bugs (hardcoded creds, shell=True, innerHTML =, dynamic SOQL) actually get fixed.

LanguageToolReference
PythonBandit (PyCQA)references/bandit.md
Gogosec (securego), via golangci-lintreferences/gosec.md
JS / TSeslint-plugin-security + eslint-plugin-no-unsanitizedreferences/eslint-security.md
Salesforce ApexPMD category/apex/security.xmlreferences/pmd-apex.md

All four follow the same adoption pattern (below). The worked example uses the ESLint pair - the biggest audience; the per-tool references carry the full flag/rule catalogs, verified against each tool's docs.

Differentiation: these linters are single-file and (mostly) syntactic. For cross-language pattern rules use semgrep-rules; for cross-file taint tracking use codeql-queries; to merge their findings with other scanners into one gate use multi-tool-finding-triage.

When to use

  • The repo is Python, Go, JS/TS, or Apex and the team wants shift-left security feedback in the editor and on every commit, not only in a scheduled CI scan.
  • CI needs a fast, PR-blocking security lint pass without adopting a scanner server or a new SaaS.
  • A legacy codebase is adopting security linting and pre-existing findings must not block every PR (baseline-diff pattern, Step 3).

The pattern

Each tool's exact commands live in its reference; the workflow is shared.

Step 1 - Install as a dev dependency

The linter installs through the ecosystem's own package manager - it versions, caches, and updates like any other dev dependency:

pip install bandit[toml]                        # Python
go install github.com/securego/gosec/v2/cmd/gosec@latest   # Go
npm install --save-dev eslint-plugin-security eslint-plugin-no-unsanitized  # JS/TS
# Apex: PMD zip / Docker image (Java 8+) - see references/pmd-apex.md

Step 2 - First scan, then filter the noise

Run the recursive scan, then immediately narrow with the tool's severity/confidence filters - every tool in this family is noisy at default settings, and an unfiltered first run is how teams end up disabling the linter:

bandit -r . --severity-level=medium --confidence-level=medium
gosec -severity=high -confidence=high ./...
npx eslint "src/**/*.{js,ts}"      # rules pre-scoped by the shared config
pmd check -d . -R category/apex/security.xml --minimum-priority 2

Step 3 - Baseline-diff adoption for legacy code

On a legacy codebase, capture the current findings once and gate only on NEW findings, so the debt is tracked without blocking every PR:

  • Bandit: bandit -r . -f json -o old-findings.json, then bandit -r . --baseline old-findings.json in CI.
  • The generic pattern for the other tools: run the scan on main, save the report, diff per-PR findings against it and fail only on additions (multi-tool-finding-triage implements this diff + waiver flow).
  • Re-baseline on a schedule; persisted findings need waiver entries.

Step 4 - Suppression with justification (MANDATORY)

Every tool has an inline suppression syntax; every suppression carries a reason, reviewer, and expiry, audited quarterly:

ToolInline syntax
Bandit# nosec B602 (always with the rule ID)
gosec// #nosec G401 -- Reason: ...
ESLint// eslint-disable-next-line security/detect-object-injection + reason comment
PMD Apex@SuppressWarnings('PMD.ApexCRUDViolation') + reason comment

Bare suppressions (no rule ID, no reason) are unreviewed debt - grep for them in the quarterly audit. Categorical noise (a rule that can never apply) belongs in the tool's config file, not scattered inline.

Step 5 - CI wiring: SARIF + exit-code gate

All four tools emit SARIF for GitHub Code Scanning and gate CI via exit code. The shape is identical per tool - scan, upload SARIF if: always(), let the exit code block the PR:

      - run: bandit -r . -f sarif -o bandit.sarif        # or gosec / eslint / pmd
      - uses: github/codeql-action/upload-sarif@v3
        if: always()
        with: { sarif_file: bandit.sarif }

JSON output from each tool feeds multi-tool-finding-triage for the cross-scanner dedupe + waiver gate.

Worked example - ESLint security pair (JS/TS)

Per github.com/eslint-community/eslint-plugin-security (opens in new window) and github.com/mozilla/eslint-plugin-no-unsanitized (opens in new window):

// eslint.config.js (flat config, ESLint 8.23+ / 9+)
import pluginSecurity from "eslint-plugin-security";
import nounsanitized from "eslint-plugin-no-unsanitized";

export default [
  pluginSecurity.configs.recommended,   // all 14 detect-* rules
  nounsanitized.configs.recommended,    // method + property DOM-sink rules
];

A scan of this code:

const userData = req.body;
element.innerHTML = userData.bio;             // nounsanitized/property
const file = fs.readFileSync(req.query.path); // security/detect-non-literal-fs-filename

reports both findings; the safe rewrites are element.textContent = ... (or DOMPurify for rich HTML) and an allowlist check on the path. The highest-volume false positive is security/detect-object-injection (any obj[key] access) - triage it per Step 4 or downgrade it to warn in config. SARIF output uses the scoped formatter name in full:

npx eslint --format @microsoft/eslint-formatter-sarif \
  --output-file eslint-security.sarif "src/**/*.{js,ts}"

ESLint exit codes per eslint.org/docs/latest/use/command-line-interface (opens in new window): 0 clean, 1 errors (the gate), 2 config error. Full rule tables, the legacy-config variant, and the complete two-pass CI workflow (JSON for the triager + SARIF for Code Scanning) are in references/eslint-security.md.

Anti-patterns

Anti-patternWhy it failsFix
Unfiltered first scan on a legacy repoNoise overwhelms; team disables the linterSeverity/confidence filters (Step 2) + baseline (Step 3)
Bare suppression without rule IDSuppresses ALL rules on that lineAlways name the rule (Step 4)
Suppression without reason + expiryPermanent unreviewed debtJustification template, quarterly audit (Step 4)
Running the linter only in scheduled CILoses the in-editor shift-left benefitDev-dependency install + pre-commit (Step 1)
Treating linter pass as full SAST coverageSingle-file, mostly syntactic analysisLayer semgrep-rules / codeql-queries

Limitations

  • Single-file analysis: none of the four track taint across module or package boundaries (gosec's SSA taint tracking is in-package). Use codeql-queries for interprocedural flows.
  • Rule depth varies by ecosystem era - Bandit is thinner on FastAPI/async patterns, gosec on generics, PMD Apex does not parse Lightning Web Components (use semgrep-rules for LWC JavaScript).
  • High-volume false-positive rules exist in each tool (detect-object-injection, G104, B101, ApexCRUDViolation on Visualforce getters) - the per-tool references name them and the sanctioned handling.

References

  • references/bandit.md - Bandit: rules, flags, pyproject config, pre-commit, baseline
  • references/gosec.md - gosec: G-rule catalog, golangci-lint integration, #nosec syntax
  • references/eslint-security.md - ESLint pair: 14+2 rule tables, SARIF formatter, CI workflow
  • references/pmd-apex.md - PMD Apex: 10-rule security category, custom rulesets, incremental cache
  • semgrep-rules, codeql-queries, sonarqube-rules - deeper / cross-language SAST siblings
  • multi-tool-finding-triage - cross-scanner dedupe + waiver gate

Bandit - Python language-native SAST

View source (opens in new window)

Bandit - Python language-native SAST

Per-tool reference for language-native-sast. Bandit is the Python-specific SAST originally from OpenStack Security.

Per bandit.readthedocs.io/en/latest/start.html (opens in new window):

Each finding has two dimensions:

  • Severity (LOW / MEDIUM / HIGH) - how dangerous if exploited
  • Confidence (LOW / MEDIUM / HIGH) - how certain Bandit is the finding is real

The two-dimensional scoring lets you tune false-positive vs false-negative tradeoff per project.

Install

Per bd-start (opens in new window):

pip install bandit[toml]

The [toml] extra enables pyproject.toml config support.

Basic recursive scan

Per bd-start (opens in new window):

bandit -r path/to/your/code

Common usage:

bandit -r .                              # current dir, recursive
bandit -r src/ tests/                    # multiple paths
bandit -r . -x tests,vendor              # exclude dirs
bandit -r . -ll                           # minimum LOW confidence + LOW severity

Severity + confidence filtering

Per bd-start (opens in new window) verbatim CLI usage:

bandit examples/*.py -n 3 --severity-level=high

Combined two-dimensional filtering:

# Only HIGH severity findings with HIGH confidence
bandit -r . --severity-level=high --confidence-level=high

# All MEDIUM+ severity, any confidence
bandit -r . --severity-level=medium

The two flags compose; neither is a strict subset of the other.

pyproject.toml config

# pyproject.toml
[tool.bandit]
exclude_dirs = ["tests", "vendor", "build"]
skips = ["B101"]                       # skip "assert used" rule globally
tests = ["B201", "B301"]                # only run flask + pickle checks (whitelist mode)

[tool.bandit.assert_used]
skips = ["**/test_*.py", "**/*_test.py"]

tests = [...] activates whitelist mode (run ONLY listed checks); skips = [...] activates blacklist mode (run all checks except listed). They're mutually exclusive.

Rule ID catalog

Bandit rules are organized by category prefix:

PrefixCategoryExamples
B1xxMiscellaneousB101 assert used, B102 exec used, B105 hardcoded password string
B2xxApplication/FrameworkB201 flask debug=True, B202 tarfile unsafe extract
B3xxBlacklists / CryptographyB301 pickle, B303 MD5, B311 random for crypto, B321 ftplib (cleartext), B324 hashlib weak hash, B403 import_pickle
B4xxImportsB401 import_telnetlib, B404 subprocess imported, B405 import_xml_etree, B413 import_pyCrypto
B5xx(less common - varies)
B6xxInjectionsB602 subprocess shell=True, B603 subprocess without shell=False, B608 sql_injection, B610 django extra used (sql injection-prone)
B7xxXSS / templatingB701 jinja2 autoescape false, B703 django mark_safe

Full catalog: bandit.readthedocs.io/en/latest/plugins/.

False-positive triage (MANDATORY)

Per the canonical Bandit workflow, the suppression layers:

MechanismExampleWhen to use
Per-line # nosecsubprocess.run(cmd, shell=True) # nosec B602Single-line exception with rule ID
Per-rule # nosec# nosec B404 (above import statement)Rule-specific suppression
[tool.bandit] skips = ["..."]Per-project rule disableCategorical disable (test fixtures, etc.)
[tool.bandit] exclude_dirs = ["..."]Per-directory excludeGenerated code, vendored libs

Justification template (mandatory in code):

import subprocess
# nosec B602 - Reason: command is statically defined, no user input
# Reviewer: alice@example.com (2026-05-15)
# Expires: 2026-12-15
result = subprocess.run("ls -la /tmp", shell=True, check=True)

For rules that can never apply (e.g., B311 random is fine outside crypto contexts), prefer per-rule disable in pyproject.toml over per-line suppressions - fewer comments to maintain, easier to audit at the project level.

Cadence: every quarter, grep for # nosec patterns lacking # Reason: lines; flag for review.

Output formats

bandit -r . -f txt                      # default human-readable
bandit -r . -f json -o bandit.json      # JSON for finding triage
bandit -r . -f sarif -o bandit.sarif    # SARIF for GitHub Code Scanning
bandit -r . -f xml -o bandit.xml        # JUnit XML
bandit -r . -f html -o bandit.html      # standalone HTML report
bandit -r . -f csv -o bandit.csv        # CSV
bandit -r . -f screen                    # colorized terminal

Baseline for legacy adoption

Capture current findings once, then gate only on new ones:

bandit -r . -f json -o old-findings.json      # capture
bandit -r . --baseline old-findings.json      # CI: only NEW findings fail

Pre-commit integration

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/PyCQA/bandit
    rev: 1.7.10
    hooks:
      - id: bandit
        args: ["--severity-level=medium", "--confidence-level=medium"]
        files: \.py$
        exclude: ^(tests/|venv/|.venv/)

CI integration

jobs:
  bandit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - uses: actions/setup-python@v5
        with: { python-version: '3.13' }
      - run: pip install bandit[toml]
      - run: bandit -r . -f sarif -o bandit.sarif
      - uses: github/codeql-action/upload-sarif@v3
        if: always()
        with: { sarif_file: bandit.sarif }

For PR-blocking: pipe through --severity-level high to limit noise.

Anti-patterns

Anti-patternWhy it failsFix
--severity-level=low everywhereNoise overwhelms; team disablesStart --severity-level=medium; ratchet down
# nosec without rule IDSuppresses ALL rules on that line; over-broad# nosec B602 (specific rule)
Skip --confidence-level filterLOW confidence findings mostly false positivesPair --severity-level=high --confidence-level=medium for triage
Run on tests directoryTest-only patterns (assert, pickle) trigger noiseexclude_dirs = ["tests"]
No baseline; every legacy finding blocks CITeam disables BanditUse --baseline old-findings.json against a captured baseline

Limitations

  • Python-only; for Go see gosec.md (opens in new window), for JS/TS see eslint-security.md (opens in new window).
  • Plugin-based detection misses some patterns that Semgrep custom rules can catch.
  • No native cross-file taint analysis (use codeql-queries for that).
  • Rule depth varies - well-maintained for OpenStack-era patterns; newer Python ecosystem (FastAPI, async patterns) coverage thinner.

Sources

  • bd-start (opens in new window) - install + basic scan reference
  • bandit.readthedocs.io - full documentation
  • bandit.readthedocs.io/en/latest/plugins/ - rule catalog
  • github.com/PyCQA/bandit - repository

ESLint security plugins - JS/TS language-native SAST

View source (opens in new window)

ESLint security plugins - JS/TS language-native SAST

Per-tool reference for language-native-sast. Two npm plugins add a security lint layer inside the standard ESLint pipeline, no external server required. They emit JSON or SARIF for a multi-scanner triage step.

Per github.com/eslint-community/eslint-plugin-security (opens in new window):

"This project will help identify potential security hotspots, but finds a lot of false positives which need triage by a human."

Per github.com/mozilla/eslint-plugin-no-unsanitized (opens in new window), the no-unsanitized plugin adds "basic security checks" for DOM sink operations (innerHTML, insertAdjacentHTML, document.write).

Install

npm install --save-dev eslint-plugin-security
npm install --save-dev eslint-plugin-no-unsanitized

For SARIF output, per github.com/microsoft/sarif-js-sdk (opens in new window):

npm install --save-dev @microsoft/eslint-formatter-sarif

Flat config setup (ESLint 8.23+ and ESLint 9+)

Per esp-sec (opens in new window) and esp-xss (opens in new window):

// eslint.config.js
import pluginSecurity from "eslint-plugin-security";
import nounsanitized from "eslint-plugin-no-unsanitized";

export default [
  pluginSecurity.configs.recommended,
  nounsanitized.configs.recommended,
];

For legacy .eslintrc configs, per esp-sec (opens in new window):

module.exports = {
  extends: ["plugin:security/recommended-legacy"],
};

Rule catalog

Per esp-sec (opens in new window), configs.recommended enables all 14 detect-* rules:

Rule IDDetects
security/detect-bidi-charactersUnicode bidi override characters (trojan-source attacks)
security/detect-buffer-noassertBuffer calls with the noAssert flag set
security/detect-child-processchild_process use and non-literal exec() calls
security/detect-disable-mustache-escapeTemplate engines with escaping disabled
security/detect-eval-with-expressioneval(variable) - arbitrary code execution
security/detect-new-buffernew Buffer(non-literal) - deprecated unsafe API
security/detect-no-csrf-before-method-overrideExpress middleware ordering that bypasses CSRF
security/detect-non-literal-fs-filenamefs calls with variable filenames - path traversal
security/detect-non-literal-regexpRegExp(variable) - potential ReDoS
security/detect-non-literal-requirerequire(variable) - dynamic require
security/detect-object-injectionobj[variable] property access - prototype injection
security/detect-possible-timing-attacksInsecure string comparisons (==, ===) for secrets
security/detect-pseudoRandomBytescrypto.pseudoRandomBytes and Math.random for security
security/detect-unsafe-regexReDoS-vulnerable regular expressions

Per esp-xss (opens in new window), configs.recommended enables:

Rule IDDetects
nounsanitized/methodUnsafe calls: insertAdjacentHTML, document.write, document.writeln with variable arguments
nounsanitized/propertyUnsafe assignments: element.innerHTML = variable, element.outerHTML = variable

Safe alternatives per esp-xss (opens in new window): construct DOM nodes with createElement and set textContent or classList rather than assigning raw HTML strings.

False-positive triage

security/detect-object-injection is the highest-volume false positive: any obj[key] access triggers it, including safe patterns like array indexing. Standard triage approaches:

Per-line suppression with mandatory justification:

// eslint-disable-next-line security/detect-object-injection
// Reason: key is validated against allowedKeys before this point
// Reviewer: eng@example.com (2026-06-04)
// Expires: 2026-12-04
const value = config[key];

Block-level suppression for generated or vendored code:

/* eslint-disable security/detect-object-injection */
// Reason: auto-generated lookup table; keys are compile-time constants
/* eslint-enable security/detect-object-injection */

Rule-level severity downgrade when a rule produces only noise on a specific codebase:

// eslint.config.js
export default [
  pluginSecurity.configs.recommended,
  {
    rules: {
      "security/detect-object-injection": "warn", // downgrade from error
    },
  },
];

Suppression cadence: audit all eslint-disable comments quarterly. Suppressions without Reason: + Reviewer: + Expires: are treated as unreviewed debt.

SARIF output for GitHub Code Scanning

Per sarif-sdk (opens in new window), the @microsoft/eslint-formatter-sarif package cannot be invoked with the abbreviated -f sarif form because its name is scoped. Use the full package name:

npx eslint \
  --format @microsoft/eslint-formatter-sarif \
  --output-file eslint-security.sarif \
  "src/**/*.{js,ts}"

To embed analyzed source content in the SARIF output:

SARIF_ESLINT_EMBED=true npx eslint \
  --format @microsoft/eslint-formatter-sarif \
  --output-file eslint-security.sarif \
  "src/**/*.{js,ts}"

JSON output for multi-scanner triage

Per eslint.org/docs/latest/use/command-line-interface (opens in new window), --format json produces an array of file result objects, each with a messages array containing ruleId, severity, line, column, and message. Feed eslint-security.json into a multi-scanner triage step alongside semgrep.json and other scanner outputs; normalize ruleId to CWE for deduplication across scanners.

CI integration with gating

Per eslint-cli (opens in new window), ESLint exit codes: 0 = no errors; 1 = errors found; 2 = config error. Gate CI on exit code 1:

# .github/workflows/security-lint.yml
name: Security Lint
on: [push, pull_request]

jobs:
  eslint-security:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: "22" }
      - run: npm ci
      - name: Run security lint (JSON for triager)
        run: |
          npx eslint \
            --format json \
            --output-file eslint-security.json \
            "src/**/*.{js,ts}" || true
      - name: Run security lint (SARIF for Code Scanning)
        run: |
          npx eslint \
            --format @microsoft/eslint-formatter-sarif \
            --output-file eslint-security.sarif \
            "src/**/*.{js,ts}"; exit $?
      - uses: github/codeql-action/upload-sarif@v3
        if: always()
        with:
          sarif_file: eslint-security.sarif

The JSON pass uses || true so SARIF upload still runs on failure; the SARIF pass propagates the real exit code so the job fails on errors.

Example

Triggering finding:

const userData = req.body;
element.innerHTML = userData.bio; // nounsanitized/property
const file = fs.readFileSync(req.query.path); // security/detect-non-literal-fs-filename

ESLint output (stylish):

src/profile.js
  12:3  error  Unsafe assignment to innerHTML  nounsanitized/property
  18:3  error  Found non-literal argument to readFileSync  security/detect-non-literal-fs-filename

Safe rewrites:

// XSS: use textContent for plain text; DOMPurify for rich HTML
element.textContent = userData.bio;
// or: element.setHTML(sanitize(userData.bio));

// Path traversal: validate against an allowlist
const allowed = ["/var/data/a.txt", "/var/data/b.txt"];
if (!allowed.includes(req.query.path)) throw new Error("invalid path");
const file = fs.readFileSync(req.query.path);

Limitations

  • detect-object-injection fires on all variable-keyed property accesses; expect high false-positive volume on data-heavy code. Pair with a code review step rather than blocking CI on it alone.
  • Neither plugin performs cross-file taint tracking; for taint flow across module boundaries, pair with semgrep-rules or codeql-queries.
  • eslint-plugin-security does not cover server-side template injection or SQL injection natively; use Semgrep p/owasp-top-ten for those patterns.
  • Rules run at parse time, not at runtime; dynamic injection via eval called through a proxy chain will not be caught.

Sources

gosec - Go language-native SAST

View source (opens in new window)

gosec - Go language-native SAST

Per-tool reference for language-native-sast.

Per github.com/securego/gosec (opens in new window):

gosec is the Go-specific SAST. It "performs static code analysis by scanning the Go AST and SSA code representation" and supports "taint analysis tracking data flow from user inputs to dangerous functions" per gs-gh (opens in new window). The taint analysis distinguishes gosec from regex-based linters - it tracks input flow through method chains, which catches injection patterns linters miss.

Install

Per gs-gh (opens in new window):

go install github.com/securego/gosec/v2/cmd/gosec@latest

For pinned versions in CI:

go install github.com/securego/gosec/v2/cmd/gosec@v2.20.0

Docker:

docker pull securego/gosec
docker run --rm -v "$PWD:/code" securego/gosec ./code/...

Basic recursive scan

Per gs-gh (opens in new window):

gosec ./...

Common variations:

gosec -severity=high ./...           # only HIGH severity
gosec -confidence=high ./...         # only HIGH confidence
gosec -exclude=G104 ./...            # skip "unhandled errors" rule
gosec -include=G101,G102 ./...       # only run specific rules

Rule ID catalog

Per gs-gh (opens in new window) the common rule IDs; the authoritative current list is emitted at runtime by gosec -list-rules:

RuleDescription
G101Hardcoded credentials
G102Bind to all interfaces (0.0.0.0)
G103Audit unsafe block (use of unsafe package)
G104Unhandled errors
G106SSH InsecureIgnoreHostKey
G107URL with potential SSRF
G201SQL query construction by string concat
G202SQL query construction by string format
G204Subprocess launched with variable
G301Poor file permissions on directory
G302Poor file permissions on file
G303Predictable temp-file name
G304File path traversal vulnerabilities
G305File traversal in tar archive
G401Weak cryptographic algorithms
G402TLS InsecureSkipVerify
G403RSA key length too short
G404Insecure random number generation
G501-G505Insecure crypto primitives (DES, MD5, RC4, SHA1)
G601Implicit memory aliasing in for-range
G602Slice bounds out of range

Prefix families at a glance: G1xx credential / injection / unsafe surface, G2xx SQL and subprocess construction, G3xx file and path handling, G4xx crypto and TLS misuse, G5xx insecure crypto primitives, G6xx Go memory and slice hazards.

Output formats

Per gs-gh (opens in new window):

gosec -fmt sarif -out results.sarif ./...
gosec -fmt json -out results.json ./...
gosec -fmt junit-xml -out results.xml ./...
gosec -fmt html -out results.html ./...
gosec -fmt text -out results.txt ./...
gosec -fmt yaml -out results.yaml ./...

For multi-scanner triage integration, use JSON.

False-positive triage (MANDATORY)

Per gs-gh (opens in new window) the canonical inline suppression syntax:

// #nosec G404 -- justification text

Format: #nosec [RuleList] [-- Justification].

MechanismExampleWhen to use
Per-line #nosec// #nosec G101 -- test fixture; not deployed to prodSingle-line exception with justification
Per-rule list #nosec// #nosec G104,G115 -- intentional in this fast-pathMulti-rule single-line
-exclude= flaggosec -exclude=G104 ./...Project-wide rule disable (CI flag)
-confidence= filtergosec -confidence=high ./...Triage workflow: only high-confidence first

Justification template (mandatory in code):

// #nosec G401 -- Reason: legacy MD5 required for vendor-mandated checksum format
// Reviewer: alice@example.com (2026-05-15)
// Expires: 2026-12-15
hash := md5.Sum(data)

Cadence: every quarter, grep for #nosec patterns lacking -- Reason: and flag for review.

golangci-lint integration

Most Go teams run gosec via golangci-lint (the universal linter runner) rather than directly:

# .golangci.yml
linters:
  enable:
    - gosec

linters-settings:
  gosec:
    excludes:
      - G104                     # unhandled errors (often noise)
    severity: medium
    confidence: medium
    config:
      G306: "0644"               # default file perm threshold
golangci-lint run ./...

This is the recommended pattern - golangci-lint handles parallelism, caching, and unified output across multiple linters.

CI integration

Standalone:

jobs:
  gosec:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - uses: actions/setup-go@v5
        with: { go-version: '1.22' }
      - uses: securego/gosec@master
        with:
          args: -fmt sarif -out gosec.sarif ./...
      - uses: github/codeql-action/upload-sarif@v3
        if: always()
        with: { sarif_file: gosec.sarif }

Via golangci-lint (preferred):

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - uses: actions/setup-go@v5
        with: { go-version: '1.22' }
      - uses: golangci/golangci-lint-action@v6
        with:
          version: latest

Custom rules

Unlike Semgrep / CodeQL, gosec doesn't have a custom-rule DSL - adding new rules requires writing Go code in gosec/rules/ and contributing upstream OR forking. For most teams, leverage the 40+ built-in rules + suppressions.

Worked example

A Go microservice needs to clear a security review. Run gosec -severity=high -confidence=high ./...; gosec reports a G401 on a md5.Sum(data) call and a G104 on an unchecked w.Write return.

  • G401 is a false positive here - the MD5 is a vendor-mandated checksum, not a security hash. Suppress it with an audited justification:

    // #nosec G401 -- Reason: legacy MD5 required for vendor-mandated checksum format
    hash := md5.Sum(data)
    
  • G104 is a real bug - the unhandled w.Write error is fixed by checking its return value, not suppressed.

Re-run gosec -fmt sarif -out gosec.sarif ./...; the SARIF now reports zero HIGH findings, and the golangci-lint gate passes in CI.

Anti-patterns

Anti-patternWhy it failsFix
#nosec without rule IDSuppresses ALL rules on that line// #nosec G401 (specific)
#nosec without -- JustificationNo audit trailRequired template
Skip -confidence= filterLOW confidence drowns the team-confidence=high for triage
Run gosec separately from golangci-lintTwo linters with different configgolangci-lint integration
Exclude G104 globallyLoses entire "unhandled errors" coveragePer-call // #nosec G104 -- intentional

Limitations

  • Go-only; for other languages see the sibling references.
  • Custom-rule authoring requires Go programming + upstream PR (vs YAML rule authoring in Semgrep).
  • Some patterns (cross-package taint flow) miss what codeql-queries catches.
  • Rule depth varies - newer Go patterns (generics, structured concurrency) coverage thinner.

Sources

  • gs-gh (opens in new window) - repository, install, rule list, suppression syntax
  • gosec subcommand gosec -list-rules - current rule catalog
  • golangci-lint.run - golangci-lint integration
  • securego/gosec GitHub Action - github.com/securego/gosec

PMD Apex security ruleset - Salesforce language-native SAST

View source (opens in new window)

PMD Apex security ruleset - Salesforce language-native SAST

Per-tool reference for language-native-sast.

Per pmd.github.io - Apex Security Rules (opens in new window), PMD ships a built-in category/apex/security.xml ruleset that covers 10 security rules for Salesforce Apex. All 10 rules are present since PMD 5.5.3 and carry Medium (3) priority by default. The ruleset addresses the Salesforce-specific threat model: SOQL injection, object/field-level security bypass, sharing-model evasion, hard-coded credentials, insecure endpoints, XSS through Visualforce, and open redirect.

Install

Per pmd.github.io - Installation (opens in new window), PMD requires Java 8 or later. Download the zip from the GitHub releases page (opens in new window), unzip, and add bin/ to PATH:

# Linux / macOS
unzip pmd-dist-*.zip -d ~/pmd
export PATH="$HOME/pmd/bin:$PATH"

# Verify
pmd --version

For CI, prefer the Docker image or the Maven/Gradle plugin to avoid zip management. The Docker image is available at ghcr.io/pmd/pmd.

First scan with the built-in security ruleset

Per pmd.github.io - CLI Reference (opens in new window):

pmd check -d . -R category/apex/security.xml -f sarif -r pmd-apex.sarif

Flag reference (per pmd-cli (opens in new window)):

FlagMeaning
-d <path>Source directory or file to analyze
-R <refs>Ruleset path; comma-separated for multiple
-f <format>Output format (sarif, text, xml, json, html; default: text)
-r <file>Write report to file instead of stdout
--minimum-priority <n>Skip rules below priority n (1=High, 5=Info)
--cache <file>Enable incremental analysis (per pmd-cache (opens in new window))

Exit codes (per pmd-cli (opens in new window)):

CodeMeaning
0Success, no violations
1Unhandled exception
2Invalid arguments
4Violations detected
5Recoverable parsing errors

The 10 Apex security rules

Per pmd-apex-sec (opens in new window):

RuleWhat it detects
ApexSOQLInjectionDynamic SOQL/DML built by string concatenation with untrusted input
ApexCRUDViolationMissing object/field permission check before SOQL, SOSL, or DML
ApexSharingViolationsClasses performing DML without an explicit sharing keyword
ApexBadCryptoHard-coded IVs or keys in cryptographic operations
ApexDangerousMethodsCalls to Configuration.disableTriggerCRUDSecurity() or sensitive System.debug()
ApexInsecureEndpointPlain HTTP (non-HTTPS) callout endpoints
ApexOpenRedirectRedirects using unsanitized user-controlled input
ApexSuggestUsingNamedCredHard-coded credentials in HTTP headers; suggests Named Credentials
ApexXSSFromEscapeFalseaddError() called with escape disabled, exposing raw user content
ApexXSSFromURLParamURL parameters used in output contexts without escaping

ApexSOQLInjection

Per pmd-apex-sec (opens in new window): "Detects the usage of untrusted / unescaped variables in DML queries."

Non-compliant:

public class Foo {
    public void test1(String t1) {
        Database.query('SELECT Id FROM Account' + t1);
    }
}

Compliant (bind variable - automatically sanitized by the Apex runtime):

public class Foo {
    public void test1(String accountName) {
        List<Account> accounts = [SELECT Id FROM Account WHERE Name = :accountName];
    }
}

ApexCRUDViolation

Per pmd-apex-sec (opens in new window): "The rule validates you are checking for access permissions before a SOQL/SOSL/DML operation." Accepted remediation paths include DescribeSObjectResult system checks, WITH SECURITY_ENFORCED, or (since Winter '23 / API v56) WITH USER_MODE.

The rule is configurable for custom authorization facades via regex properties (createAuthMethodPattern, readAuthMethodPattern, etc.) so teams using an internal ESAPI wrapper can still pass the check.

ApexSharingViolations

Per pmd-apex-sec (opens in new window): "Detect classes declared without explicit sharing mode if DML methods are used." The three accepted keywords are with sharing, without sharing, and inherited sharing. The intent is to force a conscious declaration of sharing posture, not to mandate a specific value.

Custom ruleset (subset or extended)

Per pmd.github.io - Making Rulesets (opens in new window):

Use a custom XML ruleset to select a subset, override priorities, or add exclusion patterns for generated code:

<?xml version="1.0"?>
<ruleset name="Apex Security - Regulated"
    xmlns="http://pmd.sourceforge.net/ruleset/2.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://pmd.sourceforge.net/ruleset/2.0.0
    https://pmd.sourceforge.io/ruleset_2_0_0.xsd">
  <description>Apex security rules for regulated-industry Apex</description>

  <!-- Include the full security category -->
  <rule ref="category/apex/security.xml">
    <!-- Suppress for auto-generated WSDL stubs -->
    <exclude name="ApexSuggestUsingNamedCred"/>
  </rule>

  <!-- Exclude generated code directories -->
  <exclude-pattern>.*/generated/.*</exclude-pattern>
</ruleset>

Run with the custom ruleset:

pmd check -d force-app/main/default/classes \
          -R config/pmd-apex-regulated.xml \
          -f sarif \
          -r pmd-apex.sarif

Per pmd-rulesets (opens in new window), referencing an entire category means the ruleset automatically picks up new rules added to that category in future PMD versions. Pin specific versions in CI to avoid unexpected gate changes.

Incremental analysis for faster CI

Per pmd-cache (opens in new window) (PMD 5.6.0+):

pmd check -d force-app/main/default/classes \
          -R category/apex/security.xml \
          -f sarif \
          -r pmd-apex.sarif \
          --cache .pmd-cache/apex.cache

The cache stores file checksums. Unchanged files reuse cached results; only modified files are re-analyzed. The generated report is identical to a full run (per pmd-cache (opens in new window)). Cache is invalidated automatically on PMD version change, ruleset modification, or auxclasspath change.

CI gate (GitHub Actions)

jobs:
  pmd-apex:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Download PMD
        run: |
          PMD_VERSION=7.7.0
          curl -Lo pmd.zip \
            https://github.com/pmd/pmd/releases/download/pmd_releases%2F${PMD_VERSION}/pmd-dist-${PMD_VERSION}-bin.zip
          unzip -q pmd.zip -d pmd-dist
          echo "$PWD/pmd-dist/pmd-bin-${PMD_VERSION}/bin" >> $GITHUB_PATH

      - name: Run PMD Apex security scan
        run: |
          pmd check \
            -d force-app/main/default/classes \
            -R category/apex/security.xml \
            -f sarif \
            -r pmd-apex.sarif \
            --cache .pmd-cache/apex.cache
        # Exit code 4 = violations found (per pmd-cli); gate blocks on non-zero
        continue-on-error: false

      - name: Upload SARIF to GitHub Code Scanning
        uses: github/codeql-action/upload-sarif@v3
        if: always()
        with:
          sarif_file: pmd-apex.sarif
          category: pmd-apex

Per pmd-cli (opens in new window), exit code 4 means violations detected. continue-on-error: false blocks the PR on any finding. Use --minimum-priority 2 to gate only on High and Critical findings while still uploading all findings via SARIF.

Suppression

PMD suppression in Apex (per pmd-apex-sec (opens in new window)): annotate the method or class with @SuppressWarnings:

@SuppressWarnings('PMD.ApexCRUDViolation')
public class VisualforceGetter {
    // Visualforce getters auto-enforce FLS; CRUD check is redundant here
    public List<Account> getAccounts() {
        return [SELECT Id, Name FROM Account];
    }
}

Add a comment explaining the justification. Suppressions without rationale are flagged in code review.

Anti-patterns

Anti-patternWhy it failsFix
Running against all directories including classes/testTest classes generate false positives for CRUD/sharingExclude test directories with <exclude-pattern>
No --cache in CI on large orgsFull re-scan of 500+ classes on every commit is slowAdd --cache .pmd-cache/apex.cache
Suppressing ApexSOQLInjection globallyMasks real injection risksSuppress per method with a written justification only
Floating latest PMD version in CIGate breaks when a new rule fires unexpectedlyPin the PMD_VERSION variable
Custom auth facade without *AuthMethodPattern configAll CRUD-checked methods still flagged as violationsConfigure createAuthMethodPattern etc. per pmd-apex-sec (opens in new window)

Limitations

  • PMD Apex analysis is syntactic, not data-flow-based; it can miss taint paths that cross method boundaries. For deep interprocedural analysis pair with codeql-queries.
  • ApexCRUDViolation generates false positives on Visualforce getter methods where FLS is enforced automatically; suppress with justification.
  • PMD does not parse Lightning Web Components (.js); client-side XSS is out of scope. Use semgrep-rules with the p/owasp-top-ten ruleset for LWC JavaScript.

Sources

Related skills

codeql-queries

Configures and runs GitHub CodeQL - semantic-database SAST with queries written in the CodeQL declarative query language; supports `codeql database create` (per-language) + `codeql database analyze` with --format=sarif; ships query packs (`codeql/javascript-queries`, `codeql/python-queries`, `codeql/java-queries`, `codeql/go-queries`, etc.); integrates with GitHub Code Scanning via SARIF upload; suppression via inline comment + sarif-filter + Security-tab dismissal. Use when the team uses GitHub-hosted repos and needs deep semantic SAST beyond pattern matching (cross-file taint flows, dataflow analysis).

cve-exploitability-triage

Ranks known CVE findings by real-world exploitability instead of severity alone: enriches each CVE with its EPSS probability (the chance exploitation activity is observed in the next 30 days) and CISA KEV membership (confirmed exploited in the wild), applies OpenVEX status assertions to set aside vulnerabilities the product is not affected by, applies a reachability heuristic for vulnerable code that is never called, and assigns every finding to one of four buckets (Fix-Now, Fix-This-Sprint, Fix-Backlog, Accept-Risk) using documented EPSS thresholds. Treats a CISA KEV listing as non-waivable under any justification. Use when a dependency, container image, or SBOM vulnerability scan has produced more CVEs than the team can fix in the available window and someone has to decide which ones get fixed first and which can wait.

dependabot-config

Reference for `.github/dependabot.yml` - GitHub-native dependency-update orchestrator. Required keys (`version: 2`, `updates[]` array) plus per-update fields (`package-ecosystem`, `directory` / `directories`, `schedule.interval`); common optional fields (`ignore`, `groups`, `allow`, `labels`, `milestone`, `open-pull-requests-limit`, `target-branch`, `vendor`, `versioning-strategy`, `assignees`, `commit-message`); auto-rebase + grouped-PR + security-only updates. Use when authoring or reviewing Dependabot configs in GitHub-hosted repos.

gitleaks-scanning

Configures and runs gitleaks - Go-based secret scanner with `gitleaks git` (scan local git via `git log -p`), `gitleaks dir` (filesystem), `gitleaks stdin` (pipe); 100+ built-in rules + custom rules in `.gitleaks.toml` ([[rules]] with regex / entropy / keywords / tags); allowlist via [[rules.allowlists]] (commits / paths / stopwords); pre-commit hook + GitHub Action integration; plus baseline management for legacy debt - onboarding a repo with historical findings via `--baseline-path` snapshots, `.gitleaksignore`, cross-tool suppression consistency with TruffleHog, and rot-prevention cadence. Use when the team needs OSS secret scanning at commit time + CI gate, or is adopting scanning on a repo with pre-existing findings.

multi-tool-finding-triage

Merges two or more security scanner reports into one gate. Use when you need a single BLOCK or PASS decision from multiple scanners instead of reading N separate reports. Normalizes each report into one common finding format (a canonical `Finding`), deduplicates on a per-domain key while recording which scanners agree (`caught_by` consensus), validates a waiver (finding-suppression) file, rejecting any missing `expires:` / `approved_by:` / `reason:` or expired, enriches CVE findings with EPSS (exploit-probability) and CISA KEV (known-exploited catalog), then applies a `fail_on` severity threshold to emit BLOCK or PASS plus a bucketed pull-request comment. Works across static (SAST), dynamic (DAST), secret, dependency (SCA), container, and IaC scanners. To run a single scanner instead use semgrep-rules, codeql-queries, or one of the language-native-sast linters; this runs after them to merge output - the cross-scanner gate, not a single-scanner wrapper.

npm-pip-maven-audit

Configures and runs native package-manager audit commands across ecosystems - `npm audit --audit-level=high` (npm), `yarn npm audit` (Yarn 2+), `pnpm audit` (pnpm), `pip-audit` (Python via PyPA), `mvn dependency:check` (Maven via OWASP Dependency-Check plugin), `cargo audit` (Rust, with `.cargo/audit.toml` suppression, `--deny` semantics, SARIF, binary auditing, and the rustsec/audit-check Action as a reference), and `bundle audit` (Ruby Bundler, with `.bundler-audit.yml` waivers, Rake integration, and CI gating as a reference); fastest no-install-required SCA option. Use when the team wants fast, no-extra-tooling SCA in CI as a first line of defense, when a Rust or Ruby repo needs its ecosystem-native scanner, or pairs with snyk/osv-scanner for layered coverage.

nuclei-dast

Installs and runs ProjectDiscovery Nuclei template-based HTTP scanning: selects templates via `-t {path}` and `-tags`/`-severity` filters, controls request rate with `-rl`, emits JSONL output via `-j` for cross-tool finding aggregation, authors custom YAML matchers for app-specific checks, and gates CI on severity thresholds. Use when the team runs Nuclei alongside ZAP for template-driven DAST coverage, needs fuzzing-style probes beyond ZAP passive scan, or wants to operationalize community CVE templates in a pipeline.

osv-scanner

Configures and runs Google OSV-Scanner - open-source SCA against the OSV.dev vulnerability database; supports `osv-scanner scan -r ./` recursive scan + per-lockfile scan via `-L package-lock.json`; SBOM input (CycloneDX / SPDX) for non-standard package managers; `--format json|sarif|markdown|vertical|html` output; suppressions via `osv-scanner.toml` config. Use when the team needs OSS-native SCA without commercial-license overhead, or wants a second-opinion DB pair with Snyk's commercial DB.

reachability-analyzer

Runs dead-dependency analysis across JS, Python, and Rust projects using ecosystem-native static tools (`depcheck`/`knip` for JS, `vulture` for Python, `cargo-machete` for Rust), then cross-references the unused-dependency list against SCA findings to downrank vulns in code that is never loaded. Use when SCA output (from `osv-scanner`, `snyk-test`, or `npm-pip-maven-audit`) is too noisy to triage and the team needs to separate unreachable CVEs from exploitable ones before sprint planning; sibling cve-exploitability-triage ranks by EPSS/KEV exploitation signal, not code reachability.

renovate-config

Reference for `renovate.json` - Mend Renovate dependency-update orchestrator (multi-platform: GitHub / GitLab / Bitbucket / Azure DevOps / Gitea); top-level keys (`extends` for preset references, `schedule`, `prConcurrentLimit`, `vulnerabilityAlerts`); `packageRules[]` array with `matchPackageNames` / `matchUpdateTypes` / `automerge` matching; `ignoreDeps`, `addLabels`, `automergeSchedule`. Use when authoring or reviewing Renovate configs in any repo platform Renovate supports.

sbom-formats

Reference for the two SBOM specification families and how to choose between them - CycloneDX v1.6 (OWASP-curated, security-focused: components, services, dependencies, first-class vulnerabilities[] with embedded VEX, formulation, ML/SaaS BOMs; XML / JSON / Protobuf) as the primary format, with SPDX 2.3 + 3.0 (Linux Foundation, license-focused: packages, relationships, license expressions, Tag-Value/JSON encodings, ISO/IEC 5962:2021) covered as a reference. Includes per-language generators, schema validation, sign + attest CI wiring, and the format-choice guidance (CycloneDX for security-focused consumers; SPDX for US Federal procurement, Linux Foundation, and license-compliance contexts). Use when the user asks to write or validate an SBOM in CycloneDX or SPDX form, or the team must pick its SBOM format.

secrets-rotation-runner

Build-an-X for the secret-rotation workflow after detection - detect via gitleaks/trufflehog/kingfisher → identify provider via verifier → rotate via provider API (AWS IAM / GitHub PAT / Stripe / GCP / Azure / Twilio / Slack / etc.) → invalidate old secret → audit log via observability stack → post-mortem cross-ref. Use when a secret is detected in code (or proactively for periodic rotation) - assume git-history scrub does NOT prevent compromise.

semgrep-rules

Configures and runs Semgrep - pattern-based SAST across 30+ languages with the Semgrep Registry rulesets (`p/owasp-top-ten`, `p/default`, `auto`) plus custom YAML rules; integrates `semgrep ci` for PR-blocking gates with `--baseline-commit` diff-aware scanning, per-finding inline `nosemgrep` suppressions, `--exclude` / `--include` path filters, output formats (`--json` / `--sarif` / `--gitlab-sast` / `--junit-xml`), and severity filter (INFO/WARNING/ERROR). Use when the user runs Semgrep, asks about pattern rules, or needs a low-friction SAST gate without semantic-DB setup.

snyk-test

Configures and runs Snyk, a commercial multi-mode scanner: snyk test for SCA (dependency scanning), snyk code test for SAST (code security scanning), snyk container test for container images, snyk iac test for IaC (infrastructure-as-code), snyk monitor for continuous new-vuln alerts; policy file .snyk for ignore + patch. Use when the team has a Snyk license and needs SCA (dependency scanning) or continuous vuln monitoring; for open-source scanning without a Snyk license, prefer osv-scanner.

sonarqube-rules

Configures and runs SonarQube / SonarCloud - multi-language SAST + Quality Gate platform with built-in Sonar Way rule profiles + custom rule plugins; integrates `sonar-scanner` with `sonar-project.properties` config; supports Quality Gate definitions including new-code-period blocking, branch + PR analysis, and per-issue suppression via `// NOSONAR` comment or `@SuppressWarnings("squid:RULE_ID")` annotation. Use when the user runs SonarQube Community / Developer / Enterprise edition or SonarCloud, or needs a multi-language SAST + code-quality platform with persistent issue tracking.

syft-generation

Generates, scans, and diffs Software Bills of Materials (SBOMs) with the Anchore stack - Syft generation from container images / directories / archives across OCI / Docker / Singularity formats (output CycloneDX-JSON / SPDX-JSON / Syft-JSON / table / GitHub-JSON, cosign attestation); the paired generate + scan workflow with Grype (`grype sbom:./sbom.json`, `--fail-on high`, `--only-fixed`, `.grype.yaml` ignore rules with mandatory `expires:`, EPSS/KEV prioritization); and SBOM-to-SBOM diffing via `cyclonedx diff --component-versions` to gate CI on net-new components and detect supply-chain drift between builds. Use when the team needs SBOM artifacts for compliance (US EO 14028, EU CRA, FDA medical-device guidance), SBOM-driven vulnerability scanning, or dependency-drift detection between releases.

trivy-image

Configures and runs Trivy for container image scanning: Aqua Security's all-in-one scanner combining vuln + secret + misconfiguration + license detection in one pass; `trivy image {image}` with --severity HIGH,CRITICAL filter; --format sarif/json (incl. scan-embedded CycloneDX; for standalone SBOM generation see syft-generation + sbom-formats); .trivyignore CVE suppression file; --ignore-unfixed for actionable filter; --scanners vuln/misconfig/license/secret toggle. Use when the team wants a single tool covering container image security across multiple dimensions, not for producing a standalone CycloneDX SBOM.

trufflehog-scanning

Configures and runs TruffleHog v3 - secret scanner with **live verification** (validates discovered secrets against provider APIs to confirm actual exposure vs entropy false positive); supports per-source subcommands (`git`, `github`, `gitlab`, `filesystem`, `s3`, `docker`, `gcs`, `postman`); `--results=verified` filter for high-precision output; `--exclude-detectors=TYPE` for noise reduction; exits 183 on findings via `--fail`. Use when the team needs verified secret findings (low false-positive rate) or scans across cloud + repo + container surfaces.

vex-author

Authors and validates OpenVEX documents - produces `not_affected`, `affected`, `fixed`, and `under_investigation` statements with justification codes using `vexctl create`; attaches VEX assertions to container images; outputs `.openvex.json` files consumed on a downstream VEX-filter / vulnerability-prioritization path. Use when a scanner flags a CVE that analysis confirms is not exploitable in your deployment, and a machine-readable `not_affected` assertion is needed to suppress false positives without discarding the finding from the audit trail.

zap-baseline

Configures and runs OWASP ZAP baseline scanning: `zap-baseline.py` Docker-packaged spider + passive scan suitable for CI gating; supports `-t target_url` + `-r html_report` + `-c config_file` rule customization (INFO/IGNORE/FAIL warnings) and Ajax spider via `-j` for JS-heavy SPAs; `zap-full-scan.py` active companion for staging. Covers authenticated scans end to end as a reference - ZAP Context, auth methods (form/JSON/script/browser), session management, verification strategy, OAuth/bearer injection, context XML export for `-n` - plus DAST cadence planning (PR-blocking passive baseline, nightly ZAP full + nuclei active layer, baseline-finding ratchet for legacy apps). Use when the user runs OWASP ZAP for pre-prod web app DAST, needs coverage of routes behind a login wall, or is designing a team's DAST rollout cadence.