CtrlK
BlogDocsLog inGet started
Tessl Logo

testland/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.

72

Quality

91%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

Low

Low-risk findings worth noting

Overview
Quality
Evals
Security
Files

SKILL.md

name:
language-native-sast
description:
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.

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 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-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: 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

SKILL.md

tile.json