10. Secure Development Lifecycle
This chapter covers how security is built into software from the first design sketch to the running production system, rather than bolted on after a breach. It moves through three stages that mirror how software is actually made: reasoning about threats while the design is still on a whiteboard (threat modeling and secure design), verifying the built code against those threats (security testing and assurance), and automating both so they run on every change (DevSecOps). The unifying idea is that defects found early are cheap to fix and defects found in production are expensive, so the whole discipline is about shifting security left — earlier in time and closer to the developer.
<a id="101-threat-modeling-and-secure-design"></a>
10.1 Threat Modeling and Secure Design
Before a single line of code exists, you can already reason about how a system will be attacked by studying its design. This section covers the structured techniques teams use to find design-level flaws — the ones no amount of later testing can catch — and the principles that keep a design defensible by construction.
Threat Modeling with STRIDE
Theory
Threat modeling is the practice of sitting down with a design and systematically asking, "How could this go wrong on purpose?" The reason to do it early is economic and logical: a flaw baked into the architecture (say, trusting a value the client controls) cannot be patched away later without redesigning, so you want to find it while changing the design is still just an edit to a diagram. The problem is that "think about threats" is too vague — people only imagine the attacks they already know. STRIDE fixes this by giving you a checklist of six threat categories to apply to every component, so you are prompted toward threats you would otherwise miss.
STRIDE is a mnemonic invented at Microsoft. Each letter names a threat and, usefully, each threat is the violation of a specific security property:
| Letter | Threat | Violates | Plain-language question |
|---|---|---|---|
| S | Spoofing | Authentication | Can someone pretend to be another user or system? |
| T | Tampering | Integrity | Can someone modify data or code in transit or at rest? |
| R | Repudiation | Non-repudiation | Can someone deny doing something with no proof otherwise? |
| I | Information disclosure | Confidentiality | Can someone read data they should not? |
| D | Denial of service | Availability | Can someone make the system unavailable? |
| E | Elevation of privilege | Authorization | Can someone gain capabilities they should not have? |
The method is mechanical, which is its strength: draw the system as components and data flows (see Trust Boundaries and Data Flow Diagrams below), then walk each element and each flow and ask all six questions. You come out with a list of threats, each of which you then decide to mitigate, eliminate, transfer, or accept. Think of STRIDE as a smoke detector grid for a building: you do not rely on noticing smoke, you put a detector in every room on a schedule.
Examples
Applying STRIDE to a single data flow — a browser sending a login request to an API:
[User/Browser] ──(1) POST /login {user,pass}──▶ [Auth API] ──▶ [User DB]
STRIDE walk of flow (1):
S Spoofing Attacker replays a captured request to impersonate the user
T Tampering MITM alters the request body in transit
R Repudiation No audit log → user denies a password change they made
I Info disclosure Credentials sent over plaintext HTTP are sniffed
D Denial of svc Unlimited login attempts exhaust DB connections
E Elevation "role" field accepted from client body sets role=admin
A threat model is often captured as a simple table that turns each finding into a decision:
| ID | Element | STRIDE | Threat | Mitigation | Status |
|----|-------------|--------|---------------------------------|-------------------------|----------|
| T1 | /login flow | I | Creds sniffed over HTTP | Enforce TLS (HSTS) | Mitigate |
| T2 | /login flow | D | Credential-stuffing / brute | Rate limit + lockout | Mitigate |
| T3 | Auth API | E | Client-supplied role trusted | Derive role server-side | Mitigate |
| T4 | Auth API | R | No record of sensitive actions | Append-only audit log | Mitigate |
Key Takeaways
- Threat modeling finds design flaws early, when they are cheapest to fix and before any code can be tested.
- STRIDE is a six-category checklist (Spoofing, Tampering, Repudiation, Information disclosure, Denial of service, Elevation of privilege); each category is the violation of one security property.
- Apply STRIDE by walking every component and every data flow in a diagram and asking all six questions — it forces coverage instead of relying on intuition.
- Every identified threat ends in an explicit decision: mitigate, eliminate, transfer, or accept.
🧪 Practice
- Take a simple "upload a profile picture" feature and list one concrete threat for each STRIDE letter.
- Map each STRIDE category to the CIA-plus security property it violates (e.g., Tampering → Integrity).
- Draw a two-component diagram (client → server) for a file-download endpoint and produce a four-row threat table with mitigations.
- Interview-style: "Why is threat modeling done before implementation rather than after?" Hint: compare the cost and feasibility of changing a diagram versus re-architecting shipped code.
Attack Trees and DREAD Scoring
Theory
STRIDE tells you what kinds of threats exist; attack trees and risk scoring help you understand how an attacker would achieve a goal and which threats to fix first. An attack tree is a hierarchical diagram whose root is an attacker's goal (e.g., "read another user's email") and whose branches are the alternative ways to reach it, refined down to concrete leaf actions. Child nodes are combined with OR (any one child achieves the parent) or AND (all children are required). The value is that it externalizes an attacker's reasoning: once you can see every path to the goal, you can look for the cheapest path (which is the one a real attacker takes) and cut it.
Attack trees can be annotated — with cost, skill required, or "possible/impossible" — and the annotations propagate up the tree. Under an OR node, the parent takes the minimum cost of its children (the attacker picks the easy path); under an AND node, costs add up. This quickly reveals the weakest link.
Once you have a list of threats, you must prioritize, because you cannot fix everything at once. DREAD is one scoring model that rates each threat 1–10 on five factors and averages them into a single risk score:
| Factor | Question |
|---|---|
| Damage | How bad is the impact if exploited? |
| Reproducibility | How reliably can it be exploited? |
| Exploitability | How much effort/skill does exploitation take? |
| Affected users | How many users are impacted? |
| Discoverability | How easily can an attacker find the flaw? |
DREAD is worth knowing but is openly criticized: the scores are subjective and two engineers often disagree, so many teams drop the "Discoverability" factor (you should assume flaws will be found) or replace DREAD entirely with CVSS (covered in 10.3) for consistency. Treat DREAD as a structured conversation starter, not a precise measurement.
Examples
An attack tree for stealing a session, with annotated leaf costs propagating upward:
GOAL: Hijack a user's authenticated session
│
├── OR ── Steal the session cookie
│ ├── AND ── XSS to read cookie [needs: injectable sink + no HttpOnly]
│ │ ├─ Find XSS sink cost: $ (low)
│ │ └─ Cookie lacks HttpOnly cost: $ (low) → AND = $ + $ = low
│ └── OR ─── Network sniff (no TLS) cost: $$$ (TLS everywhere → high)
│
└── OR ── Guess/forge the session token
└── Token is predictable (seq/time) cost: $$$$ (crypto-random → ~impossible)
Cheapest path = XSS + missing HttpOnly → FIX HERE FIRST.
A DREAD scoring of two threats to decide ordering:
Threat D R E A D Avg Rank
Stored XSS in comments 8 9 6 9 7 7.8 1 (fix first)
Verbose error stack trace 3 8 9 5 6 6.2 2
Higher average → higher priority. XSS wins on Damage + Affected users.
Key Takeaways
- An attack tree decomposes an attacker's goal into alternative paths (OR) and required steps (AND), exposing the cheapest path — the one you must cut.
- Cost/feasibility annotations propagate up: minimum under OR, sum under AND.
- DREAD scores a threat 1–10 on Damage, Reproducibility, Exploitability, Affected users, and Discoverability, then averages for a priority number.
- DREAD is subjective and partly deprecated; many teams drop Discoverability or use CVSS instead — use it to structure discussion, not as exact truth.
🧪 Practice
- Build an attack tree with the root goal "withdraw money from another person's account" and at least two OR branches.
- Annotate your tree's leaves with low/medium/high cost and compute the cheapest attack path.
- Score two of your own app's hypothetical bugs with DREAD and justify which you would fix first.
- Interview-style: "What are the main criticisms of DREAD, and what would you use instead?" Hint: think about inter-rater agreement and whether 'discoverability' should lower a threat's priority.
Trust Boundaries and Data Flow Diagrams
Theory
Threat modeling needs a picture to walk, and the right picture is a Data Flow Diagram (DFD): a map of how data moves through a system. A DFD uses a small, fixed vocabulary of shapes — external entities (users, third-party services; usually a rectangle), processes (code that transforms data; a circle), data stores (databases, files, queues; two parallel lines), and data flows (arrows showing data moving). Drawing the system this way is less about art and more about forcing you to name every place data is handled, because each of those is a place something can go wrong.
The single most important thing a DFD adds for security is the trust boundary: a dashed line drawn wherever data crosses from a less-trusted zone into a more-trusted one — for example, from the public internet into your server, or from your application into the operating system. Trust boundaries are where attacks happen, because that is exactly where trusted code starts handling data it did not create. The classic rule follows directly: validate all input as it crosses a trust boundary. Think of a trust boundary as a border checkpoint — inside the country you can move freely, but anything crossing the border gets inspected.
In STRIDE practice, trust boundaries tell you where to look hardest. A data flow that stays entirely within one trust zone is low-risk; a flow that crosses a boundary deserves the full six-question STRIDE walk. This is why DFDs and STRIDE are taught together: the diagram localizes risk and the checklist enumerates it.
Examples
A DFD for a web app, with the trust boundaries made explicit:
┌─────────────── INTERNET (untrusted) ───────────────┐
│ [ Browser ] · (external entity, you don't own) │
└──────────────────────┬──────────────────────────────┘
│ HTTPS request ← TRUST BOUNDARY #1 (validate here!)
┌───────────────────────▼─────────── DMZ (semi-trusted) ─┐
│ ( Web / API process ) │
└───────────┬───────────────────────────┬────────────────┘
│ SQL query ← TRUST BOUNDARY #2 (parameterize!)│ cache read
┌───────────▼──────────┐ ┌───────▼──────────┐
│ ═══ User DB ═══ │ │ ═══ Redis ═══ │
│ (data store, trusted) │ (data store) │
└──────────────────────┘ └──────────────────┘
Rule: every arrow that crosses a dashed boundary gets input validation
and the full STRIDE treatment.
Key Takeaways
- A Data Flow Diagram models a system with four element types: external entities, processes, data stores, and data flows.
- A trust boundary is a line where data moves from lower to higher trust; it is where attacks concentrate because trusted code begins handling untrusted data.
- The core rule is to validate (and often authenticate/authorize) all input as it crosses a trust boundary.
- DFDs and STRIDE pair up: the diagram shows where to look, and boundary-crossing flows earn the full STRIDE walk.
🧪 Practice
- Draw a DFD for a mobile app that talks to a backend API and a third-party payment provider; label every element by type.
- Mark all trust boundaries on your diagram and state what validation each crossing requires.
- Identify which single data flow in your diagram is highest risk and explain why using its trust-boundary crossing.
- Interview-style: "Why do security reviewers focus on trust boundaries rather than reviewing every line equally?" Hint: think about where untrusted input first meets code that acts on it.
Secure Design Patterns
Theory
Threat modeling finds problems; secure design patterns are the reusable principles that prevent whole classes of them before they appear. These are not code snippets but design dispositions — defaults you adopt so that the safe path is the easy path. They trace back to Saltzer and Schroeder's 1975 principles and remain the backbone of secure architecture.
The most important patterns:
| Principle | Meaning | Everyday analogy |
|---|---|---|
| Least privilege | Give each component only the permissions it needs, nothing more | A hotel keycard that opens only your room |
| Defense in depth | Layer independent controls so one failure is not fatal | A castle with a moat and walls and guards |
| Fail securely | On error, default to denying access, not granting it | A door that locks, not unlocks, when power fails |
| Secure defaults | Ship with the safe configuration on by default | A new phone that is encrypted out of the box |
| Complete mediation | Check authorization on every access, not just the first | Re-checking a ticket at each gate, not only the entrance |
| Economy of mechanism | Keep security-critical code small and simple | A lock with few moving parts is easier to verify |
| Separation of duties | Require two parties for sensitive actions | Two keys to launch, held by different people |
| Psychological acceptability | Make the secure way usable, or people bypass it | A seatbelt comfortable enough that people wear it |
The deepest of these is least privilege, because it bounds the blast radius of any single compromise: if the image-resizing service can only read the upload bucket, an attacker who takes it over still cannot touch your user database. "Fail securely" is the subtle one people get wrong — a common bug is a permission check that throws an exception which an outer catch swallows, accidentally allowing the action. Design so that the absence of a positive "allow" decision means deny.
Examples
"Fail securely" and "complete mediation" in code — note how the safe default is deny:
def is_authorized(user, resource, action):
try:
policy = policy_engine.lookup(user, resource) # may raise on error/timeout
return policy.permits(action) # explicit positive decision
except Exception:
log.error("authz lookup failed; denying") # FAIL SECURELY: error == deny
return False # never 'return True' here
def read_file(user, path):
# COMPLETE MEDIATION: check on every call, even for the same user/session
if not is_authorized(user, path, "read"):
raise PermissionError("denied") # deny-by-default
return storage.get(path)
Least privilege expressed as a tight IAM-style policy instead of a wildcard:
{
"Effect": "Allow",
"Action": ["s3:GetObject"],
"Resource": "arn:aws:s3:::uploads/*"
}
{
"Effect": "Allow",
"Action": "s3:*",
"Resource": "*"
}
Key Takeaways
- Secure design patterns are reusable principles that eliminate classes of flaws by making the safe path the default path.
- Least privilege limits the blast radius of any single compromise and is the highest-leverage principle.
- Fail securely means an error or missing decision must result in deny, never accidental allow; complete mediation means checking authorization on every access.
- Secure defaults, defense in depth, economy of mechanism, separation of duties, and psychological acceptability round out the core set from Saltzer and Schroeder.
🧪 Practice
- Rewrite an IAM/role policy from
"*"wildcards to the minimum actions and resources a thumbnail service needs.- Find and fix a "fail open" bug: given an authorization function whose
catchblock returnsTrue, explain the risk and correct it.- For a payment-approval feature, describe how you would apply separation of duties.
- Interview-style: "How does least privilege reduce the impact of a compromised service?" Hint: reason about what an attacker can reach after taking over a service that holds only narrow permissions.
Abuse Cases and Security Requirements
Theory
Ordinary requirements and use cases describe what the system should do for cooperative users ("a user can reset their password"). But attackers are not cooperative users, and a system that perfectly satisfies its use cases can still be wide open. Abuse cases (also called misuse cases) flip the perspective: they describe what a malicious actor will try to do to the system ("an attacker tries to reset another user's password without access to their email"). Writing them makes the implicit threat explicit so it can be designed against, tested, and signed off — rather than being discovered in production.
The point of abuse cases is that they convert vague worries into security requirements, which are testable statements the same way functional requirements are. A good security requirement is specific and verifiable: not "the app should be secure," but "password reset tokens must be single-use, cryptographically random (≥128 bits), and expire within 15 minutes." Because it is concrete, QA can write a test for it and a reviewer can confirm it. This is how security gets first-class status in the backlog instead of being an afterthought that competes with features and loses.
A practical way to generate them: for each normal use case, ask "how would someone abuse this?" and for each STRIDE threat you found, ask "what requirement would prevent it?" Abuse cases are often written in the same user-story format the team already uses, so they live in the same backlog as features — which is exactly the point.
Examples
A use case paired with its abuse case and the requirement it produces:
USE CASE
As a user, I can request a password-reset link sent to my email.
ABUSE CASE
As an attacker, I request resets for many accounts and brute-force the
token in the URL, OR I enumerate which emails are registered from the
response text.
DERIVED SECURITY REQUIREMENTS (testable)
SR-1 Reset tokens are >=128-bit CSPRNG values, single-use, 15-min TTL.
SR-2 Reset endpoint returns an identical response whether or not the
email exists (no user enumeration).
SR-3 Reset requests are rate-limited to 5 / hour / IP and / account.
The same abuse case turned into an automated test (so the requirement is enforced, not just written):
def test_reset_response_does_not_leak_account_existence(client):
# SR-2: response must be identical for known and unknown emails
known = client.post("/reset", json={"email": "real@site.test"})
unknown = client.post("/reset", json={"email": "nobody@site.test"})
assert known.status_code == unknown.status_code # same status
assert known.text == unknown.text # same body → no enumeration
Key Takeaways
- Abuse (misuse) cases describe what a malicious actor will attempt, complementing normal use cases that assume cooperative users.
- They convert threats into concrete, testable security requirements that live in the same backlog as features.
- A good security requirement is specific and verifiable (e.g., exact token entropy, TTL, and single-use), so QA and reviewers can confirm it.
- Deriving abuse cases from use cases and from STRIDE threats gives security first-class, trackable status rather than afterthought treatment.
🧪 Practice
- For a "transfer money between accounts" use case, write two abuse cases from an attacker's point of view.
- Turn one of those abuse cases into at least two specific, testable security requirements.
- Rewrite the vague requirement "the login should be safe" into three concrete, verifiable requirements.
- Interview-style: "Why are abuse cases more useful to developers than a generic 'make it secure' instruction?" Hint: think about what a developer or tester can actually build and verify against.
<a id="102-security-testing-and-assurance"></a>
10.2 Security Testing and Assurance
A design that was threat-modeled still has to be verified against what the code actually does. This section covers the techniques that find vulnerabilities in real code — by human review, by analyzing source or running programs, by inspecting dependencies, by fuzzing, and by locking fixes in place with tests — and how they complement each other because no single one finds everything.
Secure Code Review Practices
Theory
A secure code review is a human reading code specifically to find security defects — not style issues or bugs in general, but places where an attacker could gain something. It matters because automated tools are pattern matchers: they are excellent at finding known-shaped bugs and blind to logic flaws, broken authorization, and misuse of business rules, which are exactly the high-impact issues only a human who understands intent can catch. Tools and humans are complementary, and skipping the human review leaves the most damaging category uncovered.
The skill is knowing where to look, because you cannot read everything with equal attention. Reviewers prioritize the attack surface: anywhere untrusted input enters (request handlers, file uploads, message consumers), authentication and authorization logic, cryptography, anything that builds a query or command or path, and anything that handles secrets. A mental checklist helps — follow each piece of untrusted input from its entry point to every "sink" where it could cause harm (a SQL query, an HTML response, a shell command, a file path) and confirm it is validated or encoded correctly along the way. This "source-to-sink" tracing is the core move of both manual review and the SAST tools that automate it.
Practically, security review is folded into the normal pull-request process: the author describes the change, a reviewer with security awareness checks the risky parts, and significant findings block the merge. The economic payoff is the same as threat modeling — a flaw caught in review costs minutes; the same flaw in production can cost a breach.
Examples
A source-to-sink trace during review, with the reviewer's reasoning inline:
@app.route("/download")
def download():
name = request.args.get("file") # SOURCE: fully attacker-controlled
# Reviewer: is `name` validated before it reaches a filesystem sink? No.
path = os.path.join("/var/data", name) # SINK: path traversal — name="../../etc/passwd"
return open(path).read() # reads arbitrary files off the server
@app.route("/download")
def download():
name = request.args.get("file", "")
if not re.fullmatch(r"[A-Za-z0-9_.-]+", name): # allowlist the character set
abort(400)
path = os.path.realpath(os.path.join("/var/data", name))
if not path.startswith("/var/data/"): # confirm it stayed in the dir
abort(400) # defeats ../ traversal
return open(path, "rb").read()
A reviewer's priority map for where to spend attention:
HIGH authn/authz checks · input parsing · SQL/command/HTML/path sinks · crypto · secrets
MED session handling · file I/O · deserialization · error handling (leaks)
LOW pure computation with no untrusted input · UI-only formatting
Key Takeaways
- Secure code review is a human hunting for security defects, focused on logic and authorization flaws that automated tools cannot see.
- Reviewers prioritize the attack surface: untrusted-input entry points, authn/authz, crypto, secrets, and anything building queries, commands, or paths.
- The central technique is source-to-sink tracing: follow untrusted data from entry to every dangerous sink and confirm validation/encoding.
- Fold review into pull requests; human and tool review are complementary, not redundant.
🧪 Practice
- Given a request handler, identify every "source" of untrusted input and every "sink" it could reach.
- Review the vulnerable
downloadexample above and list all the ways to exploit it before validation is added.- Write a one-page checklist you would use when reviewing a new API endpoint for security.
- Interview-style: "What classes of vulnerability will a human reviewer catch that a static analyzer typically misses?" Hint: think about flaws that depend on business meaning rather than code shape, like broken access control.
Static Application Security Testing (SAST)
Theory
SAST analyzes an application's source code (or bytecode) without running it — "white-box" testing — to find vulnerabilities by inspecting the program's structure. It automates the source-to-sink reasoning a human reviewer does: the tool builds a model of the code (control-flow and data-flow graphs) and performs taint analysis, marking data from untrusted sources as "tainted" and flagging any path where tainted data reaches a dangerous sink without passing through a sanitizer. Because it reads the code directly, SAST can find the exact file and line of a bug and can run early — on every commit, before anything is deployed — which is why it is a staple of "shift left."
The tradeoff is false positives. Because SAST reasons about code paths that might be possible rather than observing real executions, it over-reports: it flags a tainted-to-sink path even when some runtime condition makes it unreachable, or when a custom sanitizer it does not recognize actually makes it safe. Teams spend real effort tuning rules and triaging noise, and a tool that cries wolf gets ignored. The complementary weakness is that SAST cannot find issues that only exist at runtime — misconfiguration, authentication flaws in a running environment, or problems in code it cannot see (like a dependency behind an API).
Mentally, contrast it with DAST (next topic): SAST reads the blueprint of a building to find structural flaws; DAST walks around the finished building trying doors. Each sees what the other cannot.
Examples
What a SAST taint path looks like conceptually, and the finding it emits:
TAINT PATH (SQL injection)
source: request.getParameter("q") line 12 [tainted]
flow: String sql = "...WHERE n='"+q+"'" line 15 [tainted, concatenated]
sink: statement.executeQuery(sql) line 16 [DANGEROUS SINK]
no sanitizer on path → REPORT: SQLi (CWE-89), High
A finding and its fix (parameterization removes the taint path):
// SAST flags: tainted `q` concatenated into SQL and reaching executeQuery
String sql = "SELECT * FROM notes WHERE name = '" + q + "'"; // CWE-89
stmt.executeQuery(sql);
// Parameterized query: `q` is bound as data, never parsed as SQL → path is safe
PreparedStatement ps = conn.prepareStatement(
"SELECT * FROM notes WHERE name = ?"); // placeholder, not concatenation
ps.setString(1, q); // driver escapes/binds the value
ResultSet rs = ps.executeQuery();
Key Takeaways
- SAST inspects source/bytecode without executing it (white-box) and can pinpoint the file and line of a flaw early in the pipeline.
- Its engine is taint analysis: track untrusted data from sources to dangerous sinks and flag paths lacking a recognized sanitizer.
- Its main cost is false positives, because it reasons over possible paths rather than observed runs; triage and rule tuning are ongoing work.
- SAST cannot see runtime-only issues (misconfiguration, environment, code behind external APIs), so it pairs with DAST/IAST.
🧪 Practice
- Install a free SAST tool (e.g., Semgrep, Bandit for Python) and run it on a small project; categorize each finding as true or false positive.
- Write a Semgrep rule that flags string concatenation into a SQL query.
- Explain, with the taint-analysis model, why a parameterized query makes a SAST finding disappear.
- Interview-style: "Why does SAST produce false positives, and how would you keep developers from ignoring the tool?" Hint: relate over-reporting of possible-but-unreachable paths to signal-to-noise and triage workflow.
Dynamic and Interactive Testing (DAST/IAST)
Theory
DAST (Dynamic Application Security Testing) tests a running application from the outside, like an attacker — "black-box" testing. It knows nothing about the source code; it sends crafted HTTP requests (malformed inputs, injection payloads, tampered parameters) and watches the responses for evidence of a vulnerability. Its great strength is the mirror image of SAST's weakness: because it observes real behavior, a DAST finding is usually a real, reachable bug (low false-positive rate), and it catches runtime and configuration issues — missing security headers, TLS problems, authentication flaws — that source analysis cannot see. Its weakness is coverage: it can only test the parts of the app it manages to reach and exercise, so unreached endpoints and hard-to-trigger states go untested, and it cannot tell you where in the code the bug lives.
IAST (Interactive Application Security Testing) is the hybrid. It places an agent (instrumentation) inside the running application while tests (manual, DAST, or ordinary functional tests) drive it. From inside, the agent sees both the incoming request and the code path it triggers — so it combines DAST's "this really happened at runtime" confidence with SAST's "here is the exact line" precision, at low false-positive rates. The catch is that IAST needs instrumentation support for your language/runtime and only observes code that your tests actually exercise.
The clean way to hold these three is by what they can see:
| SAST | DAST | IAST | |
|---|---|---|---|
| Needs source code | Yes | No | Partial (runtime agent) |
| App must be running | No | Yes | Yes |
| Perspective | White-box | Black-box | Gray-box (inside) |
| False positives | Higher | Low | Low |
| Pinpoints code line | Yes | No | Yes |
| Finds config/runtime flaws | No | Yes | Yes |
| Coverage limited by | n/a | What it can reach | What tests exercise |
Examples
A DAST probe and the response signal it keys on:
GET /product?id=1' OR '1'='1 HTTP/1.1 # injection payload in a parameter
Host: shop.test
---
HTTP/1.1 500 Internal Server Error # error + SQL message in body =
...You have an error in your SQL syntax... # strong signal of SQL injection
How IAST sees the same request from inside the process:
IAST agent (inside the app) observes one request end-to-end:
HTTP in: id = "1' OR '1'='1" (tainted at entry)
reaches: Db.query("...WHERE id='"+id+"'") at OrderDao.java:48 (sink)
verdict: tainted input hit a SQL sink during a real request
→ CONFIRMED SQLi at OrderDao.java:48 (real + exact line)
Key Takeaways
- DAST tests a running app from the outside (black-box); its findings are usually real and reachable, and it catches runtime/config flaws SAST cannot.
- DAST's limits are coverage (only what it reaches) and that it cannot point to the vulnerable source line.
- IAST runs an agent inside the running app, combining runtime confirmation with exact code-line precision and low false positives.
- IAST is bounded by instrumentation availability and by whatever code paths your tests actually exercise; the three techniques are layered, not alternatives.
🧪 Practice
- Run OWASP ZAP (a free DAST tool) against a deliberately vulnerable app (e.g., OWASP Juice Shop) and record two findings.
- Explain why a DAST finding typically has a lower false-positive rate than a SAST finding.
- Fill in the SAST/DAST/IAST comparison table from memory, then check it against the one above.
- Interview-style: "An endpoint is never exercised by your test suite — which of SAST, DAST, and IAST could still find a bug in it, and why?" Hint: consider which techniques depend on actually reaching the code at runtime.
Software Composition Analysis and SBOM
Theory
Modern applications are mostly code you did not write: a typical project is a thin layer of your own logic on top of hundreds of open-source dependencies (and their dependencies, "transitively"). That means most of your attack surface is inherited. Software Composition Analysis (SCA) is the practice of inventorying all those third-party components and checking them against databases of known vulnerabilities (CVEs). The tool reads your lockfiles or installed packages, resolves the full dependency tree — including transitive dependencies you never chose directly — and tells you which versions have known flaws and what to upgrade to. It also checks licenses, since an incompatible license is a different kind of risk.
An SBOM (Software Bill of Materials) is the formal artifact SCA produces and that the industry increasingly requires: a complete, machine-readable list of every component in a piece of software, with versions and often hashes and licenses. The analogy is the ingredients list on packaged food — you cannot assess an allergy risk if you do not know what is inside, and you cannot respond to a newly disclosed vulnerability if you do not know whether you ship the affected component. The two dominant formats are SPDX and CycloneDX. The reason SBOMs became mandatory in many contexts (notably after Log4Shell and various supply-chain incidents) is speed of response: when a critical CVE drops, an organization with SBOMs can query "do we use this, and where?" in minutes instead of weeks.
The key realization is that this is about known vulnerabilities in dependencies — it complements SAST/DAST (which look at your own and runtime behavior) by covering the large inherited portion of the codebase.
Examples
An SCA scan output mapping dependencies to known CVEs:
$ sca scan
PACKAGE VERSION VULNERABILITY SEVERITY FIXED IN
log4j-core 2.14.1 CVE-2021-44228 CRITICAL 2.17.1 (Log4Shell / RCE)
lodash 4.17.15 CVE-2020-8203 HIGH 4.17.19
express 4.17.1 — — —
└─ (transitive) qs 6.7.0 CVE-2022-24999 HIGH 6.10.3 ← you never installed qs directly
A minimal SBOM fragment in CycloneDX (JSON), one component entry:
{
"bomFormat": "CycloneDX",
"specVersion": "1.5",
"components": [
{
"type": "library",
"name": "log4j-core",
"version": "2.14.1",
"purl": "pkg:maven/org.apache.logging.log4j/log4j-core@2.14.1",
"licenses": [{ "license": { "id": "Apache-2.0" } }],
"hashes": [{ "alg": "SHA-256", "content": "..." }]
}
]
}
Key Takeaways
- Most of an app is third-party code, so most of its vulnerabilities are inherited; SCA inventories all dependencies (including transitive ones) and matches them to known CVEs.
- SCA also flags license risks, not just security ones.
- An SBOM is the machine-readable ingredients list of a build (SPDX or CycloneDX), increasingly mandated for supply-chain transparency.
- SBOMs turn "are we affected by this new CVE?" from a multi-week hunt into a query, which is why they became standard after incidents like Log4Shell.
🧪 Practice
- Run an SCA tool (e.g.,
npm audit, Trivy, or OWASP Dependency-Check) on a real project and list its critical/high findings.- Generate an SBOM with a tool like Syft and identify three transitive dependencies you never installed directly.
- Explain how an SBOM would speed your response to a brand-new critical CVE in a common library.
- Interview-style: "Why can't SAST or DAST replace SCA for finding vulnerable dependencies?" Hint: think about whether those tools know the version and CVE history of a compiled or imported package.
Dependency and Supply Chain Risk
Theory
SCA finds known vulnerabilities in dependencies, but the software supply chain faces a broader and nastier category of risk: an attacker who compromises something upstream of you — a library, a build tool, a package registry, a CI system — so that malicious code flows into your product through a channel you trust. Because you trust your dependencies and your build pipeline, a compromise there inherits that trust and can reach enormous numbers of victims at once. This is why supply-chain attacks are so prized: one compromise of a popular package or build server can hit thousands of downstream organizations (as in the SolarWinds and event-stream incidents).
The threat takes several concrete forms worth naming:
| Attack | How it works |
|---|---|
| Typosquatting | Publish a malicious package named like a popular one (reqeusts) hoping for a typo'd install |
| Dependency confusion | Publish a public package with the same name as a company's private one; the resolver picks the higher public version |
| Malicious maintainer / account takeover | A trusted package's owner turns hostile or is compromised, and ships a poisoned update |
| Build-system compromise | Attacker injects code during the build (not in source), so the artifact is backdoored though the repo looks clean |
Defenses follow from the idea that you must verify, not just trust: pin exact versions and verify integrity hashes in lockfiles so a package cannot silently change; configure your registry/namespace to defeat dependency confusion; vet and minimize dependencies; and verify provenance — cryptographic proof of how and from what an artifact was built (frameworks like SLSA and signing with Sigstore address this, connecting to the artifact signing in 10.3). The mindset shift is from "is this dependency known-vulnerable?" to "can I prove this dependency is what it claims to be and was built from the source I think it was?"
Examples
Dependency confusion, the mechanism in brief:
Company uses an internal package: @acme/auth-utils (private, v1.4.2)
Attacker publishes to PUBLIC npm: @acme/auth-utils v99.0.0 (malicious)
Misconfigured resolver sees v99.0.0 > v1.4.2 and pulls the PUBLIC one.
→ Attacker code runs inside the company's build. Fix: scope-lock the
namespace to the private registry so public names can't shadow it.
Pinning and integrity verification in a lockfile (the hash is the defense):
{
"name": "left-pad",
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz",
"integrity": "sha512-XMg...=="
}
# `npm ci` installs EXACTLY the locked versions and FAILS if any integrity
# hash does not match — a swapped/tampered package cannot sneak in silently.
npm ci
Key Takeaways
- Supply-chain risk is broader than known CVEs: an attacker compromises something upstream (package, maintainer, registry, or build system) and rides your trust into your product.
- Named forms include typosquatting, dependency confusion, malicious/compromised maintainers, and build-system compromise; one hit can cascade to thousands of victims.
- Defenses verify rather than trust: pin exact versions with integrity hashes, lock private namespaces, vet and minimize dependencies, and check provenance.
- Provenance and signing (SLSA, Sigstore) let you prove an artifact was built from the source you expect — the mindset is "prove it's what it claims," not just "is it known-bad."
🧪 Practice
- Explain, step by step, how a dependency-confusion attack causes a public package to shadow a private one, and one configuration that prevents it.
- Inspect a lockfile in one of your projects and identify the integrity hashes; explain what
npm ci(or your tool's equivalent) does with them.- List three questions you would ask before adding a new open-source dependency to a project.
- Interview-style: "Why is a build-system compromise harder to detect than a vulnerable dependency?" Hint: consider whether the malicious code ever appears in the source repository you review.
Coverage-Guided Fuzzing in CI
Theory
A fuzzer tests software by throwing large volumes of automatically generated, often malformed input at it and watching for crashes, hangs, or sanitizer-detected memory errors. The premise is that developers write tests for inputs they thought of, while security bugs live in the inputs they did not — the edge cases, the oversized fields, the unexpected byte sequences. Fuzzing explores that space mechanically and tirelessly. It is especially effective on code that parses untrusted, structured input: file formats, network protocols, decoders, and serializers, where a single mishandled byte can mean a buffer overflow.
Naive random fuzzing is inefficient because most random bytes are rejected immediately by early input checks and never reach deep code. Coverage-guided fuzzing (as in AFL++ and libFuzzer) is the breakthrough: the target is compiled with instrumentation that reports which code paths each input executed, and the fuzzer keeps and mutates inputs that reach new code, evolving toward deeper coverage. It is a feedback loop — like a lockpicker who keeps any wiggle that moves a pin further, discarding the rest — so it steadily works its way into code random input would almost never reach.
Running fuzzing in CI (continuous fuzzing, as popularized by Google's OSS-Fuzz and ClusterFuzz) matters because fuzzing finds more the longer it runs, and code keeps changing. Rather than a one-off campaign, you keep a corpus of interesting inputs, fuzz continuously or on each change, and automatically file and track any new crash with its reproducer. The output of a crash is a specific input that triggers it, which becomes a regression test (next topic).
Examples
A libFuzzer-style fuzz target — the whole interface is one function that takes bytes:
// The fuzzer calls this repeatedly with mutated byte buffers.
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
parse_packet(data, size); // target code; built with ASan to catch memory errors
return 0; // a crash/overflow here = a reproducible finding
}
// Build: clang -fsanitize=fuzzer,address packet_fuzz.c -o fuzzer
// Run: ./fuzzer corpus/ # mutates corpus, reports coverage + crashes
How coverage guidance climbs into deep code that random bytes would miss:
if (len < 4) return; // random input dies here ~constantly
if (buf[0]==0x7F && buf[1]==0x45) { // "magic bytes": random almost never matches
parse_header(buf); // ← coverage-guided fuzzer LEARNS to reach here
decode_body(buf, len); // by keeping inputs that pass each gate
}
# CI: run a short fuzzing session on every pull request, upload any crashers.
fuzz:
script:
- ./fuzzer -max_total_time=120 corpus/ # 2-minute budget per PR
artifacts:
when: on_failure
paths: [crash-*] # the exact input that crashed = repro
Key Takeaways
- Fuzzing floods a program with generated/malformed inputs to find the crashes and memory errors that live in inputs developers never tested.
- It is most powerful on parsers of untrusted structured input (file formats, protocols, decoders).
- Coverage-guided fuzzing instruments the target and keeps inputs that reach new code, evolving toward deep paths that random fuzzing never reaches.
- Continuous fuzzing in CI (OSS-Fuzz style) keeps a growing corpus, fuzzes on each change, and turns every crash into a reproducible input for a regression test.
🧪 Practice
- Write a libFuzzer or AFL++ target for a small parser (e.g., a function that parses a date string) and run it for a minute.
- Explain why coverage guidance finds bugs behind a "magic bytes" check that pure random input essentially never would.
- Add a step to a CI config that runs a time-boxed fuzzing session and saves any crashing input as an artifact.
- Interview-style: "Why is fuzzing especially suited to code that parses untrusted input?" Hint: think about where unexpected byte sequences are most likely to cause memory-safety failures.
Security Unit and Regression Tests
Theory
Finding a vulnerability once is good; ensuring it never comes back is better. A security regression test is an automated test that encodes a specific past vulnerability (or a specific security requirement) so that if the flaw is ever reintroduced, the test fails and blocks the change. This matters because security fixes are fragile: a later refactor, a reverted commit, or a careless merge can silently undo a patch, and without a test guarding it, nobody notices until it is exploited again. The discipline is simple and powerful — every confirmed vulnerability gets a test before it is closed.
These tests come in two flavors. Security unit tests assert that a security control behaves correctly in isolation: the password hasher uses a strong algorithm, the authorization function denies by default, the sanitizer neutralizes a known XSS payload, the input validator rejects a traversal string. Regression tests are the subset that reproduce a specific historical bug — often built directly from the exploit or the crashing input a fuzzer found. Both run in the normal test suite on every commit, which is how they turn a one-time fix into a permanent guarantee.
The connection to everything above closes the loop: threat modeling and abuse cases produce security requirements, review and SAST/DAST/fuzzing find violations, and security tests lock the fixes in so the pipeline enforces them forever. This is the mechanism by which "shift left" actually sticks — the knowledge of each past failure lives on as executable, automatically checked code.
Examples
A regression test built from a real finding (the traversal bug from the review topic):
def test_download_rejects_path_traversal():
# Regression for VULN-2024-013: `file=../../etc/passwd` read arbitrary files.
resp = client.get("/download", query_string={"file": "../../etc/passwd"})
assert resp.status_code == 400 # must be rejected, not served
assert b"root:" not in resp.data # and must never leak /etc/passwd contents
Security unit tests asserting controls behave as required:
def test_passwords_use_strong_hash():
h = hash_password("hunter2")
assert h.startswith("$argon2") # not MD5/SHA1/plaintext
assert verify_password("hunter2", h) # and round-trips correctly
def test_authz_denies_by_default_on_error(monkeypatch):
# Fail-securely requirement: lookup failure must DENY (see Secure Design Patterns)
monkeypatch.setattr(policy_engine, "lookup", _raise)
assert is_authorized(user, "/secret", "read") is False
def test_sanitizer_neutralizes_known_xss():
assert "<script>" not in sanitize("<script>alert(1)</script>")
Key Takeaways
- A security regression test encodes a specific past vulnerability so its reintroduction fails the build; the rule is that every confirmed vuln gets a test before closing.
- Security unit tests assert that controls (hashing, authorization, sanitization, validation) behave correctly in isolation.
- Both run in the normal suite on every commit, converting one-time fixes into permanent, automatically enforced guarantees.
- These tests close the SDLC loop: requirements → detection → locked-in fix, which is what makes "shift left" durable rather than a one-off effort.
🧪 Practice
- Take the path-traversal fix from the review topic and write a regression test that fails if the fix is reverted.
- Write a security unit test asserting that your authorization function denies access when its policy lookup throws.
- Turn a fuzzer-found crashing input into a regression test that feeds that exact input and asserts no crash.
- Interview-style: "Why should every security bug fix be accompanied by a test, even when the fix is obviously correct?" Hint: think about what happens to an untested fix during a future refactor or merge.
<a id="103-devsecops"></a>
10.3 DevSecOps
DevSecOps is the practice of building security into the automated pipeline that turns a developer's commit into running software, so that security checks happen on every change without a human having to remember them. This section covers the automated gates and controls that live in CI/CD — from blocking insecure builds, to proving what you shipped, to managing the vulnerabilities that inevitably get through.
Security Gates in CI/CD Pipelines
Theory
A CI/CD pipeline is the automated assembly line that takes code from commit to production: build, test, scan, package, deploy. A security gate is a checkpoint in that line that automatically evaluates a security condition and can stop the build if it fails. The idea is to make security non-optional by moving it from "something a person should do" to "something the pipeline does every time," because humans forget, skip under deadline pressure, and do not scale to every commit. The pipeline never forgets and never gets tired.
The gates are simply the testing techniques from 10.2 wired into the pipeline with a pass/fail policy: run SAST, SCA, secrets scanning, and (against a staging deploy) DAST, then compare the results to a threshold. A typical policy is "fail the build on any new critical or high finding; warn on medium." The crucial design decisions are what severity blocks and how to handle noise: too strict and developers route around the pipeline; too loose and it is theater. Two refinements make gates livable — baselining (only fail on new findings, not the pre-existing backlog) and break-the-build vs. warn tiers so that low-confidence checks inform rather than block.
The reason gates belong early and automated is the same economic argument running through this whole chapter: a gate at the pull-request stage catches a vulnerability in seconds, before it merges, when fixing it is cheapest. Think of it as a series of quality-control stations on a factory line — a defective unit is pulled before it is boxed and shipped, not recalled from customers.
Examples
A pipeline with security gates and an explicit fail policy:
# .gitlab-ci.yml — security gates run on every merge request
stages: [build, test, security, deploy]
sast:
stage: security
script:
- semgrep ci --config auto --error # non-zero exit on findings = gate fails
# policy: block merge on any NEW high/critical; pre-existing issues are baselined
dependency-scan:
stage: security
script:
- trivy fs --exit-code 1 --severity CRITICAL,HIGH . # fail on critical/high CVEs
dast:
stage: security
needs: [deploy-staging] # scan the running staging app
script:
- zap-baseline.py -t https://staging.app.test -I # -I: warn, don't hard-fail (tunable)
The decision logic a gate applies:
for each finding:
if severity >= BLOCK_THRESHOLD and finding is NEW (not in baseline):
exit 1 # break the build → merge blocked
else:
annotate MR # visible warning, does not block
Key Takeaways
- A CI/CD pipeline automates build→deploy; a security gate is a checkpoint in it that can fail the build on a security condition.
- Gates make security non-optional by running the 10.2 techniques (SAST, SCA, secrets, DAST) automatically on every change.
- The hard part is policy: choose what severity blocks, baseline pre-existing findings, and split checks into break-the-build vs. warn tiers so developers do not route around the pipeline.
- Early gates (at pull-request time) catch flaws when they are cheapest to fix, consistent with shifting security left.
🧪 Practice
- Add a SAST step to a CI config (GitHub Actions or GitLab CI) that fails the build on new high-severity findings.
- Explain the risk of setting a gate too strictly, and how baselining mitigates it.
- Decide which of SAST, SCA, and DAST should run before deploy versus after a staging deploy, and justify the ordering.
- Interview-style: "How do you prevent a security gate from being so noisy that developers disable or bypass it?" Hint: think about baselining, severity thresholds, and warn-versus-block tiers.
Build Reproducibility and Artifact Signing
Theory
When you deploy a binary or container image, two questions matter for trust: was this built from the source I think it was, and has it been tampered with since. Reproducible builds answer the first. A build is reproducible if compiling the same source with the same toolchain always yields a bit-for-bit identical artifact. That sounds trivial but usually is not — builds embed timestamps, file ordering, absolute paths, and other nondeterminism that makes two builds of identical source differ. Making a build reproducible (fixing those sources of variance) lets independent parties rebuild from source and confirm they get the same artifact, which means the published binary contains nothing extra — a direct defense against the build-system compromise described in 10.2.
Artifact signing answers the second question. After building, you create a cryptographic digital signature over the artifact using a private key; anyone with the corresponding public key can later verify that the artifact is authentic (from you) and unmodified (the hash still matches). This is the integrity-and-authenticity guarantee from cryptography applied to software distribution: a consumer verifies the signature before trusting the artifact, so a swapped or backdoored image is rejected. Sigstore (with cosign) has made this practical by supporting keyless signing tied to an identity (e.g., an OIDC identity) with a public transparency log, removing the painful key-management step that kept signing rare.
Together they establish provenance — a verifiable chain from source to running artifact — which is exactly what frameworks like SLSA formalize. The mental model is a tamper-evident seal plus a notarized receipt: the seal (signature) shows nothing was altered, and reproducibility is the notary being able to independently recreate the item and confirm it matches.
Examples
Signing and verifying a container image with cosign (keyless):
# Sign after building — identity-based, signature recorded in a transparency log
cosign sign --yes registry.test/app@sha256:abc123...
# Consumer verifies BEFORE deploy: authentic + untampered, or the command fails
cosign verify \
--certificate-identity "ci@app.test" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
registry.test/app@sha256:abc123...
# Non-zero exit = do not deploy.
Why nondeterminism breaks reproducibility, and the fix:
Build #1 artifact hash: 9f2a... ┐ same source, same compiler,
Build #2 artifact hash: 4c81... ┘ DIFFERENT hash → NOT reproducible
Cause: embedded build timestamp + random file ordering in the archive.
Fix: SOURCE_DATE_EPOCH=<commit time> # pin the timestamp
sort archive entries, strip absolute paths
Result: Build #1 == Build #2 == independent rebuild → provenance verifiable.
Key Takeaways
- A reproducible build yields a bit-for-bit identical artifact from the same source, letting anyone independently rebuild and confirm the published binary has nothing extra.
- Nondeterminism (timestamps, file ordering, paths) breaks reproducibility; fixing those sources of variance restores it.
- Artifact signing attaches a digital signature so consumers can verify authenticity and integrity before trusting an artifact; verify before deploy.
- Sigstore/cosign enable keyless, transparency-logged signing; together with reproducibility they establish verifiable provenance (formalized by SLSA) against build-chain tampering.
🧪 Practice
- Sign a container image or file with
cosignand then verify it; then modify the artifact by one byte and show verification fails.- List three common sources of build nondeterminism and how you would eliminate each.
- Explain how reproducible builds plus signing together defend against a compromised build server.
- Interview-style: "Signing proves integrity and authenticity — why do we also want reproducible builds?" Hint: a signature proves the artifact you received is the one that was signed, but not what that artifact was actually built from.
Secrets Scanning in Repositories
Theory
A secret is any credential that grants access — an API key, database password, private key, cloud access token, or OAuth secret. The chronic problem is that secrets get hardcoded into source and then committed to version control, where they are dangerous out of all proportion: a repository is copied to every developer's machine and often pushed to a public host, so one leaked key can be scraped by automated bots within minutes and used to run up cloud bills, exfiltrate data, or pivot deeper. Secrets scanning is the automated detection of these credentials in code, commit history, and pipelines so they can be caught and revoked.
Scanners find secrets two ways, and good tools use both. Pattern matching uses regular expressions for known key formats (e.g., AWS keys start with AKIA), which is precise for recognizable shapes. Entropy analysis flags strings that are statistically random-looking — high Shannon entropy — because real secrets are random while ordinary code and prose are not; this catches generic tokens that match no known pattern. The output is a list of likely secrets with file and line, which the team must then treat as compromised.
The single most important operational rule trips people up: a leaked secret must be rotated (revoked and replaced), not just deleted from the code. Git history is permanent and distributed, so once a secret has been committed and pushed, you must assume it is captured forever and the only real fix is to invalidate the credential itself. The right place for secrets is never the repo but a dedicated secrets manager (Vault, cloud KMS/Secrets Manager) injected at runtime — and scanning as a pre-commit hook plus a CI gate stops the leak before it ever lands.
Examples
A pattern-plus-entropy scan finding and what each row means:
$ gitleaks detect
FINDING file: config/prod.py:14
rule: aws-access-key-id (pattern: AKIA[0-9A-Z]{16})
secret: AKIAIOSFODNN7EXAMPLE
FINDING file: deploy/run.sh:3
rule: generic-high-entropy (entropy 4.9 bits/char, looks random)
secret: xQ9f_3Lk82mZ...
ACTION REQUIRED: rotate both credentials; history rewrite alone is NOT enough.
The leak and the fix (load from the environment / a secrets manager, never literal):
# LEAKED: the key is in source → in every clone → in history forever
AWS_SECRET = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" # scanner flags this
import os
# Secret is injected at runtime from a secrets manager / env; repo stays clean
AWS_SECRET = os.environ["AWS_SECRET_ACCESS_KEY"] # nothing sensitive committed
# Pre-commit hook stops a secret before it is ever committed
repos:
- repo: https://github.com/gitleaks/gitleaks
hooks: [{ id: gitleaks }] # blocks the commit if a secret is detected
Key Takeaways
- Hardcoded secrets committed to version control are extremely dangerous: repos are copied and often published, and bots scrape leaked keys within minutes.
- Secrets scanners combine pattern matching (known key formats) with entropy analysis (statistically random strings) to catch both recognizable and generic secrets.
- A leaked secret must be rotated, not merely deleted — git history is permanent and distributed, so the credential itself must be invalidated.
- Keep secrets out of the repo entirely (use a secrets manager injected at runtime) and scan as both a pre-commit hook and a CI gate.
🧪 Practice
- Run a secrets scanner (e.g., gitleaks or trufflehog) over a repo's full history and report what it finds.
- Explain why entropy analysis catches secrets that pattern matching misses, and give an example string.
- Describe the exact remediation steps after a cloud key is found committed to a public repo, in the correct order.
- Interview-style: "A secret was committed yesterday and the commit was reverted today. Is the secret safe? Why or why not?" Hint: think about who may have cloned, forked, or scraped the repository in between.
Infrastructure as Code Security Scanning
Theory
Infrastructure as Code (IaC) means defining your servers, networks, databases, and permissions in declarative files (Terraform, CloudFormation, Kubernetes manifests, Helm charts) that are version-controlled and applied automatically, instead of clicking around a cloud console by hand. This is a huge win for reliability and repeatability, but it has a security consequence: a single misconfiguration in a template becomes a misconfiguration deployed everywhere the template is used, every time. The misconfiguration is now code — which is bad (it scales the mistake) and good (you can scan it, like any other code, before it is applied).
IaC security scanning statically analyzes these definition files against a library of policies encoding cloud security best practices, catching dangerous settings before deployment: a storage bucket set to public, a security group open to 0.0.0.0/0 on SSH, an unencrypted database, an over-permissive IAM role, a container running as root. It is essentially SAST for infrastructure — same shift-left logic, but the "vulnerability" is a risky resource configuration rather than a tainted data path. Because cloud breaches overwhelmingly stem from misconfiguration rather than exotic exploits, this class of scanning has outsized payoff.
Tools like Checkov, tfsec, KICS, and Terrascan run as a pipeline gate (and often pre-commit), and many support policy as code — writing your own rules (e.g., in Open Policy Agent's Rego) so organization-specific requirements ("all S3 buckets must have encryption and block public access") are enforced automatically. The mental model is a building-code inspector reviewing blueprints: cheaper and safer to reject an unsafe plan than to demolish a finished, occupied building.
Examples
A dangerous Terraform snippet and the scanner finding it produces:
resource "aws_s3_bucket" "data" {
bucket = "acme-customer-data"
acl = "public-read" # ← anyone on the internet can read objects
}
resource "aws_security_group_rule" "ssh" {
type = "ingress"
from_port = 22
to_port = 22
cidr_blocks = ["0.0.0.0/0"] # ← SSH exposed to the entire internet
}
$ checkov -d .
FAILED CKV_AWS_20 aws_s3_bucket.data S3 bucket allows public READ access
FAILED CKV_AWS_24 aws_security_group_rule.ssh Ingress 22 open to 0.0.0.0/0
→ gate fails: fix ACL to private and restrict CIDR before this can be applied.
Policy-as-code enforcing an org rule (OPA/Rego) in the pipeline:
package terraform.s3
# Deny any S3 bucket that is not configured to block public access.
deny[msg] {
bucket := input.resource.aws_s3_bucket[name]
not bucket.acl == "private"
msg := sprintf("S3 bucket '%s' must be private", [name])
}
Key Takeaways
- IaC defines infrastructure in version-controlled declarative files, so a single template misconfiguration scales to every deployment that uses it.
- IaC security scanning is SAST for infrastructure: it statically checks templates against best-practice policies and catches risky settings (public buckets, open SSH, unencrypted stores, over-broad IAM) before apply.
- Misconfiguration is the leading cause of cloud breaches, giving this scanning outsized value.
- Run it as a pipeline gate (and pre-commit); policy as code (e.g., OPA/Rego) lets you enforce organization-specific rules automatically.
🧪 Practice
- Run Checkov or tfsec against a Terraform/CloudFormation file and list every failed check with its severity.
- Fix the vulnerable S3 and security-group example above so both scanner findings pass.
- Write a policy-as-code rule that denies any database resource without encryption-at-rest enabled.
- Interview-style: "Why is scanning IaC often higher-value than scanning application code for preventing cloud breaches?" Hint: recall what actually causes most real-world cloud compromises.
Vulnerability Management and SLAs
Theory
No amount of prevention finds everything, so organizations need a process for the vulnerabilities that are discovered over time — in dependencies, in scans, from pen tests, from bug-bounty reports. Vulnerability management is that continuous lifecycle: identify (aggregate findings from all your scanners and reports), prioritize (decide what to fix first), remediate (fix, mitigate, or formally accept the risk), and verify (confirm the fix and watch for recurrence), then repeat forever. The point is that vulnerabilities arrive continuously — a dependency that was clean yesterday gets a new CVE today — so this is a running operation, not a project with an end date.
The teeth of the process are SLAs (Service Level Agreements): time limits for remediation tied to severity. A typical policy says critical vulnerabilities must be fixed within, say, 7 days, high within 30, medium within 90, and low on a best-effort basis. SLAs exist because without a deadline, security work is always deprioritized under feature pressure; a clock turns "we should fix that" into an accountable commitment with escalation when it is breached. This is where severity scoring (CVSS, next topic) feeds in directly: the score determines the severity band, and the band determines the SLA clock.
Two concepts keep the process honest. Risk acceptance acknowledges that some vulnerabilities will be formally accepted rather than fixed (too costly, not reachable, compensating controls exist) — but only through an explicit, documented, time-bounded decision by someone with authority, not silent inaction. And prioritization must consider exploitability and context, not raw severity alone: a critical CVE in a component that is not reachable or already mitigated may rank below a high one that is internet-facing. The mental model is a hospital emergency room: triage by severity, treat on a clock, document every decision, and keep the queue moving.
Examples
A severity-to-SLA policy and a worked queue:
SEVERITY REMEDIATION SLA ESCALATION IF BREACHED
Critical 7 days notify security lead + eng manager
High 30 days notify team lead
Medium 90 days tracked, reviewed monthly
Low best effort / backlog —
ID CVSS Severity Discovered Due Status Notes
V-101 9.8 Critical 2026-09-28 2026-10-05 In progress internet-facing API
V-102 7.5 High 2026-09-10 2026-10-10 Open internal only
V-103 9.1 Critical 2026-09-20 2026-09-27 ACCEPTED not reachable; WAF rule added,
risk accepted 90d by CISO
Risk acceptance as a structured record (so "not fixed" is still a decision, not a gap):
vuln: V-103
cvss: 9.1
decision: accept
justification: "Vulnerable code path is unreachable in our config; WAF blocks the vector."
compensating_control: "WAF rule WAF-882"
approved_by: "CISO"
expires: 2026-12-31 # must be re-reviewed, not accepted forever
Key Takeaways
- Vulnerability management is a continuous lifecycle — identify, prioritize, remediate, verify — because new vulnerabilities arrive constantly.
- SLAs set remediation deadlines by severity (e.g., critical in 7 days, high in 30), turning intent into accountable, escalatable commitments.
- Prioritization weighs exploitability and context (reachability, exposure, compensating controls), not raw severity alone.
- Risk acceptance is legitimate only as an explicit, documented, time-bounded decision by an authorized owner — never silent inaction.
🧪 Practice
- Draft a severity-to-SLA policy for your own team, with escalation rules for breaches.
- Given three findings (critical-but-unreachable, high-internet-facing, medium-internal), rank the remediation order and justify it beyond raw severity.
- Write a risk-acceptance record for a vulnerability you would not fix immediately, including a compensating control and an expiry.
- Interview-style: "Why would a team fix a high-severity, internet-facing bug before a critical one that is not reachable?" Hint: distinguish a vulnerability's intrinsic score from its actual exploitability in your environment.
CVSS Scoring and Prioritization
Theory
To run SLAs and prioritize consistently, you need an objective, shared way to rate how serious a vulnerability is — otherwise every team argues severity from scratch. The Common Vulnerability Scoring System (CVSS) is the industry-standard framework that produces a number from 0.0 to 10.0 (mapped to bands: None, Low, Medium, High, Critical) by scoring a vulnerability's characteristics. Its value is a common language: a "CVSS 9.8" means roughly the same thing to every security team in the world, which is why CVEs ship with CVSS scores and why SLA bands are defined in CVSS terms.
CVSS has three metric groups, and understanding the split is the key insight. The Base score (the one usually quoted) measures the intrinsic, context-free properties of the vulnerability — how it is attacked and how bad the impact is — and never changes. It is built from factors like attack vector (network/adjacent/local/physical), attack complexity, privileges required, user interaction, and the impact on confidentiality, integrity, and availability (plus scope — whether the flaw can affect components beyond the vulnerable one). The Temporal group adjusts for things that change over time (is there a known exploit? a patch?), and the Environmental group adjusts for your specific context (how critical is the affected asset, is it exposed?).
The most important practical lesson — and a very common interview point — is that the Base score alone should not drive prioritization. A CVSS 9.8 vulnerability in a lab tool with no network exposure is less urgent than a 7.5 on your public login service. Base CVSS tells you how bad the bug is in the abstract; you must apply Temporal and especially Environmental context (and signals like EPSS, which estimates exploitation probability, and CISA's KEV catalog of known-exploited CVEs) to turn a score into a real priority. Treat CVSS as the thermometer, not the diagnosis.
Examples
Reading a CVSS vector string (the compact encoding of the Base metrics):
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H → Base 9.8 (Critical)
AV:N Attack Vector = Network (exploitable remotely — worst case)
AC:L Attack Complexity = Low (no special conditions needed)
PR:N Privileges Required = None (unauthenticated)
UI:N User Interaction = None (no victim action needed)
S:U Scope = Unchanged
C:H /I:H /A:H Confidentiality/Integrity/Availability impact all High
Why Base score is not priority — same Base, different real urgency:
Vuln A Base 9.8 on an INTERNET-FACING auth service, public PoC exists (EPSS high)
Vuln B Base 9.8 on an INTERNAL build tool, no network path, no known exploit
Base score: A = B = 9.8
Real priority: A >> B ← Environmental (exposure) + Temporal (exploit) decide it
Fix A now; B can follow the normal SLA.
| Metric group | Captures | Changes with |
|---|---|---|
| Base | Intrinsic severity (attack + impact) | Never (fixed property of the bug) |
| Temporal | Exploit maturity, remediation level | Time (new exploits/patches) |
| Environmental | Importance and exposure of your asset | Your specific deployment |
Key Takeaways
- CVSS is the standard 0.0–10.0 severity framework (None→Critical bands) giving teams a common language; CVEs ship with it and SLA bands are defined by it.
- It has three groups: Base (intrinsic, never changes), Temporal (exploit/patch maturity over time), and Environmental (your asset's importance and exposure).
- The Base vector encodes attack vector, complexity, privileges, user interaction, scope, and CIA impact.
- Base score is not priority: combine it with Temporal/Environmental context and signals like EPSS and CISA KEV to decide what to fix first.
🧪 Practice
- Decode the vector
CVSS:3.1/AV:L/AC:H/PR:H/UI:R/S:U/C:L/I:L/A:Ninto plain English and estimate whether its Base is low or high.- Use an online CVSS calculator to score a hypothetical remote, unauthenticated RCE and compare it to a local, authenticated info-leak.
- Given two CVSS 8.1 vulnerabilities, describe what environmental information would make you fix one before the other.
- Interview-style: "Why is it a mistake to prioritize remediation purely by CVSS Base score?" Hint: think about what the Base score deliberately excludes and where EPSS/KEV and asset exposure come in.
<a id="11-cloud-and-infrastructure-security"></a>