eslint-security-rules
Configures and runs `eslint-plugin-security` (14 detect-* rules covering injection, path traversal, ReDoS, unsafe buffers, and bidi trojan-source) plus `eslint-plugin-no-unsanitized` (DOM XSS via `innerHTML`, `outerHTML`, `document.write`, `insertAdjacentHTML`) as the JS/TS first-party SAST layer; covers flat config setup, per-rule suppression with justification templates, SARIF output via `@microsoft/eslint-formatter-sarif` for GitHub Code Scanning upload, and CI gating on ESLint exit code 1. Use when the project is JS or TS and needs an in-process security lint pass without a separate SAST server.
Install with skills.sh (any agent)
npx skills add testland/qa --skill eslint-security-ruleseslint-security-rules
Overview
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).
When to use
Step 1 - Install
Per esp-sec (opens in new window):
npm install --save-dev eslint-plugin-securityPer esp-xss (opens in new window):
npm install --save-dev eslint-plugin-no-unsanitizedFor SARIF output, per github.com/microsoft/sarif-js-sdk (opens in new window):
npm install --save-dev @microsoft/eslint-formatter-sarifStep 2 - 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,
];pluginSecurity.configs.recommended enables all 14 detect-* rules. nounsanitized.configs.recommended enables nounsanitized/method and nounsanitized/property.
For legacy .eslintrc configs, per esp-sec (opens in new window):
module.exports = {
extends: ["plugin:security/recommended-legacy"],
};Step 3 - Rule catalog
pluginSecurity.configs.recommended enables all 14 detect-* rules (injection, path traversal, ReDoS, unsafe buffers, bidi trojan-source); nounsanitized.configs.recommended enables nounsanitized/method and nounsanitized/property for DOM-sink XSS. The full per-rule tables and the safe DOM alternatives are in references/eslint-security-reference.md.
Step 4 - 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.
Step 5 - 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}"Upload to GitHub Code Scanning:
- uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: eslint-security.sarifStep 6 - CI integration with gating
Per eslint.org/docs/latest/use/command-line-interface (opens in new window):
ESLint exit codes: 0 = no errors; 1 = errors found; 2 = config error. Gate CI on exit code 1. The complete GitHub Actions workflow (JSON pass for the triager, SARIF pass that propagates the exit code, SARIF upload) is in references/eslint-security-reference.md.
Step 7 - JSON output for multi-scanner triage
Per eslint-cli (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.
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-filenameESLint 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-filenameSafe 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
References
eslint security rule catalog and CI workflow
View source (opens in new window)eslint security rule catalog and CI workflow
Full rule catalog and the complete GitHub Actions workflow for eslint-security-rules. The SKILL.md spine keeps setup, triage, and the SARIF/JSON commands; this file holds the exhaustive tables and workflow.
eslint-plugin-security rules
Per esp-sec (opens in new window), configs.recommended enables all 14 detect-* rules:
| Rule ID | Detects |
|---|---|
security/detect-bidi-characters | Unicode bidi override characters (trojan-source attacks) |
security/detect-buffer-noassert | Buffer calls with the noAssert flag set |
security/detect-child-process | child_process use and non-literal exec() calls |
security/detect-disable-mustache-escape | Template engines with escaping disabled |
security/detect-eval-with-expression | eval(variable) - arbitrary code execution |
security/detect-new-buffer | new Buffer(non-literal) - deprecated unsafe API |
security/detect-no-csrf-before-method-override | Express middleware ordering that bypasses CSRF |
security/detect-non-literal-fs-filename | fs calls with variable filenames - path traversal |
security/detect-non-literal-regexp | RegExp(variable) - potential ReDoS |
security/detect-non-literal-require | require(variable) - dynamic require |
security/detect-object-injection | obj[variable] property access - prototype injection |
security/detect-possible-timing-attacks | Insecure string comparisons (==, ===) for secrets |
security/detect-pseudoRandomBytes | crypto.pseudoRandomBytes and Math.random for security |
security/detect-unsafe-regex | ReDoS-vulnerable regular expressions |
eslint-plugin-no-unsanitized rules
Per esp-xss (opens in new window), configs.recommended enables nounsanitized/method and nounsanitized/property:
| Rule ID | Detects |
|---|---|
nounsanitized/method | Unsafe calls: insertAdjacentHTML, document.write, document.writeln with variable arguments |
nounsanitized/property | Unsafe 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.
CI workflow (GitHub Actions)
# .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.sarifThe 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.
Related skills
bandit-python
Configures and runs Bandit - Python-only SAST covering 60+ rule IDs across 7 categories (B1xx-B7xx: misc, app, crypto, imports, injections, XSS); `bandit -r .` scan, `--severity-level` + `--confidence-level` filtering, `# nosec`/`# nosec B404` per-line and per-rule suppression, `pyproject.toml [tool.bandit]` config. Use for a focused, low-overhead Python SAST in pre-commit / CI. Python-only: for Go use gosec-go, for cross-language pattern SAST use semgrep-rules; to merge Bandit findings with other scanners into one gate use multi-tool-finding-triage - not this for non-Python code.
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).
gosec-go
Configures and runs gosec - Go-only SAST covering 40+ rule IDs (G101 hardcoded creds, G104 unhandled errors, G304 path traversal, G401 weak crypto, G601 memory aliasing) via Go AST + SSA taint tracking; `gosec ./...` scan, `#nosec G404 -- justification` suppression, `--fmt sarif|json|junit-xml|html`, golangci-lint integration. Use for a focused Go SAST wired into golangci-lint / CI. Go-only: for Python use bandit-python, for cross-language pattern SAST use semgrep-rules; to merge gosec findings with other scanners into one gate use multi-tool-finding-triage - not this for non-Go code.
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, bandit-python, or gosec-go; this runs after them to merge output - the cross-scanner gate, not a single-scanner wrapper.
pmd-apex-rules
Runs PMD's built-in Apex security ruleset (`category/apex/security.xml`) against Salesforce Apex source to detect injection, privilege-escalation, cryptographic, and XSS vulnerabilities; configures custom rulesets for regulated-industry Apex codebases; emits SARIF for GitHub Code Scanning upload; integrates `pmd check` as a PR-blocking CI gate. Use when the codebase contains Salesforce Apex and the team needs SAST coverage for ApexSOQLInjection, ApexCRUDViolation, ApexSharingViolations, or the full 10-rule security category.
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.
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.