Application Security Program

Production-grade guide to application security program covering architecture patterns, implementation strategies, testing approaches, and operational best practices for enterprise engineering teams.

An Application Security Program (AppSec Program) is a structured, ongoing effort to identify, prevent, and respond to security risks across an organisation’s software applications. It matters when applications are the primary attack surface, when teams deploy frequently, and when security is not just a checklist but a shared responsibility. This page details how to build and operate an AppSec Program using real tools and practices that work across diverse engineering environments.

Establishing the AppSec Program Foundation

The AppSec Program begins with defining roles, responsibilities, and expectations. The core team — typically security engineers, application developers, DevOps engineers, and product owners — agrees on a shared understanding of security risks and processes. This foundation is captured in a Security Charter document, versioned in Git, with a canonical security-charter.md at the root of the monorepo.

The charter defines:

The charter is reviewed quarterly, and changes require a pull request with at least one approval from a security engineer and one from a product lead.

Integrating Security into Development Workflows

Security is embedded in the development lifecycle through automated security gates in the CI/CD pipeline. Each pull request triggers a security scan pipeline, defined in ci/security.yml using GitHub Actions or GitLab CI.

Static Application Security Testing (SAST)

For every PR, the pipeline runs SAST using CodeQL and SonarQube.

- name: Run SAST with CodeQL
  uses: github/codeql-action/init@v2
  with:
    languages: javascript,python,go
    config-file: .codeql/config.yml
    # Config file: .codeql/config.yml
    #   queries: 
    #     - security-and-quality
    #     - custom/security-queries
    #   filters: 
    #     - path: "src/"
    #     - path: "packages/"

CodeQL generates a codeql-results.sarif file, which is uploaded to the PR and used to annotate code. The pipeline fails if:

Dynamic Application Security Testing (DAST)

After deployment to a staging environment, the pipeline runs OWASP ZAP (Zed Attack Proxy) in automated scanning mode.


docker run --rm -v $(pwd)/zap-results:/zap -w /zap \
  owasp/zap2docker-stable \
  zap-baseline.py \
    -t https://staging.example.com \
    -d \
    -J zap-results/baseline-report.json \
    -r zap-results/baseline-report.html \
    -o zap-results/baseline-report.xml

The DAST report is parsed and evaluated:

Dependency Scanning

The pipeline uses Snyk to scan dependencies for known vulnerabilities.

- name: Run Snyk Vulnerability Scan
  run: |
    npm install snyk
    snyk test --file=package.json --json --dev
    snyk monitor --file=package.json

Snyk outputs a snyk-results.json file with:

The pipeline fails if:

Managing Security in Production

Once deployed, applications are monitored for security events through centralised logging and alerting.

Runtime Application Self-Protection (RASP)

The application runtime includes RASP agents that detect and respond to threats in real time. The agent is configured via environment variables:

RASP_ENABLED=true
RASP_LOG_LEVEL=debug
RASP_ALERT_ON_SQL_INJECTION=true
RASP_ALERT_ON_XSS=true
RASP_ALERT_ON_SESSION_HIJACKING=true
RASP_ALERT_ON_RATE_LIMIT_BREACH=true
RASP_ALERT_ON_DATA_LEAKAGE=true
RASP_THREAT_FEED_URL=https://security.example.com/threats

The agent logs structured JSON events to stdout:

{
  "timestamp": "2025-04-05T12:34:56.789Z",
  "event": "threat-detected",
  "threat": "sql-injection",
  "source": "api/user/login",
  "payload": {
    "input": "username=admin'--&password=123",
    "query": "SELECT * FROM users WHERE username = ? AND password = ?",
    "parameters": ["admin'--", "123"],
    "detected": true,
    "confidence": 0.94
  },
  "context": {
    "ip": "192.168.1.100",
    "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)",
    "session_id": "abc123xyz"
  }
}

These events are shipped via Fluent Bit to Elasticsearch, where alerts are triggered based on:

Security Incident Response

When a security alert is triggered, the system creates a Security Incident Record (SIR) in a shared database:

CREATE TABLE security_incidents (
  id UUID PRIMARY KEY,
  title VARCHAR(255) NOT NULL,
  severity VARCHAR(10) CHECK (severity IN ('low', 'medium', 'high', 'critical')),
  status VARCHAR(20) DEFAULT 'new',
  created_at TIMESTAMPTZ NOT NULL,
  updated_at TIMESTAMPTZ NOT NULL,
  details JSONB,
  related_events UUID[],
  assigned_to UUID REFERENCES users(id)
);

The incident is assigned to a responder who:

The responder updates the SIR via a PATCH request:

