Feature Flags as Evidence: Turning Release Toggles into SOC 2 & PCI DSS Controls Your Auditors Will Love
Most teams already use feature flags, kill switches, and progressive delivery to ship safer changes. The missed opportunity is this: those same flags can double as change management, least privilege, and rollback evidence for SOC 2 and PCI DSS—if you design them that way.
This guide shows how engineering teams can turn feature flags as evidence into a first-class pattern:
- Every flag change becomes a traceable change record (SOC 2 CC8, PCI DSS Req. 6).
- Targeting rules reflect least privilege and access control (SOC 2 CC6, PCI DSS Req. 7).
- Progressive rollouts and kill switches produce rollback and logging proof (SOC 2 CC7, PCI DSS Req. 10).
- Each rollout automatically triggers a lightweight risk assessment using your CI tools plus the free Website Vulnerability Scanner from Pentest Testing Corp (free.pentesttesting.com).

Along the way, we’ll plug into Pentest Testing Corp’s Risk Assessment Services and Remediation Services so your auditors and customers get formal, external evidence on top of your in-house telemetry.
1. Model feature flag changes as structured evidence
To treat feature flags as evidence, you first need a consistent event model for flag changes.
Think in terms of “mini change records”:
- Who changed what flag?
- In which environment?
- With which approvals?
- Linked to which ticket/risk item?
- With which rollback plan?
TypeScript model for feature flag evidence
// feature-flag-evidence.ts
export type Framework = 'soc2' | 'pci' | 'iso27001' | 'hipaa' | 'gdpr';
export interface ControlRef {
framework: Framework;
id: string; // e.g. "CC8.1", "PCI-6.5.1"
description?: string;
}
export interface FeatureFlagChange {
id: string; // uuid
flagKey: string; // e.g. "payments.strong-auth"
env: 'dev' | 'staging' | 'prod';
previousValue: string | null;
newValue: string;
changeType: 'create' | 'update' | 'delete';
changedBy: string; // user id / email
approvals: string[]; // approver ids
ticketId: string; // Jira / Azure DevOps / Linear
riskSummary: string; // short text summary
createdAt: string; // ISO timestamp
rollbackPlan: string; // link or short description
controls: ControlRef[]; // mapped SOC 2 / PCI DSS controls
evidenceArtifacts: string[]; // paths/URLs to reports, screenshots, logs
}
Now, every time a flag changes, you emit one of these events into your log or evidence store.
Node.js helper to emit flag evidence events
// log-flag-change.ts
import { FeatureFlagChange, ControlRef } from './feature-flag-evidence';
import { randomUUID } from 'crypto';
import fs from 'fs';
import path from 'path';
const EVIDENCE_DIR = process.env.EVIDENCE_DIR || 'evidence/flags';
function mapControls(flagKey: string): ControlRef[] {
// Simple example – tune for your org
const controls: ControlRef[] = [
{ framework: 'soc2', id: 'CC8.1', description: 'Change management' },
{ framework: 'soc2', id: 'CC7.2', description: 'Monitoring & incidents' },
{ framework: 'pci', id: '6.5', description: 'Secure development' },
];
if (flagKey.includes('admin') || flagKey.includes('role')) {
controls.push(
{ framework: 'soc2', id: 'CC6.1', description: 'Access control' },
{ framework: 'pci', id: '7.1', description: 'Access to cardholder data' },
);
}
return controls;
}
export async function logFeatureFlagChange(input: Omit<FeatureFlagChange, 'id' | 'createdAt' | 'controls'>) {
const event: FeatureFlagChange = {
...input,
id: randomUUID(),
createdAt: new Date().toISOString(),
controls: mapControls(input.flagKey),
};
const day = event.createdAt.slice(0, 10); // yyyy-mm-dd
const file = path.join(EVIDENCE_DIR, `${day}.jsonl`);
fs.mkdirSync(path.dirname(file), { recursive: true });
fs.appendFileSync(file, JSON.stringify(event) + '\n');
console.log('[flag-evidence] wrote', event.id, 'to', file);
}
Call logFeatureFlagChange() from your feature flag management layer (or webhooks from your flag provider). Now you have append-only, queryable evidence proving:
- Changes were approved and ticketed.
- Controls like SOC 2 CC8 and PCI change requirements are actively enforced, not just documented.
2. Wire flag approvals into CI/CD and tickets
Next step: tie feature flag changes directly into your CI/CD pipeline and ticketing system so approvals and risk context are fully traceable.
Example: GitHub Action to validate required approvals per flag
Assume each deployment PR sets a list of flags being modified via labels or a simple YAML metadata file.
# .github/flag-changes.yaml (checked into the PR branch)
flags:
- key: "payments.strong-auth"
env: "prod"
ticketId: "SEC-1234"
approvers:
- "[email protected]"
- "[email protected]"
riskSummary: "Enforce MFA for high-value payments"
GitHub Action to enforce approvals and log feature flags as evidence:
# .github/workflows/feature-flag-evidence.yml
name: feature-flag-evidence
on:
pull_request:
types: [opened, synchronize, reopened, closed]
jobs:
validate-flags:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Load flag changes
id: flags
run: |
test -f .github/flag-changes.yaml || echo "flags: []" > .github/flag-changes.yaml
echo "flags=$(yq -o=json '.flags' .github/flag-changes.yaml)" >> "$GITHUB_OUTPUT"
- name: Validate approvals and ticket
run: |
node scripts/validate-flags.js '${{ steps.flags.outputs.flags }}'
And a simple validator:
// scripts/validate-flags.ts
import { logFeatureFlagChange } from '../flag-evidence/log-flag-change';
const [,, raw] = process.argv;
const flags = JSON.parse(raw || '[]');
function requireValue<T>(val: T | undefined, msg: string): T {
if (!val) {
console.error(msg);
process.exitCode = 1;
throw new Error(msg);
}
return val;
}
(async () => {
const actor = process.env.GITHUB_ACTOR!;
const sha = process.env.GITHUB_SHA!;
const env = process.env.DEPLOY_ENV || 'staging';
for (const flag of flags) {
requireValue(flag.ticketId, `Flag ${flag.key} missing ticketId`);
if (!Array.isArray(flag.approvers) || flag.approvers.length === 0) {
console.error(`Flag ${flag.key} missing approvers`);
process.exitCode = 1;
}
await logFeatureFlagChange({
flagKey: flag.key,
env,
previousValue: flag.previousValue ?? null,
newValue: flag.newValue ?? 'enabled',
changeType: flag.changeType ?? 'update',
changedBy: actor,
approvals: flag.approvers,
ticketId: flag.ticketId,
riskSummary: flag.riskSummary ?? '',
rollbackPlan: `Rollback via flag toggle in release ${sha}`,
evidenceArtifacts: [], // we’ll enrich this later
});
}
})();
Result: every flag change is:
- Linked to a ticket + approvers (SOC 2 CC8.1 change management, PCI DSS Req. 6).
- Stored in an evidence file per day or per release.
- Validated at CI time, not as an afterthought.
3. Use feature flags to enforce least privilege
Feature flags are also a powerful way to demonstrate least privilege and role-based access for SOC 2 CC6 and PCI DSS Req. 7.
Instead of treating flags as simple on/off switches, encode the who into your evaluation logic and logs.
Role-aware flag evaluation (Node/TypeScript)
// flag-eval.ts
interface UserContext {
userId: string;
roles: string[];
orgId: string;
isPrivileged: boolean;
}
interface FlagRule {
key: string;
allowedRoles: string[]; // e.g. ["admin", "support"]
environments: string[]; // ["staging", "prod"]
}
function isFlagEnabled(
flag: FlagRule,
user: UserContext,
env: 'dev' | 'staging' | 'prod'
): boolean {
if (!flag.environments.includes(env)) return false;
if (user.isPrivileged) return true;
return user.roles.some(role => flag.allowedRoles.includes(role));
}
// usage
const flag: FlagRule = {
key: 'billing.refund-portal',
allowedRoles: ['billing-admin', 'support-level2'],
environments: ['staging', 'prod'],
};
const user: UserContext = {
userId: 'u-123',
roles: ['support-level2'],
orgId: 'org-9',
isPrivileged: false,
};
const enabled = isFlagEnabled(flag, user, 'prod');
Now log evaluations for high-risk flags:
function logFlagEvaluation(flagKey: string, user: UserContext, env: string, enabled: boolean) {
console.log(JSON.stringify({
type: 'flag_eval',
flagKey,
env,
userId: user.userId,
roles: user.roles,
orgId: user.orgId,
enabled,
ts: new Date().toISOString(),
controls: ['SOC2-CC6.x', 'PCI-7.x'],
}));
}
In an audit, you can show:
- Only specific roles ever see the flagged feature.
- Access is enforced in code, not just in a policy document.
- Logs are searchable by control IDs.
4. Turn progressive delivery & kill switches into rollback evidence
Progressive delivery + kill switches already help you manage risk. With a small amount of structure, they become rollback evidence for SOC 2 CC7 and PCI DSS logging requirements.
Example: percentage rollout with automatic kill switch
// rollout-manager.ts
interface RolloutConfig {
flagKey: string;
env: 'staging' | 'prod';
percentage: number; // 0–100
errorRateThreshold: number; // e.g. 1.5 (%)
}
async function getCurrentErrorRate(service: string): Promise<number> {
// call Prometheus / Datadog / NewRelic API here
return 0.8; // mock
}
async function applyRollout(config: RolloutConfig) {
const rate = await getCurrentErrorRate('payments-api');
const eventBase = {
flagKey: config.flagKey,
env: config.env,
ts: new Date().toISOString(),
controls: ['SOC2-CC7.2', 'PCI-10.x'],
};
if (rate > config.errorRateThreshold) {
// kill switch
console.log(JSON.stringify({
...eventBase,
type: 'flag_killswitch',
reason: `errorRate ${rate}% > threshold ${config.errorRateThreshold}%`,
}));
await setFlagPercentage(config.flagKey, config.env, 0);
return;
}
console.log(JSON.stringify({
...eventBase,
type: 'flag_rollout_step',
newPercentage: config.percentage,
}));
await setFlagPercentage(config.flagKey, config.env, config.percentage);
}
async function setFlagPercentage(flagKey: string, env: string, pct: number) {
// Call feature flag provider API here
console.log(`[flags] ${flagKey} in ${env} set to ${pct}%`);
}
For each rollout wave, you get:
- A rollout step event with control IDs.
- A kill-switch event if metrics go bad.
Auditors can follow the complete story for a risky change: “We tested at 5%, 25%, 50%. At 50% we saw a spike, so the kill switch automatically set the flag to 0% and the deployment was rolled back.”
5. Attach SAST/DAST/IaC and free website scans to each flag rollout
To fully operationalize feature flags as evidence, attach automated testing around each high-risk flag:
- SAST / SCA / secrets scanning.
- DAST or integration tests.
- IaC and configuration scans.
- Free external web scan via the Website Vulnerability Scanner from Pentest Testing Corp.
This gives you a mini risk assessment per rollout that can be formalized later via Pentest Testing Corp’s Risk Assessment Services and Remediation Services pages.
GitHub Actions: gate rollout on tests + website scan
# .github/workflows/flag-rollout.yml
name: flag-rollout
on:
workflow_dispatch:
inputs:
env:
required: true
type: choice
options: [staging, prod]
flagKey:
required: true
type: string
jobs:
rollout:
runs-on: ubuntu-latest
env:
DEPLOY_ENV: ${{ inputs.env }}
FLAG_KEY: ${{ inputs.flagKey }}
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: npm ci
- name: Run SAST / SCA
run: npm run lint:sec && npm run test:sec
- name: Run IaC checks
run: npm run iac:scan
- name: Run integration / DAST tests
run: npm run test:e2e
- name: Run external website scan (CLI wrapper)
run: |
node scripts/run-website-scan.js \
--url "https://app.example.com" \
--output "artifacts/webscan-${{ inputs.env }}.json"
- name: Collect evidence manifest
run: |
node scripts/build-evidence-manifest.js \
--flag "$FLAG_KEY" \
--env "$DEPLOY_ENV" \
--scan "artifacts/webscan-${{ inputs.env }}.json" \
--out "artifacts/evidence-${{ inputs.env }}.json"
- name: Upload evidence artifacts
uses: actions/upload-artifact@v4
with:
name: flag-evidence-${{ inputs.env }}
path: artifacts/
- name: Apply rollout
run: node scripts/apply-rollout.js "$FLAG_KEY" "$DEPLOY_ENV"
Build an “evidence manifest” for the rollout
// scripts/build-evidence-manifest.ts
import { FeatureFlagChange } from '../flag-evidence/feature-flag-evidence';
import fs from 'fs';
interface Args {
flag: string;
env: string;
scan: string;
out: string;
}
function parseArgs(): Args {
const argv = process.argv.slice(2);
const args: any = {};
for (let i = 0; i < argv.length; i += 2) {
args[argv[i].replace(/^--/, '')] = argv[i + 1];
}
return args as Args;
}
const args = parseArgs();
const manifest = {
type: 'flag_rollout_evidence',
flagKey: args.flag,
env: args.env,
createdAt: new Date().toISOString(),
pipeline: {
runId: process.env.GITHUB_RUN_ID,
repo: process.env.GITHUB_REPOSITORY,
sha: process.env.GITHUB_SHA,
},
tests: {
sast: 'pass',
iac: 'pass',
dast: 'pass',
},
artifacts: {
websiteScanReport: args.scan,
// You can add screenshot paths here
},
controls: [
'SOC2-CC7.x',
'SOC2-CC8.1',
'PCI-6.x',
'PCI-10.x',
],
};
fs.mkdirSync('artifacts', { recursive: true });
fs.writeFileSync(args.out, JSON.stringify(manifest, null, 2));
console.log('Wrote evidence manifest to', args.out);
This manifest can be:
- Attached to a change ticket.
- Exported as part of your SOC 2 or PCI DSS evidence binder.
- Handed off to Pentest Testing Corp as input to a deeper Risk Assessment or Remediation engagement.
6. Mapping feature flag telemetry to SOC 2 & PCI DSS controls
Here’s how feature flags as evidence line up with common control objectives:
| Evidence source | SOC 2 examples | PCI DSS 4.x examples |
|---|---|---|
| Flag change events + approvals | CC8.1 (change mgmt) | Req. 6 (change mgmt / SDLC) |
| Flag evaluation logs w/ roles & orgs | CC6.x (access control) | Req. 7 (access control) |
| Rollout + kill-switch logs | CC7.2 (incident detection/response) | Req. 10 (logging & monitoring) |
| CI/CD evidence manifest per rollout | CC7.x, CC8.x, CC9.x (ops & risk) | Req. 6 & 10 (secure dev + logging) |
| Website scanner + SAST/DAST findings per flag | CC7.x (vuln mgmt) | Req. 11 & 12 (testing & risk reporting) |
The same artifacts also help with:
- ISO 27001 Annex A (A.12 operations security, A.14 secure development).
- HIPAA technical safeguards around change and access.
- GDPR Art. 25/32 (data protection by design and by default).
7. Using the free Website Vulnerability Scanner as an external signal
Your internal telemetry shows how your CI/CD and flags behave. Auditors also like external signals: what does the outside world see?
Screenshot of our free Website Vulnerability Scanner interface

