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.
72
91%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Low
Low-risk findings worth noting
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.
| Language | Tool | Reference |
|---|---|---|
| Python | Bandit (PyCQA) | references/bandit.md |
| Go | gosec (securego), via golangci-lint | references/gosec.md |
| JS / TS | eslint-plugin-security + eslint-plugin-no-unsanitized | references/eslint-security.md |
| Salesforce Apex | PMD category/apex/security.xml | references/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.
Each tool's exact commands live in its reference; the workflow is shared.
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.mdRun 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 2On a legacy codebase, capture the current findings once and gate only on NEW findings, so the debt is tracked without blocking every PR:
bandit -r . -f json -o old-findings.json, then
bandit -r . --baseline old-findings.json in CI.multi-tool-finding-triage implements this diff + waiver flow).Every tool has an inline suppression syntax; every suppression carries a reason, reviewer, and expiry, audited quarterly:
| Tool | Inline 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.
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.
Per github.com/eslint-community/eslint-plugin-security and github.com/mozilla/eslint-plugin-no-unsanitized:
// 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-filenamereports 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:
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-pattern | Why it fails | Fix |
|---|---|---|
| Unfiltered first scan on a legacy repo | Noise overwhelms; team disables the linter | Severity/confidence filters (Step 2) + baseline (Step 3) |
| Bare suppression without rule ID | Suppresses ALL rules on that line | Always name the rule (Step 4) |
| Suppression without reason + expiry | Permanent unreviewed debt | Justification template, quarterly audit (Step 4) |
| Running the linter only in scheduled CI | Loses the in-editor shift-left benefit | Dev-dependency install + pre-commit (Step 1) |
| Treating linter pass as full SAST coverage | Single-file, mostly syntactic analysis | Layer semgrep-rules / codeql-queries |
codeql-queries for interprocedural flows.semgrep-rules for LWC JavaScript).detect-object-injection, G104, B101, ApexCRUDViolation on
Visualforce getters) - the per-tool references name them and the
sanctioned handling.#nosec syntaxsemgrep-rules, codeql-queries, sonarqube-rules - deeper /
cross-language SAST siblingsmulti-tool-finding-triage - cross-scanner dedupe + waiver gate