Linting Code¶
Linting analyzes source code for stylistic issues, bugs, and suspicious patterns without running it. While linters started as style and quality tools, modern linters and their security plugins catch a meaningful share of security problems — making linting a fast, low-friction first line of defense that fits naturally in pre-commit and CI.
Why linting matters for security¶
- Consistency reduces risk — consistent, readable code is easier to review and less likely to hide bugs in unexpected places.
- Security rules catch real flaws — many linters detect dangerous functions, injection-prone patterns, weak crypto usage, insecure randomness, and hardcoded values.
- Fast feedback — linters run in seconds on a developer's machine, so they belong at pre-commit and on every pull request without meaningfully slowing the pipeline.
- Bridge to SAST — lightweight, rule-based scanners like Semgrep blur the line between linting and SAST, giving security depth at lint speed.
Where to run it¶
Run linting at every layer for maximum coverage without introducing bottlenecks:
- IDE — real-time feedback as the developer types. This is the lowest-friction catch — the developer sees the issue at the moment of creation. IDE plugins for ESLint, Bandit, Semgrep, and language servers can highlight problems inline.
- Pre-commit — block obvious issues before they enter history. Scope checks to changed files only (using
lint-stagedor Lefthook'sglobfiltering) to keep execution fast. See Pre-commit. - CI — authoritative enforcement that cannot be bypassed. Fail the build on new violations. Run the full rule set here, not just the fast subset used at pre-commit.
Scope to lint¶
Application code¶
Enable security rule sets in language-specific linters. Do not run only style rules — security rules are where the risk is:
| Language | Linter | Security rules |
|---|---|---|
| Python | Bandit, Semgrep, Ruff | B-prefix rules (Bandit); S rules (Ruff, flake8-bandit port); p/python rule set (Semgrep) |
| JavaScript / TypeScript | ESLint + eslint-plugin-security, Semgrep |
no-eval, no-new-func, XSS patterns, detect-non-literal-regexp |
| Go | golangci-lint (includes gosec) | gosec rules (G1xx–G7xx, see the gosec rule list) |
| Java | SpotBugs + find-sec-bugs, Semgrep | find-sec-bugs plugin detects SQL injection, XXE, SSRF |
| Ruby | Brakeman | Full Rails-aware security analysis |
| PHP | Psalm, Semgrep | Psalm taint analysis (--taint-analysis); p/php rule set (Semgrep) |
| Kotlin / Android | Android Lint, Detekt, Semgrep, SpotBugs + find-sec-bugs | Android Lint security checks; Semgrep Kotlin/Android rule sets; find-sec-bugs works on Kotlin bytecode |
Infrastructure as code¶
Lint IaC files for misconfiguration:
- Dockerfile —
hadolintenforces best practices: nolatesttag,USERinstruction set, shell injection inRUNcommands. - Terraform —
tflintfor correctness and provider-specific errors, andTrivy(trivy config) for security misconfigurations beforeterraform apply.tfsechas been folded into Trivy and is no longer actively developed. - Kubernetes manifests —
kube-linterchecks for privilege escalation, missing resource limits, host network access. - Helm charts —
helm lint+kube-linteron rendered templates.
See IaC Scanning for deeper coverage.
Configuration and data files¶
Validate YAML/JSON/TOML in CI to prevent malformed or unsafe config from reaching deployment:
# .pre-commit-config.yaml entry
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
- id: check-yaml
- id: check-json
- id: check-toml
Practical Semgrep example¶
Semgrep lets you write custom rules in YAML to enforce project-specific security patterns:
# semgrep-rules/no-md5.yaml
rules:
- id: no-md5-for-hashing
patterns:
- pattern: hashlib.md5(...)
message: "MD5 is cryptographically broken. Use hashlib.sha256() or better."
languages: [python]
severity: ERROR
metadata:
category: security
cwe: CWE-327
Run it in CI:
Baseline management¶
New projects inherit years of existing lint violations. Blocking on all of them at once is counterproductive. Manage this with baselines:
- Semgrep baseline —
semgrep --baseline-commit <sha>reports only findings introduced after the baseline commit, not the historical backlog. - ESLint — use bulk suppressions (
eslint --suppress-all, ESLint v9.24+) to record existing violations ineslint-suppressions.json, then fail only on new ones and clear the backlog incrementally.--report-unused-disable-directiveshelps keep inline suppressions honest. - detect-secrets baseline — generate a baseline file with
detect-secrets scan > .secrets.baselineand commit it. New secrets are flagged against the baseline.
Treat clearing lint debt as technical debt: schedule it, track it, and do not let it grow by blocking new violations.
Common pitfalls¶
- Security rules disabled — style linting without security rules enabled misses the most valuable signal. Audit your
eslint.config.js(or legacy.eslintrc) /pyproject.tomlto confirm security plugins are active. - Noisy rules driving suppression — if developers add
# noqa,// eslint-disable, or// noseccomments routinely, the rules are misconfigured. Tune or replace noisy rules instead of suppressing them everywhere. - Only running in CI, not at pre-commit — slow feedback loops mean developers context-switch after forgetting what the code was doing.
- Linting only changed files in CI — fine for speed, but run full-repo linting on a schedule (weekly) to catch violations in unchanged files.
- Ignoring
lint-stagedperformance — on large repos, running linters against all files at pre-commit is slow. Scope to staged files.
Metrics¶
- Number of security-category lint violations introduced per sprint (should trend toward zero).
# noqa/// eslint-disablesuppression count per repo (rising counts indicate noise problems or avoidance).- Time from lint violation to fix (measures how actionable feedback is).
- Percentage of repos with security lint rules enabled (org-level metric).
Maturity progression¶
| Level | Practice |
|---|---|
| Starting | Style linter running in CI; security rules added to at least one language linter |
| Developing | Security linting (Semgrep or equivalent) at pre-commit and CI; IaC linting for Dockerfile and Terraform; baseline managed |
| Defined | Custom Semgrep rules for project-specific patterns; IDE plugins standard in developer tooling; violations trend tracked |
| Advanced | Org-wide shared Semgrep rule library; lint results fed into ASPM for correlation with runtime findings; suppressions reviewed in security audits |
Tools1¶
Open-source¶
- Bandit — Finds common security issues in Python code. Checks for dangerous calls (
exec,eval,pickle), weak crypto, SQL injection patterns, and more. Integrates with pre-commit and CI. - Brakeman — Static analysis specifically for Ruby on Rails. Detects SQL injection, XSS, mass assignment, and other Rails-specific security issues.
- ESLint — Pluggable JavaScript/TypeScript linter. Add
eslint-plugin-securityandeslint-plugin-no-unsanitizedfor security rules coveringeval, regular expression injection, and unsafe HTML insertion. - golangci-lint — Fast aggregator of Go linters. Includes
gosecfor security analysis (hardcoded credentials, SQL injection, weak crypto, file permissions). - hadolint — Dockerfile linter that enforces best practices: avoid
latesttags, set non-rootUSER, avoid shell injection inRUN. - kube-linter — Static checks for Kubernetes manifests and Helm charts: privileged containers, missing resource limits, host namespace access, and more.
- Opengrep — Community fork of Semgrep CE (LGPL 2.1, backed by a consortium of AppSec vendors) that remains compatible with Semgrep rules. Consider it if you need open-source features, such as intrafile taint analysis, that Semgrep keeps in its commercial tier.
- Ruff — Very fast Python linter and formatter. Its
Srules reimplement many Bandit checks, so one tool can cover style and basic security at pre-commit speed. - Semgrep — Fast, multi-language static analysis using readable YAML pattern rules. Community registry contains thousands of security rules. Supports custom rules for project-specific patterns. The Community Edition engine is open source; some cross-file analysis and the managed rule packs require the commercial platform.
- ShellCheck — Static analysis for shell scripts: unquoted variables, command injection patterns, and portability bugs. Useful for CI scripts and Dockerfile
RUNhelpers. - SpotBugs + find-sec-bugs — SpotBugs is a Java bytecode analyzer; find-sec-bugs adds 130+ security bug patterns covering OWASP Top 10 for Java/Kotlin.
- TFLint — Terraform linter that catches invalid provider arguments, deprecated syntax, and module issues. Pair it with Trivy for security checks.
Commercial¶
- SonarQube Server / SonarQube Cloud — Code quality and security analysis across 30+ languages (formerly SonarQube and SonarCloud). The open-source Community Build is free; commercial editions add deeper security analysis, security hotspot management, portfolio reporting, and branch analysis.
Links¶
Further reading¶
- OWASP Code Review Guide
- Semgrep rule registry
- gosec — Go security checker
- OWASP Cheat Sheet — Injection Prevention
-
Listed in alphabetical order. ↩