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.
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.
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.
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:
auth/, api/, middleware/).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:
DAST-NEW- prefix in the issue tracker (e.g., DAST-NEW-001).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:
vulnerabilities array of objects with id, title, severity, cvss, fixAvailable, and references.dependencies list with name, version, latest, and upgradePath.The pipeline fails if:
devDependencies but not in dependencies.Once deployed, applications are monitored for security events through centralised logging and alerting.
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:
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"
]
}
}
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:
changelog-entry of the form: ## [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
The AppSec Program is sustained through regular review and feedback loops.
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.
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.
ZAP baseline report shows 503 Service Unavailable for /api/health, but the service is not yet ready.RASP_ALERT_ON_DATA_LEAKAGE is set to true, but the agent does not log data-leakage events — because the configuration key is dataLeakage in the JSON, not data_leakage.details field is null, not {}, and related_events is an empty list instead of [].psql query uses staging but the environment is preprod.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.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy