ISO 27001 Compliance Testing Checklist for Developers

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:

  1. Static application security testing (SAST) in your CI pipeline
  2. Software composition analysis (SCA) for dependency vulnerabilities
  3. Dynamic application security testing (DAST) before releases
  4. Penetration testing at defined intervals
  5. 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 retention

SCA 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: 90

Control 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) == 1

Evidence 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:

  1. Automated security testing in CI — SAST and SCA on every commit
  2. Cryptography policy enforcement — tests verifying no weak algorithms
  3. Input validation and access control — standard security unit tests
  4. Audit log verification — tests that security events are recorded
  5. 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.

Read more