The Mars Climate Orbiter was lost in 1999 to a unit-conversion error that code review failed to catch — a bug a modern type-checked static analyzer would have flagged as an error on the first compile. That failure cost $327 million. NASA's Jet Propulsion Laboratory subsequently mandated the use of formal static analysis on all flight software, embedding it directly into build pipelines rather than treating it as an optional quality gate.
Linting is a lightweight form of static analysis focused on style, syntax, and common anti-patterns. The term comes from lint, a 1978 Unix tool written by Stephen Johnson at Bell Labs to flag suspicious C constructs. Modern linters such as ESLint, Pylint, and RuboCop operate on the abstract syntax tree (AST) of a file without executing it, applying rule sets that encode known bad patterns.
Static analysis is the broader discipline: it includes data-flow analysis, taint tracking, inter-procedural analysis, and formal verification. Tools like SonarQube, Coverity, and Facebook's Infer go further than linters — they reason about how values move through the program across function boundaries, catching null-pointer dereferences, SQL injection paths, and resource leaks that simple pattern matching misses.
The practical difference in a team workflow is coverage vs. speed. A linter runs in under a second on a file; a deep static analyzer may take minutes on a large codebase. Both have a place in the pipeline, at different stages.
The Heartbleed vulnerability (CVE-2014-0160), disclosed April 2014, was a buffer over-read in OpenSSL's heartbeat extension. The bug was a missing bounds check on a user-supplied length value before memcpy. Analysis conducted after the disclosure by researchers at Carnegie Mellon's CyLab Security and Privacy Institute showed that both Coverity Scan and Klocwork — tools available and free for open source projects at the time — would have flagged the specific memcpy call as an unvalidated size issue.
The OpenSSL project was not running those tools in its CI pipeline in 2012 when the vulnerable code was introduced. After Heartbleed, the OpenSSL Foundation implemented mandatory Coverity scanning on every commit. This is a documented, publicly available lesson in the cost of treating static analysis as optional.
Static analysis tools do not replace code review. They eliminate the category of defects that tools are better at than humans — consistent pattern matching at scale — so human reviewers can focus on logic, architecture, and intent.
Understanding what each tool class targets helps teams select the right tool for the right pipeline stage.
| Tool Class | Primary Targets | Example Tools | Typical Trigger |
|---|---|---|---|
| Linter | Style, syntax, naming, simple anti-patterns | ESLint, Pylint, RuboCop, golangci-lint | Pre-commit / on save |
| Type Checker | Type mismatches, null safety, interface violations | mypy, TypeScript compiler, Sorbet, Flow | Pre-commit / CI |
| SAST (Security) | Injection flaws, unsafe deserialization, hardcoded secrets | Semgrep, Bandit, Brakeman, CodeQL | PR check / nightly |
| Deep SA | Null deref, race conditions, memory leaks, inter-proc bugs | Coverity, Infer, Klocwork, Polyspace | Nightly / pre-release |
Linters and type checkers should block the commit. Deep static analysis should block the release. SAST tools should block the merge — but require triage to avoid paralysis from false positives in legacy codebases.
Every team that deploys static analysis confronts the same challenge: tools that cry wolf too often get silenced. The 2016 Facebook Infer deployment at Facebook engineering is a well-documented case. The team found that running Infer in CI without tuning produced a 70%+ false-positive rate on new Android code. Instead of abandoning the tool, they built a differential mode — Infer only reported bugs that were new in the diff, not pre-existing in the codebase. This reduced alert volume dramatically and maintained developer trust.
Google's internal experience, described in the 2018 paper "Lessons from Building Static Analysis Tools at Google" (Sadowski et al., CACM), similarly concluded that warnings must be actionable, accurate, and timely to be adopted. Their FindBugs successor tooling required 90%+ true-positive rate before a check was enabled by default.
In Lesson 2 we will examine how to configure and enforce ESLint, Pylint, and SonarQube in real team environments — including how to write custom rules and manage rule inheritance across monorepos.
Your team maintains a Python/TypeScript monorepo with 300k lines of code. You are evaluating which static analysis tools to add to different stages of your CI/CD pipeline. You need to justify your recommendations to a skeptical engineering manager who is worried about build-time impact and false positives.
In 2015, Airbnb Engineering published eslint-config-airbnb on npm, encoding their internal JavaScript style guide as a sharable ESLint configuration. It became one of the most downloaded npm packages in history — consistently above 30 million weekly downloads by 2023. The reason was not Airbnb's brand but a practical reality: most teams lacked the time to build a principled rule set from scratch, and Airbnb's configuration represented documented, deliberated decisions rather than arbitrary defaults.
ESLint's flat config system (default from v9, introduced in v8.21) uses eslint.config.js instead of .eslintrc. The key architectural decisions a team must make are: which rule sets to extend, which plugins to require, and how to handle monorepo overrides.
The critical practice is the legacy override block. When inheriting a codebase, enabling strict rules globally will flood the team with warnings on code no one is actively changing. The pattern of creating a legacy/** glob with relaxed rules — documented with a comment explaining why — lets teams tighten standards incrementally without blocking current work.
Pylint uses a .pylintrc or pyproject.toml section. Its most important configuration decision is the disable list: which checks to silence. Teams that disable checks without documented rationale create invisible technical debt.
The fail-under threshold is a powerful enforcement mechanism. By setting a minimum score (Pylint scores 0–10), CI will break if code quality degrades below the baseline. The threshold should be set at the current project score, then raised incrementally as quality improves — never lowered without a documented decision.
SonarQube's most important concept for teams adopting it on existing codebases is the New Code Period (formerly called the "leak period"). Rather than requiring teams to fix all existing technical debt before shipping, SonarQube gates only on issues introduced in the current branch. This is conceptually identical to Facebook Infer's differential mode — focus enforcement on new code, track old debt separately.
The default SonarQube Quality Gate ("Sonar way") fails a build if new code has: fewer than 80% coverage, any new bugs rated A–C, or any new security hotspots unreviewed. Teams typically customize this — the important governance practice is that any loosening of the Quality Gate requires an Architecture Decision Record (ADR), not just a config change.
Spotify's engineering blog (2020) described their SonarQube rollout across 150+ microservices: they ran SonarQube in "advisory" mode for 90 days before making it a hard gate, using the period to establish baselines and train teams on interpreting findings. Hard enforcement without a grace period caused developer resistance in early pilots.
Configuration drift occurs when different teams in a monorepo maintain separate .eslintrc or .pylintrc files that diverge over time. The solution used by large organizations (Google, Meta, Stripe) is to publish a single internal npm/PyPI package — e.g., @company/eslint-config — that contains the canonical rules. Teams extend from this package and can only add rules, never remove them. The config package itself is reviewed by the platform team before changes are merged.
Every disabled lint rule must have a comment with three things: the rule ID, the reason it is disabled, and a reference to a ticket or ADR. Silent suppressions — // eslint-disable-next-line with no explanation — are themselves a lint violation in well-run codebases.
packages/legacy/**) in an ESLint configuration?fail-under configuration option does what in a CI pipeline?fail-under sets a minimum acceptable Pylint score (0–10); falling below it fails the CI run.fail-under sets a minimum Pylint score threshold; the CI job exits non-zero if the project scores below that number.You've inherited a monorepo that has an .eslintrc with 12 silently-disabled rules (no comments) and a .pylintrc with fail-under = 5.0. Your task is to develop a remediation plan — what to document, what to raise, and how to structure a shared config package rollout.
When GitHub made CodeQL available to all public repositories in September 2020 as part of GitHub Advanced Security, their internal data showed that 72% of critical vulnerabilities discovered by CodeQL in open-source projects were found in code that had passed all existing CI checks. The tool used semantic analysis — understanding what the code does, not just what it looks like — to trace paths from user input to dangerous sinks across multiple files and function calls.
SAST (Static Application Security Testing) tools differ from linters in two ways that affect pipeline integration: they take significantly longer to run (minutes vs. seconds), and their findings require security expertise to triage. This means they cannot simply block PRs the way a linter does — a 10-minute CodeQL scan that fails every PR with a false positive will be disabled within a week.
The functional integration model used by most mature organizations has three components: fast rules block PRs (Semgrep patterns, Bandit, Brakeman on diff only), deep scans run on main branch nightly (CodeQL, Coverity full analysis), and findings are triaged into a security backlog with SLA-based remediation windows rather than immediate block requirements.
The following represents a production-grade two-stage SAST setup: Semgrep runs fast checks on PR diffs, CodeQL runs deep analysis on every push to main.
The most common failure mode in SAST adoption is treating all findings as equally urgent. A documented triage model, aligned with CVSS severity, prevents alert fatigue while ensuring critical issues are addressed promptly. The model below is based on practices documented by OWASP's DevSecOps guidance and is consistent with how organizations like Netflix and Cloudflare handle SAST output.
| Severity | Example Finding | Pipeline Action | Remediation SLA |
|---|---|---|---|
| Critical | SQL injection, RCE vector, hardcoded credential | Block PR immediately | Fix before merge |
| High | XSS sink, path traversal, insecure deserialization | Block PR; allow exception with security team sign-off | 7 days |
| Medium | Insecure random, missing TLS validation | Advisory warning; tracked in backlog | 30 days |
| Low / Info | Verbose error messages, weak cipher preference | Nightly report only | Next quarter |
Post-breach analysis of the Equifax Apache Struts vulnerability (CVE-2017-5638) showed that the organization had SAST tooling deployed but findings were not being acted upon within SLA. A critical-severity finding on the specific Struts version was documented in their tracking system 68 days before exploitation. The tool worked; the process did not. Triage SLA enforcement is not optional infrastructure.
Semgrep's power for teams is the ability to write organization-specific rules that encode your own security anti-patterns. The rule format is YAML with code pattern matching.
Every SAST finding that is suppressed (marked as false positive or accepted risk) must be reviewed by a second person with security knowledge and logged with an expiration date. Accepted risks do not stay accepted indefinitely — they must be reviewed when the affected component is next modified.
You are designing the SAST integration for a fintech startup's GitHub Actions pipeline. The codebase is Python/Node.js, handles payment card data, and must demonstrate compliance with PCI DSS requirements. Your engineering manager wants security checks but cannot tolerate broken pipelines on routine commits.
Stripe's engineering team described their progressive TypeScript adoption in a 2021 blog post: they wrote a custom ESLint rule that flagged any use of TypeScript's escape hatches — any, @ts-ignore, and type assertions without documented justification — in files above a certain test coverage threshold. This created a self-reinforcing system: well-tested code had to be well-typed; legacy code could still use escape hatches. The rule was written by the developer tools team and published to their internal ESLint config package, automatically applying to all repositories on next version bump.
Custom rules are justified when: (1) a pattern appears in code review feedback repeatedly and reviewers must catch it manually; (2) the pattern is domain-specific enough that public rule sets don't cover it; (3) the fix is mechanical enough that automation is reliable. The signal that a custom rule is warranted is when the same review comment appears three or more times in a sprint across different PRs.
Custom rules are not the right tool for architectural decisions, design judgment, or anything that requires context unavailable from the syntax tree. "Don't build this feature this way" is a conversation, not a lint rule.
ESLint rules are Node.js modules that export an object with a create method returning an AST visitor. The visitor is a map from AST node types to handler functions. The following example flags direct usage of Math.random() in files under a src/crypto/ path, where cryptographically secure randomness is required.
Pylint's checker system is based on AST visitors using Python's ast module. Custom checkers are registered as plugins via load-plugins in .pylintrc. The following checker flags any function that both writes to a logger and returns a value — a pattern that, in the team's codebase, indicated that side effects were being hidden in utility functions.
Custom rules accumulate technical debt of their own if not governed. Teams at Amazon and Google have documented a lifecycle for internal lint rules: proposal (3+ repeated review comments trigger consideration), review (platform team evaluates false-positive risk), staging (warning-only for 2 weeks on main branch), enforcement (promoted to error), and retirement (rule is removed when the underlying pattern is eliminated from the codebase).
The retirement phase is frequently neglected. Rules that no longer apply to the codebase add to build time and confuse new engineers who cannot understand why a pattern is forbidden. Quarterly audits of custom rule trigger counts — rules with zero violations in 90 days are candidates for retirement — prevent this accumulation.
| Phase | Trigger | Duration | Outcome |
|---|---|---|---|
| Proposal | 3+ repeated review comments on same pattern | 1–2 days | Rule RFC submitted to platform team |
| Review | Platform team evaluates FP risk and scope | Up to 1 week | Approved or rejected with rationale |
| Staging | Rule merged as warning-only | 2 weeks | FP rate measured; developers notified |
| Enforcement | FP rate below threshold; no objections | Ongoing | Rule promoted to error; blocks CI |
| Retirement | Zero violations for 90 days | 1 sprint | Rule removed; ADR updated |
The most powerful enforcement mechanism for team-wide linting standards is not CI gates — it's editor integration. When ESLint or Pylint runs in developers' editors via LSP and highlights violations as they type, issues are caught before they're even committed. Pre-commit hooks and CI are the backstop, not the primary defense. Invest in developer tooling documentation and onboarding scripts alongside rule authorship.
This completes the four lessons of Module 3. Proceed to the Module Test to validate your understanding across all topics: tool categories, configuration governance, SAST pipeline integration, and custom rule authorship.
create(context) method return?create(context) returns an AST visitor object — keys are AST node type names, values are handler functions called when nodes of that type are encountered during tree traversal.Your team uses Django (Python) and React (TypeScript). Two patterns keep appearing in code review: (1) Django views that call print() instead of using the logger, and (2) React components that use dangerouslySetInnerHTML without a corresponding JSDoc comment linking to the security review ticket. You need to design custom rules for both and define their lifecycle governance.
// eslint-disable-next-line) in a well-governed codebase?fail-under threshold should never be lowered without what?pattern-either key does what?meta.messages object?