---
key: "minimal-appsec-program"
title: "Start a minimal AppSec program with owners, triggers and metrics"
url: "https://softwaresecurity.com/frameworks/appsec-program/"
api: "https://softwaresecurity.com/api/v1/articles/minimal-appsec-program.json"
domain: "governance"
topic: "Governance \u0026 frameworks"
type: "Guide"
identifiers: []
tags: ["AppSec programs","Secure development lifecycle"]
stages: ["govern"]
roles: ["appsec-engineer","security-leader"]
license: "CC-BY-4.0"
license_url: "https://creativecommons.org/licenses/by/4.0/"
code_license: "MIT"
---

# Start a minimal AppSec program with owners, triggers and metrics

Begin with ownership, a few risk-triggered activities, and evidence that findings are resolved; expand from demonstrated needs.

Canonical URL: https://softwaresecurity.com/frameworks/appsec-program/

## In short

Start with one product, a named engineering owner and the few kinds of change that alter its exposure, such as authentication, tenant data, payments and release credentials. Give every confirmed finding an owner, a fix path and a closure rule that requires the fix to be running in production. Add scanners to the pipeline only where someone will triage the results, measure review coverage against the changes that should have been reviewed, and widen scope once that loop works.

- A program is an operating loop: owned inventory, change triggers, finding records, a release decision and verification of the deployed fix.
- Name who fixes, who verifies and who may accept risk. The security engineer should not silently become the risk owner.
- Close a finding when the fixed artifact runs in production, not when the pull request merges.
- Measure against the population that should have been covered: 9 reviewed of 10 triggered changes is 90%, whatever the total number of changes.
- NIST SSDF supplies practice vocabulary and OWASP SAMM a maturity model; neither tells a team which eight hours of work reduce risk most.

## Verify it

1. Pick three recent changes that touched authentication or tenant data and confirm each has a review record.
2. Take one closed finding and confirm the fixed version is the one running in production.
3. Confirm every service in scope has a named engineering owner and every exception has an expiry and a risk owner.
4. Recompute each program metric from its stated numerator and denominator.

## What you will learn

- Bound an AppSec pilot to a service with an owner and state what it does not cover.
- Fit detection and review work into a fixed security capacity.
- Decide which tools may block a build and route exceptions to the right owner.
- Measure review coverage against triggered changes, not all changes.

Audience: Engineering leads and the first security engineer starting an application security program

Before you start:

- Identify the engineering owners and deployment paths of a small service inventory.

Time: About 30 minutes: 7 to read and 23 for the exercise

## Where should a small program draw its boundary?

A small AppSec program needs a repeatable path from risk to engineering action. Starting with every repository and every control can produce an impressive spreadsheet without a team able to operate it. Start with a product whose important assets, release path and accountable owner can be named. Define which applications, environments and teams are inside the pilot and what remains outside. A partial but honest boundary is more useful than implied organization-wide coverage.

The fictional Willow company has three services and one security engineer with eight hours per week available for the pilot. The program starts with Billing, because its cross-tenant data boundary and weekly releases create a specific decision point. This does not mean the other services are safe. It means their assessment is pending and visible. NIST SSDF supplies a lifecycle practice vocabulary; OWASP SAMM supplies a maturity-improvement model. Neither tells Willow which eight hours of work will most reduce its current product risk. That prioritization needs the supplied exposure and ownership evidence. For choosing between those models, see /frameworks/bsimm-samm-ssdf/.

CISA’s Secure by Design principles describe the same accountability from the supplier’s side: take ownership of customer security outcomes, be transparent about vulnerabilities, and make security a leadership decision rather than a task delegated to one engineer. A pilot program is where those principles become specific people, triggers and records. The pledge goals and how to evidence them are in /frameworks/secure-by-design/.

## What is the smallest operating loop?

The baseline has five connected parts: an inventory owner, change triggers, an actionable finding record, a release decision path and a verification step. A scanner without an owner creates a queue; an owner without fix capacity creates an escalation loop; a closed ticket without deployment evidence creates a false success metric. Define the whole path for one important defect before scaling detection. Training should teach the patterns engineers actually encounter in that path.

Willow's pilot treats changes to tenant authorization, authentication, payment integration and release credentials as review triggers. Findings record affected artifact, abuse path, evidence, owner, due date, interim control and closure condition. A release-blocking defect goes to the engineering lead for a fix or to the designated business risk owner for a bounded exception; the security engineer does not silently become the person accepting commercial risk. The pilot's two-day response target is an internal policy choice, not a universal framework requirement. Make those local decisions explicit so teams can challenge their practicality.

## How do you record the program boundary as data?

Write the pilot's boundary, owners, review triggers and scanner rules in one file in a repository the engineering leads can read. Services outside the pilot are listed with the status `pending assessment` rather than left out, so nobody reads the file as a claim that they are safe. The file also names who approves exceptions, which keeps the security engineer from becoming the person who accepts business risk.