PATCH /incidents/7c1b5f3a-2d4e-6f8a-0b1c-2d3e4f5a6b7c HTTP/1.1
Host: security.example.com
Content-Type: application/json

{
  "status": "triaged",
  "details": {
    "root_cause": "SQL injection in login endpoint due to missing input sanitization",
    "impact": {
      "affected_users": 4321,
      "affected_systems": ["auth-service", "user-db"],
      "downtime_estimate": "30 minutes"
    },
    "mitigation": [
      "Apply rate limiting (100 reqs/min) to /login",
      "Update all user login handlers to use prepared statements",
      "Add WAF rule to block common SQLi patterns"
    ]
  }
}

Patching and Vulnerability Management

A monthly vulnerability prioritisation meeting reviews the state of the application portfolio. The meeting uses a vulnerability dashboard built from data in a security inventory database.

The database schema includes:

CREATE TABLE application_vulnerabilities (
  id UUID PRIMARY KEY,
  application_name VARCHAR(255) NOT NULL,
  vulnerability_id VARCHAR(255) NOT NULL,
  severity VARCHAR(10),
  cvss_score DECIMAL(3,2),
  reported_at TIMESTAMPTZ,
  fixed_at TIMESTAMPTZ,
  status VARCHAR(20) DEFAULT 'open',
  assigned_to UUID REFERENCES users(id),
  notes TEXT,
  remediation_plan JSONB
);

The dashboard surface:

Patch decisions are made based on:

A patch is approved when:

  ## [Security] Fix SQL Injection in User Login

  - Fixes CVE-2025-12345
  - Applies to all apps in the `user` module
  - Includes automated tests and RASP rule update
  - Backward compatible with v1.2.0 and earlier

Maintaining Program Health

The AppSec Program is sustained through regular review and feedback loops.

Quarterly Security Reviews

Each quarter, the security team runs a Security Review using a checklist defined in reviews/quarterly-review.yml:

- section: "SAST & DAST"
  checks:
    - name: "All PRs pass SAST"
      expected: "100% of PRs have SAST results with no new critical issues"
      query: "SELECT COUNT(*) FROM prs WHERE sast_status = 'passed' AND created_at >= '2025-01-01'"
    - name: "DAST coverage"
      expected: "80% of public endpoints scanned in staging"
      query: "SELECT COUNT(DISTINCT endpoint) FROM zap_scans WHERE environment = 'staging'"
    - name: "Dependency vulnerability backlog"
      expected: "Less than 50 high-critical vulnerabilities open"
      query: "SELECT COUNT(*) FROM vulnerabilities WHERE severity IN ('high', 'critical') AND fixed_at IS NULL"

- section: "Production Security"
  checks:
    - name: "RASP alerts monitored"
      expected: "All RASP events processed within 30 minutes"
      query: "SELECT COUNT(*) FROM rasp_alerts WHERE processed_at IS NULL"
    - name: "Incident response SLA met"
      expected: "80% of incidents resolved within 72 hours"
      query: "SELECT AVG(extract(epoch from (resolved_at - created_at))) FROM incidents WHERE status = 'closed'"

The review is run via a script:

#!/bin/bash
# run-quarterly-review.sh
set -e

echo "Running quarterly security review..."

for section in $(yq e '.section | keys | .[]' reviews/quarterly-review.yml); do
  echo "## $section"
  for check in $(yq e ".section | .[\"$section\"] | .checks | .[].name" reviews/quarterly-review.yml); do
    expected=$(yq e ".section | .[\"$section\"] | .checks | .[] | select(.name == \"$check\") | .expected" reviews/quarterly-review.yml)
    query=$(yq e ".section | .[\"$section\"] | .checks | .[] | select(.name == \"$check\") | .query" reviews/quarterly-review.yml)

    result=$(psql -t -c "$query" -d security_db)
    if [[ "$result" == "$expected" ]]; then
      echo "✅ $check: $result"
    else
      echo "❌ $check: expected $expected, got $result"
    fi
  done
done

The script is run daily via a cron job and sends a report to the security team.

Security Training and Knowledge Sharing

Each quarter, every engineer completes one security training module from a curated list:

Training is tracked in a training/ directory with a trainings.csv file:

id,topic,author,completed,pr_link
1,"OWASP Top 10",[email protected],true,https://github.com/org/appsec/pull/45
2,"RASP Configuration",[email protected],false,https://github.com/org/appsec/pull/67

The program tracks completion and identifies gaps, such as low completion rates in API security or incident response.

Key Failure Modes and Sharp Edges

These patterns, when executed consistently, turn security from an event into a continuous, organisational capability.

This page was rewritten on 16 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.