Testland
Browse all skills & agents

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-rules
View source

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

  • The repo is JS or TS and has an existing ESLint config.
  • The team wants shift-left security feedback in the editor and on every commit, not only in a scheduled CI scan.
  • The project uses DOM APIs (innerHTML, insertAdjacentHTML, document.write) that need sink-level XSS coverage.
  • The team needs SARIF output for GitHub Code Scanning without adopting a new scanner binary.

Step 1 - Install

Per esp-sec (opens in new window):

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

Per esp-xss (opens in new window):

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

Step 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.sarif

Step 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-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.

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

eslint-plugin-no-unsanitized rules

Per esp-xss (opens in new window), configs.recommended enables nounsanitized/method and nounsanitized/property:

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.

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

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.