```yaml
# AppSec pilot boundary and operating loop, as data. Fictional company Willow.
capacity_hours_per_week: 8
services:
  billing:
    owner: omar
    in_scope: true
    data: [tenant invoices, payment references]
    review_triggers: [src/auth/, src/tenancy/, src/payments/, .github/workflows/release.yml]
    scanners:
      secrets: {gate: true, triage_owner: priya}
      dependencies: {gate: kev-only, triage_owner: omar}
      sast: {gate: false, triage_owner: omar}
  marketing:
    owner: gia
    in_scope: false
    status: pending assessment
  analytics:
    owner: null
    in_scope: false
    status: pending assessment
exceptions:
  approver: tess   # business risk owner, not the security engineer
  max_days: 30
```

## What should run at each delivery stage first?

The table is a starting point for a small team, not a framework requirement. Each row pairs one practice with the evidence that shows it ran, so the pilot can tell a configured tool from a working one.

Let a tool fail the build only for results the team has agreed to fix at once, such as a verified live secret or a dependency vulnerability listed in CISA’s Known Exploited Vulnerabilities catalog. Route everything else into the finding queue until its false-positive rate is known; a gate that blocks on noise teaches engineers to bypass it.

| Stage | Start with | Evidence that it runs |
| --- | --- | --- |
| Design | A short threat-model discussion for changes that meet a review trigger | A dated note naming the trust boundary and the control chosen |
| Code review | Peer review on every change, plus a security reviewer for triggered changes | Review record on the merged change |
| Build | Secret scanning on push, dependency scanning against the lockfile, static analysis on changed code | Results attached to the build that produced the artifact |
| Release | An immutable artifact identity and an SBOM for each release | Artifact digest and SBOM stored with the release record |
| Operate | A published vulnerability contact (`security.txt`) and dependency alerts routed to the owning team | Intake log and alert routing rules |
| Respond | Finding states from reported to verified closed, each with an owner and due date | Finding records that name the deployed fixed version |

## How does one finding move from report to closure?

Finding F17 says Billing's export endpoint reads an invoice by ID without restricting the lookup to the requester's tenant. The supplied negative test retrieves another tenant's invoice in release B4. This is a demonstrated boundary failure, not simply a scanner severity label. The fix must change the server-side lookup (/fix/missing-authorization/ shows the pattern per stack), preserve legitimate export behavior and reach production. Hiding the export button or using unguessable IDs would not remove the missing authorization check.

In the fictional record, engineer Lena owns implementation, engineering lead Omar owns delivery, security reviewer Priya owns verification advice, and business owner Tess owns any proposed exception. Evidence E17 is the failing request/response pair; E18 is the code change; E19 is the regression result; E20 identifies the deployed artifact. Only the combination of a relevant passing test and correct deployment supports closure. These are assigned roles for the example, not real people's approvals. A policy document that assigns everyone as responsible would not identify who can make the next decision.

## Which pilot metrics show that the loop works?

Useful pilot measures reveal whether the operating loop works. Define the eligible population and the timestamp for each before calculating it. Nine reviewed pull requests out of ten triggered changes is 90% coverage, even if 100 pull requests were merged in total; calling it 9% or 100% would answer different questions or hide the missed trigger.

The pilot must also track workload and friction. Eight hours cannot support arbitrary manual review of every small change. Reusable authorization tests and focused design questions can improve coverage without making one security engineer the release bottleneck. Review the missed change to learn why the trigger failed, then choose one improvement. A growing finding count after introducing a new test may mean better discovery, not worsening engineering. Expand scope only when evidence shows the initial team can consistently triage, fix, verify and learn.

- Ownership: services in scope with a named engineering owner, divided by services in scope.
- Review coverage: triggered changes with a review record, divided by changes that met a trigger.
- Action: confirmed findings with an owner and due date within two days, divided by findings confirmed in the period.
- Closure: findings whose fix is verified in the deployed artifact, divided by findings due in the period.

## How do you check ownership and review coverage automatically?

A short script turns the file into the first two measures: services without an owner, and the share of changes that met a review trigger and got a security review. Run it weekly over the merged pull requests. With the sample data it reports that Analytics has no engineering owner and that four of the five triggered changes were reviewed; the missed one, PR 104, changed the payments webhook. That is the change to investigate: was the trigger seen and skipped, or was the review request lost? Tested with Python 3.14 and PyYAML 6.0.3.