This visual reinforces that:
- Every internet-facing change can have a quick external scan attached.
- The scan URL can be added to the
evidenceArtifactsarray in yourFeatureFlagChangeevents.
Sample report from our free scanner to check Website Vulnerability

In your evidence manifest, store:
- The raw JSON output for machine-readable tracking.
- The PDF/report and screenshot path for auditors.
Those artifacts become part of the change record for the relevant feature flag—especially useful for PCI DSS and SOC 2 evidence packs.
8. When to pull in Pentest Testing Corp for formal evidence
Once your team has basic feature flags as evidence wired into CI/CD, you can layer on formal assessments from Pentest Testing Corp:
- Use their Risk Assessment Services to review your change and rollout patterns against HIPAA, PCI DSS, SOC 2, ISO 27001, and GDPR and build a prioritized roadmap.
- Use their Remediation Services to implement control fixes, documentation, and audit-ready artifacts mapped to those frameworks.
Practical flow:
- Build flag evidence, manifests, and external scan outputs as shown above.
- Share a subset with Pentest Testing Corp.
- They turn your raw data into formal risk reports, remediation plans, and clean evidence sets for customers, QSAs, and auditors.
This also pairs naturally with the CI/CD-focused content already on the Cyber Rely blog, such as:
- Embedded compliance in CI/CD tactics – building audit gates and evidence pipelines into your pipelines.
- CI gates for API security with OPA – merge-blocking checks that generate SOC 2 and PCI DSS evidence.
- Policy-as-code rollouts with OPA vs Cedar – authZ decisions as code with CI gating and observability.
- Mapping CI/CD findings to SOC 2 & ISO 27001 – normalizing scanner output into auditor-ready control mappings.
🔐 Frequently Asked Questions (FAQs)
Find answers to commonly asked questions about Feature flags as evidence for SOC 2 & PCI DSS.