ISO 27001 Compliance Testing Checklist for Developers
ISO 27001 is the international standard for information security management systems (ISMS). Certification requires demonstrating that security controls are implemented, tested, and continuously monitored. For developers, this translates to a specific set of testing responsibilities — most of which can be automated.
What ISO 27001 Requires Developers to Test
ISO 27001 Annex A contains 93 controls (in the 2022 version) organized into four domains. The controls most relevant to development teams:
Technological controls (Annex A.8):
- A.8.8 Management of technical vulnerabilities
- A.8.9 Configuration management
- A.8.24 Use of cryptography
- A.8.25 Secure development life cycle
- A.8.26 Application security requirements
- A.8.27 Secure system architecture and engineering principles
- A.8.28 Secure coding
- A.8.29 Security testing in development and acceptance
Access control (A.5.15–A.5.18):
- Authentication requirements
- Privileged access management
- Access rights provisioning and review
This guide focuses on A.8.29 — security testing in development — and how to automate evidence collection for audit.
Control A.8.29: Security Testing in Development
The standard requires:
"Security testing shall be planned and performed in the development and acceptance life cycle for all information systems."
In practice, this means:
- Static application security testing (SAST) in your CI pipeline
- Software composition analysis (SCA) for dependency vulnerabilities
- Dynamic application security testing (DAST) before releases
- Penetration testing at defined intervals
- Evidence retention for each test run
SAST Integration
Add SAST to your CI pipeline using Semgrep, Snyk, or Bandit (Python):
# .github/workflows/security.yml
name: Security Testing
on: [push, pull_request]
jobs:
sast:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Run Semgrep SAST
uses: returntocorp/semgrep-action@v1
with:
config: >-
p/owasp-top-ten
p/secrets
p/default
env:
SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }}
- name: Upload SAST results
uses: actions/upload-artifact@v3
if: always()
with:
name: sast-results-${{ github.sha }}
path: semgrep-results.sarif
retention-days: 90 # ISO 27001 requires retentionSCA for Dependency Vulnerabilities
sca:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Run dependency audit (Node.js)
run: npm audit --audit-level=high --json > dependency-audit.json
continue-on-error: true
- name: Check for critical vulnerabilities
run: |
CRITICAL=$(cat dependency-audit.json | jq '.metadata.vulnerabilities.critical')
HIGH=$(cat dependency-audit.json | jq '.metadata.vulnerabilities.high')
echo "Critical: $CRITICAL, High: $HIGH"
if [ "$CRITICAL" -gt "0" ]; then
echo "FAIL: Critical vulnerabilities found"
exit 1
fi
- name: Upload SCA results
uses: actions/upload-artifact@v3
if: always()
with:
name: sca-results-${{ github.sha }}
path: dependency-audit.json
retention-days: 90Control A.8.24: Use of Cryptography
ISO 27001 requires a cryptography policy. Automated tests verify:
No weak algorithms:
# test_cryptography.py
import subprocess
import pytest
def test_no_md5_in_source():
"""ISO 27001 A.8.24: MD5 is not approved for security use"""
result = subprocess.run(
['grep', '-rn', '--include=*.py', 'hashlib.md5', 'src/'],
capture_output=True, text=True
)
assert result.returncode != 0, \
f"MD5 usage found (prohibited by crypto policy):\n{result.stdout}"
def test_no_sha1_for_security():
"""SHA-1 allowed only for non-security checksums, not authentication"""
result = subprocess.run(
['grep', '-rn', '--include=*.py', '-E', 'sha1|SHA1', 'src/auth/'],
capture_output=True, text=True
)
assert result.returncode != 0, \
f"SHA-1 found in auth code (prohibited by crypto policy):\n{result.stdout}"
def test_minimum_key_length():
"""RSA keys must be >= 2048 bits per crypto policy"""
# Test that key generation code uses minimum 2048 bits
from src.crypto.keys import generate_rsa_key
key = generate_rsa_key()
assert key.key_size >= 2048, \
f"RSA key {key.key_size} bits — minimum is 2048 (crypto policy)"
def test_tls_minimum_version():
"""TLS 1.0 and 1.1 must not be configured"""
import ssl
ctx = ssl.create_default_context()
assert ctx.minimum_version >= ssl.TLSVersion.TLSv1_2, \
"TLS minimum version must be 1.2 or higher"Control A.8.28: Secure Coding
Track secure coding practices with automated checks:
Input validation:
// test/security/input-validation.test.js
const { validateEmail, validateUserId, sanitizeHtml } = require('../../src/validators');
describe('ISO 27001 A.8.28 — Input Validation Controls', () => {
describe('SQL injection prevention', () => {
const sqlPayloads = [
"' OR '1'='1",
"'; DROP TABLE users; --",
"1 UNION SELECT * FROM users",
"admin'--",
];
sqlPayloads.forEach(payload => {
it(`rejects SQL injection: ${payload.substring(0, 30)}`, () => {
expect(() => validateUserId(payload)).toThrow();
});
});
});
describe('XSS prevention', () => {
const xssPayloads = [
'<script>alert("xss")</script>',
'<img src=x onerror=alert(1)>',
'javascript:alert(1)',
'<svg onload=alert(1)>',
];
xssPayloads.forEach(payload => {
it(`sanitizes XSS payload: ${payload.substring(0, 30)}`, () => {
const result = sanitizeHtml(payload);
expect(result).not.toContain('<script>');
expect(result).not.toContain('javascript:');
expect(result).not.toContain('onerror=');
});
});
});
});Access Control Testing (A.5.15)
# test_access_control.py
import pytest
from src.auth import create_token, verify_access
class TestISOAccessControl:
"""ISO 27001 Annex A.5.15 — Access Control"""
def test_unauthenticated_access_denied(self, client):
"""Resources require authentication"""
response = client.get('/api/data')
assert response.status_code == 401
def test_role_enforcement(self, client):
"""Users cannot access resources beyond their role"""
user_token = create_token(user_id=1, role='user')
response = client.get(
'/api/admin/users',
headers={'Authorization': f'Bearer {user_token}'}
)
assert response.status_code == 403
def test_token_expiry(self, client):
"""Expired tokens are rejected"""
expired_token = create_token(user_id=1, expires_in=-1)
response = client.get(
'/api/profile',
headers={'Authorization': f'Bearer {expired_token}'}
)
assert response.status_code == 401
def test_privilege_escalation_prevented(self, client):
"""Users cannot modify their own roles"""
user_token = create_token(user_id=1, role='user')
response = client.patch(
'/api/users/1',
json={'role': 'admin'},
headers={'Authorization': f'Bearer {user_token}'}
)
assert response.status_code in (400, 403)Audit Logging Verification (A.8.15)
ISO 27001 A.8.15 requires audit logging. Test that security events are logged:
def test_failed_login_is_logged(client, audit_log):
"""A.8.15: Failed authentication attempts must be logged"""
client.post('/auth/login', json={
'username': 'test@example.com',
'password': 'wrong-password'
})
log_entries = audit_log.get_entries(event_type='auth.failed')
assert len(log_entries) == 1
entry = log_entries[0]
assert 'username' in entry
assert 'timestamp' in entry
assert 'ip_address' in entry
assert 'user_agent' in entry
def test_privilege_change_is_logged(client, audit_log, admin_token):
"""A.8.15: Privilege changes must be logged"""
client.patch(
'/api/users/2',
json={'role': 'admin'},
headers={'Authorization': f'Bearer {admin_token}'}
)
log_entries = audit_log.get_entries(event_type='access.privilege_change')
assert len(log_entries) == 1Evidence Collection for Audits
ISO 27001 audits require evidence that controls are operating. Automate evidence collection:
# scripts/collect-security-evidence.py
"""
Collects security test evidence for ISO 27001 Statement of Applicability.
Run before audit reviews.
"""
import json
import subprocess
from datetime import datetime, timezone
from pathlib import Path
def collect_evidence():
evidence = {
'collection_date': datetime.now(timezone.utc).isoformat(),
'controls': {}
}
# A.8.29 — Security testing results
sast_results = Path('reports/sast-latest.json')
if sast_results.exists():
with open(sast_results) as f:
data = json.load(f)
evidence['controls']['A.8.29'] = {
'control': 'Security testing in development',
'last_run': data.get('scan_date'),
'findings': data.get('findings_count'),
'critical': data.get('critical_count'),
'status': 'PASS' if data.get('critical_count', 0) == 0 else 'FAIL'
}
# A.8.8 — Vulnerability management
sca_result = subprocess.run(
['npm', 'audit', '--json'],
capture_output=True, text=True
)
if sca_result.returncode == 0:
sca_data = json.loads(sca_result.stdout)
vuln = sca_data.get('metadata', {}).get('vulnerabilities', {})
evidence['controls']['A.8.8'] = {
'control': 'Management of technical vulnerabilities',
'critical': vuln.get('critical', 0),
'high': vuln.get('high', 0),
'status': 'PASS' if vuln.get('critical', 0) == 0 else 'FAIL'
}
# Write evidence file
output_path = Path(f'evidence/iso27001-{datetime.now().strftime("%Y-%m-%d")}.json')
output_path.parent.mkdir(exist_ok=True)
with open(output_path, 'w') as f:
json.dump(evidence, f, indent=2)
print(f'Evidence collected: {output_path}')
return evidence
if __name__ == '__main__':
collect_evidence()Checklist for Developers
Before each release:
- SAST scan completed with no critical findings
- SCA audit shows no critical vulnerabilities
- All authentication tests passing
- Access control tests passing
- Cryptography tests passing (no weak algorithms)
- Audit logging tests passing
- Evidence artifacts retained (90+ days)
For each quarter:
- DAST scan against staging environment
- Review and update threat model
- Verify backup restoration tested
- Review access rights for departing team members
For annual audit:
- Compile evidence package from CI artifacts
- Penetration test report from qualified tester
- Risk assessment review documented
- All Annex A controls reviewed for continued applicability
Summary
ISO 27001 compliance testing for developers focuses on:
- Automated security testing in CI — SAST and SCA on every commit
- Cryptography policy enforcement — tests verifying no weak algorithms
- Input validation and access control — standard security unit tests
- Audit log verification — tests that security events are recorded
- Evidence retention — CI artifacts stored 90+ days for audit
The key insight: ISO 27001 auditors verify that controls are operating — not just configured. Automated tests that run on every PR and retain artifacts are the most efficient way to demonstrate continuous control operation.