```python
"""Check an AppSec pilot's inventory and compute trigger-review coverage.

Usage: python program_check.py program.yaml changes.csv
"""
import csv
import sys

import yaml


def inventory_problems(program):
    for name, svc in program["services"].items():
        if not svc.get("owner"):
            yield f"{name}: no engineering owner"
        if svc.get("in_scope"):
            if not svc.get("review_triggers"):
                yield f"{name}: in scope but no review triggers"
            for scanner, cfg in svc.get("scanners", {}).items():
                if not cfg.get("triage_owner"):
                    yield f"{name}: {scanner} results have no triage owner"
        elif svc.get("status") != "pending assessment":
            yield f"{name}: out of scope without a stated status"


def coverage(program, changes_path):
    triggered = reviewed = 0
    missed = []
    with open(changes_path, newline="", encoding="utf-8") as f:
        for row in csv.DictReader(f):
            triggers = program["services"][row["service"]].get("review_triggers", [])
            if any(p.startswith(t) for p in row["paths"].split(";") for t in triggers):
                triggered += 1
                if row["security_review"] == "yes":
                    reviewed += 1
                else:
                    missed.append(row["pr"])
    return triggered, reviewed, missed


if __name__ == "__main__":
    program = yaml.safe_load(open(sys.argv[1], encoding="utf-8"))
    for problem in inventory_problems(program):
        print("INVENTORY", problem)
    triggered, reviewed, missed = coverage(program, sys.argv[2])
    print(f"Trigger-review coverage: {reviewed}/{triggered} triggered changes reviewed; missed PRs: {', '.join(missed) or 'none'}")
```

## Guided practice

Willow, a fictional company, gives its first security engineer eight hours a week for a four-week AppSec pilot. Choose the scope, fit the work into the hours, decide which tools may block a build, measure the first week and route the first exception request.

### Supplied material

```text
Services: Billing (tenant invoices and payments, weekly releases, owner Omar); Marketing (public content site, owner Gia); Analytics (internal batch jobs, no owner).
Candidate activities with estimated weekly effort:
  A Review every pull request in all three services: 20 h
  B Trigger-based review for Billing (auth, tenancy, payments, release workflow): 3 h
  C Secret scanning with push protection on all repositories: 0.5 h
  D Dependency scanning on Billing, blocking only KEV-listed vulnerabilities: 1 h
  E SAST on all repositories, blocking on any High finding: 6 h of triage; trial run showed 70% false positives
  F Cross-tenant authorization regression tests for Billing exports: 2 h for the first two weeks
Week 1 Billing pull requests: 101 src/auth/session.py (reviewed); 102 docs/readme.md (not reviewed); 103 src/tenancy/export.py (reviewed); 104 src/payments/webhook.py (not reviewed); 105 src/reports/csv.py (not reviewed); 106 .github/workflows/release.yml (reviewed); 107 src/auth/mfa.py (reviewed)
Week 2 request from Omar: ship release B6 although its dependency scan blocks on a KEV-listed library with no fixed version yet; he proposes a WAF rule. Business risk owner: Tess.
```

### 1. Choose and bound the scope

Pick the pilot service and state what the program does not yet claim.

Worked answer: Billing, because it holds tenant data and payments and has a named owner. Marketing and Analytics are listed as pending assessment, not as safe; Analytics also needs an owner, which is an inventory action for engineering leadership, not for security.

### 2. Fit the eight hours

Choose activities from A to F within the weekly capacity.

Worked answer: B, C, D and F: 3 + 0.5 + 1 + 2 = 6.5 hours in weeks 1 and 2, and 4.5 hours afterwards, leaving room for triage. A needs 20 hours and cannot be done. E would consume most of the time on noise.

### 3. Decide what may block a build

Which of the chosen tools should fail the build, and what happens to SAST?

Worked answer: Secret scanning and the KEV-only dependency rule can block, because each result is something the team agrees to fix at once. SAST should not block while 70% of its findings are false positives; route its results to the queue, tune the rules, and revisit once the false-positive rate is known.

### 4. Measure week 1

Compute trigger-review coverage and name the follow-up.

Worked answer: Triggered changes are 101, 103, 104, 106 and 107; four were reviewed, so coverage is 4/5 = 80%. PR 102 and 105 did not meet a trigger and are not in the denominator. Investigate why PR 104 (payments webhook) was missed.

### 5. Route the exception request

Who decides on Omar's request, and what must the request contain?

Worked answer: Tess decides, as the business risk owner; the security engineer advises. The request needs the scenario, the affected release, a test showing the WAF rule blocks the exploit path, an expiry no longer than the policy allows (30 days) and reopening triggers such as a fix becoming available. Until Tess approves, the release stays blocked.

Deliverable: A one-page pilot charter: scope and exclusions, the chosen activities with hours, the build-blocking rules, the week-1 coverage figure with its follow-up, and the exception route.

Scope: Willow, its people and the effort estimates are fictional. The eight-hour budget and the 30-day exception limit are local choices, not framework requirements.

Articles and teaching material: CC BY 4.0. Original executable code: MIT. Attribution: Software Security.

## Sources

- [NIST SP 800-218: SSDF 1.1, final](https://csrc.nist.gov/pubs/sp/800/218/final). Version 1.1 (February 2022).
- [OWASP SAMM version 2 model](https://owaspsamm.org/model/)
- [CISA and partners: Shifting the Balance of Cybersecurity Risk, Secure by Design principles](https://www.cisa.gov/resources-tools/resources/secure-by-design). Revised 25 October 2023.
- [CISA: Known Exploited Vulnerabilities Catalog](https://www.cisa.gov/known-exploited-vulnerabilities-catalog)