pci-dss-control-test-author · git:20260724.26faef0 · 2026-07-24 · sha256 6b0fcf8b94be6d6f
pci-dss-control-test-author git:20260724.26faef0A
Immutable. This exact content never changes and is served at /api/v1/blob/6b0fcf8b94be6d6f.
---
name: pci-dss-control-test-author
description: "Build-an-X for PCI DSS v4.0 scope verification - cardholder data environment (CDE) boundary tests, segmentation tests (PCI Req 1), prohibited-data-storage assertions per Req 3 (no full track data, no CVV/CAV2/CVC2/CID, no PIN/PIN block post-authorization), key-management tests per Req 3.6, encryption-of-transmissions per Req 4. Use when authoring PCI DSS scope-reduction + control tests for any system handling payment-card data."
---
# pci-dss-control-test-author
This is a **build-an-X workflow** for verifying CDE scope is correctly bounded and prohibited-data assertions are captured in fixtures, targeting PCI DSS v4.0 (fully required March 2025).
## Helper stubs
Full stub signatures live in `STUBS.md`. Key interfaces used across steps:
```python
# network / infra
def network_policy.get_allowed_connections(src: str, dst: str) -> list[str]: ...
# data-access
def export_database() -> DatabaseDump: ...
def recent_logs() -> list[LogEntry]: ...
def scan_repo_files() -> list[str]: ...
def read_file(path: str) -> str: ...
# TLS inspection - TLSInfo has: .protocol (str e.g. 'TLSv1.2'), .cipher_strength (int), .cipher (str)
def inspect_tls(endpoint: str) -> TLSInfo: ...
# crypto / format
def is_encrypted_aes_256(value: str) -> bool: ...
def is_truncated_last_4(value: str) -> bool: ...
def is_hashed_with_salt(value: str) -> bool: ...
def is_tokenized(value: str) -> bool: ...
def redact(value: str) -> str: ...
# constants
CDE_API_ENDPOINTS: list[str] = [...] # All API endpoints inside the CDE
DEPRECATED_CIPHERS: list[str] = [...] # e.g. ['RC4', 'DES', '3DES', 'NULL']
```
---
## Step 1 - Define + assert CDE boundary
```python
# pci_scope.py
# Anti-pattern: CDE/non-CDE network policy not tested → policy drift, segmentation breach
CDE_SYSTEMS = {
'payment-service',
'tokenization-service',
'card-vault-db',
'pci-zone-fw',
}
NON_CDE_SYSTEMS = {
'web-frontend', # tokens only; no raw PAN
'analytics', # never sees PAN
'support-tickets', # never sees PAN
}
def test_cde_systems_isolated_from_non_cde():
for cde_system in CDE_SYSTEMS:
for non_cde in NON_CDE_SYSTEMS:
# Network policy MUST block direct connections from non-CDE to CDE
allowed = network_policy.get_allowed_connections(cde_system, non_cde)
# Only via tokenization gateway (via PCI zone FW)
assert allowed == [] or allowed == ['via-pci-zone-fw']
```
Segmentation testing per PCI DSS Req 11.4.1 must be performed at
least every 6 months by a qualified internal resource OR external
penetration tester.
> **Checkpoint:** If any CDE/non-CDE boundary assertion fails, halt and remediate the
> network policy before proceeding. A segmentation failure means downstream steps may
> be testing a mis-scoped environment.
---
## Step 2 - Assert no SAD storage post-authorization (Req 3.2)
SAD may transit during auth but **storage after auth completes** (logs, DB, audit trails, backup) is **forbidden** - full track data, CVV2/CVC2/CID, and PIN/PIN block, all never post-authorization.
```python
import re
# Anti-pattern: trusting developers to never log PAN → eventually slipped into log line
TRACK1_PATTERN = re.compile(r'%[A-Z]\d{12,19}\^[^\?]*\?\d*\?') # Track 1
CVV_PATTERN = re.compile(r'(?<!\d)\d{3,4}(?!\d)') # naive; pair with DLP
def test_no_full_track_data_in_storage():
"""Req 3.2.1: track data must not be retained post-authorization."""
db_dump = export_database()
for table in db_dump.tables:
for row in table:
for value in row.values():
assert not TRACK1_PATTERN.search(str(value)), \
f"Full track data in {table.name}"
def test_no_cvv_in_logs():
"""Req 3.2.2: CVV2/CVC2/CID must not be retained."""
log_entries = recent_logs()
for entry in log_entries:
# Look for proximity of CVV-like 3-4 digit numbers near "cvv" / "card" tokens
if 'cvv' in entry.text.lower() or 'card' in entry.text.lower():
matches = CVV_PATTERN.findall(entry.text)
for m in matches:
assert m == '***' or m == '----', \
f"Possible CVV in log: {entry.id}"
```
> **Checkpoint:** If SAD is found in storage or logs, **halt and remediate** before
> proceeding to Step 3. Continuing to test encryption-at-rest while SAD is present
> misrepresents compliance posture. File a severity-1 finding and trigger your incident
> response process before re-running.
---
## Step 3 - Assert PAN encryption at rest (Req 3.4) + key management (Req 3.6)
```python
# Anti-pattern: hardcoded encryption keys in repo → Req 3.6 violation, PAN exposure
def test_pan_stored_encrypted():
"""Req 3.4: PAN unreadable wherever stored (encryption / truncation / hashing / tokenization).
Prefer tokenization for new paths; treat the others as escape hatches for pre-existing systems.
"""
card_record = CardVault.objects.first()
raw_pan = card_record._raw_pan_field # accessor for storage-format field
# Format MUST be one of:
# - encrypted (AES-256 or stronger) <- preferred for new paths
# - truncated (e.g., last 4 only)
# - hashed (with salt; one-way)
# - tokenized (replaced with non-sensitive token) <- default for new systems
valid_format = (
is_encrypted_aes_256(raw_pan)
or is_truncated_last_4(raw_pan)
or is_hashed_with_salt(raw_pan)
or is_tokenized(raw_pan)
)
assert valid_format, f"PAN stored unprotected: {redact(raw_pan)}"
def test_decryption_keys_not_in_app_repo():
"""Req 3.6: cryptographic keys protected against unauthorized access."""
repo_files = scan_repo_files()
for f in repo_files:
content = read_file(f)
# No hardcoded AES keys (high entropy + length 256+ bits)
assert not re.search(r'AES_KEY\s*=\s*["\'][A-Za-z0-9+/=]{40,}', content), \
f"Possible hardcoded AES key in {f}"
# No KMS key file references
assert 'kms-private-key.pem' not in f, \
f"KMS private key file referenced in repo: {f}"
```
> **Checkpoint:** If PANs are stored unprotected or keys are found in the repo, halt.
> Key-in-repo findings require immediate secret rotation before continuing.
---
## Step 4 - Encryption of transmissions (Req 4)
```python
# Anti-pattern: TLS 1.0 / 1.1 still enabled → Req 4 violation
def test_pan_only_transmitted_via_strong_crypto():
"""Req 4.2.1: strong crypto for transmission of cardholder data over open networks."""
for endpoint in CDE_API_ENDPOINTS:
tls_info = inspect_tls(endpoint)
assert tls_info.protocol >= 'TLSv1.2', \
f"Weak TLS protocol on {endpoint}: {tls_info.protocol}"
assert tls_info.cipher_strength >= 256, \
f"Cipher strength too low on {endpoint}: {tls_info.cipher_strength}"
assert tls_info.cipher not in DEPRECATED_CIPHERS, \
f"Deprecated cipher on {endpoint}: {tls_info.cipher}"
```
> **Checkpoint:** TLS failures on any CDE endpoint are blocking. Deprecated protocols
> (TLS 1.0 / 1.1) must be disabled before proceeding to access-control testing.
---
## Step 5 - Access control (Req 7 + 8)
```python
# Anti-pattern: generic shared accounts (e.g., 'admin' / 'svc-account') → Req 8.2.1 violation
def test_cde_access_requires_unique_id():
"""Req 8.2.1: assign all users a unique ID before access to system components."""
# No shared / generic accounts
response = client.post('/cde-api/login', json={'username': 'shared-svc', 'password': 'secret'})
assert response.status_code == 403
def test_cde_access_requires_mfa():
"""Req 8.4: implement MFA for all access into the CDE."""
response = client.post('/cde-api/login', json={
'username': 'alice@example.com',
'password': correct_password,
# No MFA token
})
assert response.status_code == 401
assert response.json()['error'] == 'mfa_required'
```
> **Checkpoint:** Shared-account or MFA bypass failures are blocking. Do not proceed
> to logging tests while unauthorized access paths remain open.
---
## Step 6 - Logging (Req 10)
Cross-ref `audit-trail-test-author`:
```python
def test_pan_access_creates_audit_record():
"""Req 10.2: audit trails to reconstruct events."""
user.access_card(card_id=123)
audit = AuditLog.objects.filter(
actor=user.id,
action='pan_access',
subject=f'card:{card_id}',
).first()
assert audit is not None
assert audit.timestamp is not None
# Audit log itself MUST not contain the PAN:
assert not re.search(r'\d{13,19}', audit.full_event_text), \
"PAN found in audit log text - log masking is broken"
```
---
## Step 7 - Scope reduction strategies
PCI DSS scope reduction is the highest-leverage cost-saving. Full strategy guidance lives in `STRATEGIES.md`; defaults are:
- **Tokenization (default):** replace PAN with a non-sensitive token at the earliest possible boundary so downstream systems handle tokens only. Shrinks CDE the most for the least integration churn in a typical SaaS architecture.
- **Hosted payment page (iframe):** when pre-tokenization isn't feasible and card data must never touch your servers.
- **P2PE:** physical POS / card-present flows; encrypted at swipe with only decryption point in CDE.
- **Network segmentation:** layered defense on top of one of the above; never the sole scope-reduction strategy.
**PAN-storage format default (Req 3.4):** prefer **tokenization** at the storage boundary; the four `is_*` checks in Step 3's `test_pan_stored_encrypted` are an OR because pre-existing systems may already use any of them, but for new storage paths pick tokenization and treat encryption / truncation / hashing as escape hatches when tokenization isn't feasible.
## Limitations
- Targets PCI DSS v4.0; v3.2.1 has subtle differences.
- QSA audit-style validation requires a credentialed assessor; tests verify implementation only.
- CVV regex patterns are approximate; pair with QSA-validated DLP tooling.
- Scope-reduction strategies require organizational and technical changes beyond this skill.
## References
- [pci][pci] - PCI Security Standards Council
[pci]: https://www.pcisecuritystandards.org/
- pcisecuritystandards.org/document_library/ - PCI DSS v4.0 docs
- pcisecuritystandards.org/glossary/ - PCI terminology
- `gdpr-test-patterns`,
`hipaa-test-patterns`,
`ccpa-test-patterns`,
`soc2-evidence-collector` -
sister compliance pattern catalogs
- `audit-trail-test-author` -
Req 10 audit log requirements