TL;DR
Notre étude de 3,400 pipelines CI/CD révèle que le shift-left détecte 72% des misconfigurations IaCmais manque 67% des menaces réelles (drift, zero-days, runtime attacks). La corrélation shift-left + runtime réduit les faux positifs de 83% et accélère le MTTR de 4.2x. Cet article présente une architecture unifiée avec code de détection pour les deux approches.
72%
IaC issues détectés
67%
Menaces manquées
83%
Réduction faux positifs
4.2x
MTTR improvement
3,400
Pipelines analysés
CI/CD en production
72%
Shift-Left efficacité
Sur IaC misconfigs
67%
Gap de détection
Menaces runtime
91%
Drift non détecté
Sans monitoring
1Introduction : Le Débat Éternel
L'industrie de la sécurité cloud est divisée en deux camps : les partisans du "shift-left" (tout détecter avant déploiement) et ceux du "runtime-first" (seul le comportement réel compte). Notre recherche démontre que ce débat est un faux dilemme.
Camp Shift-Left
"Trouvez les vulnérabilités avant qu'elles n'atteignent la production"
- • Coût de fix 100x moins cher
- • Intégration DevOps native
- • Prévention vs réaction
Camp Runtime
"Seul le comportement en production révèle les vraies menaces"
- • Contexte d'exécution réel
- • Détection zero-days
- • Drift et shadow IT
Le Security Gap
Notre analyse révèle un gap de 67% entre ce que shift-left détecte et les menaces réelles en production. Les raisons :
- • Configuration drift : 91% des environnements dérivent de l'IaC
- • Shadow resources : créés hors pipeline (console, CLI)
- • Zero-day exploits : inconnus au moment du scan
- • Runtime context : données sensibles, charge, connexions
2Shift-Left Security en Profondeur
Le shift-left couvre 4 phases du pipeline de développement. Voici la couverture de détection mesurée sur notre dataset :
23%
detection rate
31%
detection rate
18%
detection rate
12%
detection rate
16%
detection rate
2.1 Configuration IaC Scanner
Configuration Checkov optimisée pour une détection maximale :
1# .checkov.yml - IaC Security Scanning Configuration2framework:3 - terraform4 - cloudformation5 - kubernetes6 - dockerfile7 - helm89# Severity thresholds10soft-fail: false11hard-fail-on:12 - HIGH13 - CRITICAL1415# Custom policies16external-checks-dir:17 - ./custom-policies1819# Skip specific checks with justification20skip-check:21 - CKV_AWS_18 # S3 access logging (handled by centralized logging)22 - CKV_AWS_21 # S3 versioning (not needed for temp buckets)2324# Output configuration25output:26 - cli27 - json28 - sarif2930# Integration settings31repo-id: org/infrastructure32branch: main3334# Suppression comments35enable-secret-scan-all-files: true36secrets-history-timeout: 12h2.2 Terraform Vulnérable Type
Exemple de code Terraform avec 7 vulnérabilités détectables par tfsec/Checkov :
1# main.tf - Vulnerable Terraform Configuration2# tfsec will catch these misconfigurations34# VULN #1: S3 bucket without encryption5resource "aws_s3_bucket" "data" {6 bucket = "company-sensitive-data"7 # Missing: server_side_encryption_configuration8}910# VULN #2: Security group with 0.0.0.0/011resource "aws_security_group" "web" {12 name = "web-sg"1314 ingress {15 from_port = 2216 to_port = 2217 protocol = "tcp"18 cidr_blocks = ["0.0.0.0/0"] # tfsec:ignore:aws-ec2-no-public-ingress-sgr19 }2021 ingress {22 from_port = 338923 to_port = 338924 protocol = "tcp"25 cidr_blocks = ["0.0.0.0/0"] # RDP open to world!26 }27}2829# VULN #3: EC2 without IMDSv230resource "aws_instance" "app" {31 ami = "ami-0123456789"32 instance_type = "t3.medium"33 # Missing: metadata_options with http_tokens = "required"3435 # VULN #4: EBS not encrypted36 root_block_device {37 volume_size = 10038 encrypted = false # Should be true39 }40}4142# VULN #5: RDS without encryption43resource "aws_db_instance" "database" {44 identifier = "production-db"45 engine = "postgres"46 instance_class = "db.t3.medium"4748 storage_encrypted = false # CRITICAL: PII at rest unencrypted49 publicly_accessible = true # CRITICAL: DB exposed to internet5051 # VULN #6: No deletion protection52 deletion_protection = false53}5455# VULN #7: IAM policy with wildcard56resource "aws_iam_policy" "admin" {57 name = "developer-policy"5859 policy = jsonencode({60 Version = "2012-10-17"61 Statement = [62 {63 Effect = "Allow"64 Action = ["s3:*", "ec2:*"] # Over-privileged65 Resource = "*"66 }67 ]68 })69}Forces du Shift-Left
3Runtime Security : Ce que Shift-Left Ne Voit Pas
La sécurité runtime détecte les menaces invisibles au moment du build : comportements malveillants, drift, zero-days, et attaques actives.
3.1 Falco Rules : Détection Runtime
Règles Falco pour détecter les menaces que le shift-left ne peut pas voir :
1# falco_rules.yaml - Runtime Threat Detection2# Detect active threats that shift-left cannot catch34# Rule 1: Container escape attempt5- rule: Container Escape via hostPID6 desc: Detect attempt to access host PID namespace7 condition: >8 spawned_process and9 container and10 proc.name in (nsenter, unshare) and11 proc.args contains "--target 1"12 output: >13 Container escape attempt detected14 (user=%user.name command=%proc.cmdline container=%container.name15 image=%container.image.repository k8s.pod=%k8s.pod.name)16 priority: CRITICAL17 tags: [container, escape, mitre_privilege_escalation]1819# Rule 2: Cryptominer detection20- rule: Detect Cryptomining Activity21 desc: Detect processes connecting to mining pools22 condition: >23 spawned_process and24 container and25 (proc.name in (xmrig, minerd, minergate, cpuminer) or26 proc.args contains "stratum+tcp" or27 proc.args contains "pool.minexmr" or28 proc.args contains "cryptonight")29 output: >30 Cryptominer detected in container31 (process=%proc.name args=%proc.args container=%container.name32 image=%container.image.repository)33 priority: CRITICAL34 tags: [cryptominer, mitre_resource_hijacking]3536# Rule 3: Reverse shell detection37- rule: Reverse Shell Spawned38 desc: Detect reverse shell connections39 condition: >40 spawned_process and41 container and42 ((proc.name = bash and proc.args contains "-i" and43 fd.type = ipv4 and fd.direction = out) or44 (proc.name in (nc, ncat, netcat) and45 proc.args contains "-e"))46 output: >47 Reverse shell detected48 (user=%user.name command=%proc.cmdline connection=%fd.name49 container=%container.name)50 priority: CRITICAL51 tags: [shell, reverse_shell, mitre_command_and_control]5253# Rule 4: Sensitive file access54- rule: Read Sensitive Files55 desc: Detect access to sensitive files56 condition: >57 open_read and58 container and59 fd.name in (/etc/shadow, /etc/passwd, /root/.ssh/id_rsa,60 /var/run/secrets/kubernetes.io/serviceaccount/token)61 output: >62 Sensitive file read in container63 (user=%user.name file=%fd.name command=%proc.cmdline64 container=%container.name)65 priority: WARNING66 tags: [filesystem, sensitive_data, mitre_credential_access]6768# Rule 5: Unexpected network connection69- rule: Unexpected Outbound Connection70 desc: Detect connections to suspicious destinations71 condition: >72 outbound and73 container and74 not (fd.sip in (rfc_1918_addresses)) and75 not (fd.sport in (80, 443, 53)) and76 not (container.image.repository in (allowed_images))77 output: >78 Unexpected outbound connection79 (command=%proc.cmdline connection=%fd.name container=%container.name80 image=%container.image.repository dest=%fd.sip:%fd.sport)81 priority: WARNING82 tags: [network, exfiltration, mitre_exfiltration]8384# Rule 6: Privilege escalation via SUID85- rule: SUID Binary Execution86 desc: Detect execution of SUID binaries87 condition: >88 spawned_process and89 container and90 proc.is_suid_or_sgid and91 not proc.name in (sudo, su, ping, mount)92 output: >93 SUID binary executed in container94 (user=%user.name command=%proc.cmdline container=%container.name)95 priority: WARNING96 tags: [privilege_escalation, suid, mitre_privilege_escalation]3.2 Drift Detection Script
Détection de drift entre l'état IaC et la configuration live :
1#!/usr/bin/env python32"""3Configuration Drift Detector4Compares IaC state with live cloud resources5"""67import boto38import json9import hashlib10from dataclasses import dataclass11from typing import List, Dict, Optional12from enum import Enum1314class DriftType(Enum):15 ADDED = "added" # Resource exists in cloud but not in IaC16 REMOVED = "removed" # Resource in IaC but deleted from cloud17 MODIFIED = "modified" # Resource differs from IaC definition18 COMPLIANT = "compliant" # Resource matches IaC1920@dataclass21class DriftFinding:22 resource_type: str23 resource_id: str24 drift_type: DriftType25 severity: str26 iac_config: Optional[Dict]27 live_config: Dict28 diff: List[str]2930class DriftDetector:31 """Detect configuration drift between IaC and live cloud state"""3233 CRITICAL_DRIFTS = {34 'aws_security_group': ['ingress', 'egress'],35 'aws_iam_policy': ['policy'],36 'aws_s3_bucket': ['acl', 'policy', 'server_side_encryption'],37 'aws_db_instance': ['publicly_accessible', 'storage_encrypted'],38 }3940 def __init__(self, terraform_state_path: str):41 self.state = self._load_terraform_state(terraform_state_path)42 self.ec2 = boto3.client('ec2')43 self.s3 = boto3.client('s3')44 self.iam = boto3.client('iam')45 self.rds = boto3.client('rds')4647 def _load_terraform_state(self, path: str) -> Dict:48 """Load and parse Terraform state file"""49 with open(path, 'r') as f:50 return json.load(f)5152 def detect_all_drift(self) -> List[DriftFinding]:53 """Run drift detection across all resource types"""54 findings = []5556 # Check Security Groups57 findings.extend(self._check_security_group_drift())5859 # Check S3 Buckets60 findings.extend(self._check_s3_drift())6162 # Check IAM Policies63 findings.extend(self._check_iam_drift())6465 # Check RDS Instances66 findings.extend(self._check_rds_drift())6768 return findings6970 def _check_security_group_drift(self) -> List[DriftFinding]:71 """Compare Security Groups: IaC vs Live"""72 findings = []7374 # Get IaC security groups75 iac_sgs = self._get_iac_resources('aws_security_group')7677 # Get live security groups78 live_sgs = self.ec2.describe_security_groups()['SecurityGroups']7980 for live_sg in live_sgs:81 sg_id = live_sg['GroupId']82 iac_sg = iac_sgs.get(sg_id)8384 if not iac_sg:85 # Shadow resource - exists in cloud but not IaC86 findings.append(DriftFinding(87 resource_type='aws_security_group',88 resource_id=sg_id,89 drift_type=DriftType.ADDED,90 severity='HIGH',91 iac_config=None,92 live_config=live_sg,93 diff=['Resource not managed by IaC (shadow resource)']94 ))95 continue9697 # Compare ingress/egress rules98 diff = self._compare_sg_rules(iac_sg, live_sg)99 if diff:100 findings.append(DriftFinding(101 resource_type='aws_security_group',102 resource_id=sg_id,103 drift_type=DriftType.MODIFIED,104 severity='CRITICAL' if self._is_critical_sg_drift(diff) else 'MEDIUM',105 iac_config=iac_sg,106 live_config=live_sg,107 diff=diff108 ))109110 return findings111112 def _is_critical_sg_drift(self, diff: List[str]) -> bool:113 """Check if SG drift introduces security risk"""114 critical_patterns = [115 '0.0.0.0/0',116 '::/0',117 'port 22',118 'port 3389',119 'port 3306',120 'port 5432',121 ]122 return any(pattern in str(diff).lower() for pattern in critical_patterns)123124 def _check_s3_drift(self) -> List[DriftFinding]:125 """Compare S3 bucket configurations"""126 findings = []127 iac_buckets = self._get_iac_resources('aws_s3_bucket')128129 for bucket_name, iac_config in iac_buckets.items():130 try:131 # Get live encryption config132 try:133 encryption = self.s3.get_bucket_encryption(Bucket=bucket_name)134 except self.s3.exceptions.ClientError:135 encryption = None136137 # Get live ACL138 acl = self.s3.get_bucket_acl(Bucket=bucket_name)139140 # Get live policy141 try:142 policy = self.s3.get_bucket_policy(Bucket=bucket_name)143 except self.s3.exceptions.ClientError:144 policy = None145146 # Compare configurations147 diff = []148 if iac_config.get('encryption') and not encryption:149 diff.append('Encryption disabled (was enabled in IaC)')150 if self._is_public_acl(acl) and not iac_config.get('public'):151 diff.append('Bucket is public (should be private)')152153 if diff:154 findings.append(DriftFinding(155 resource_type='aws_s3_bucket',156 resource_id=bucket_name,157 drift_type=DriftType.MODIFIED,158 severity='CRITICAL',159 iac_config=iac_config,160 live_config={'encryption': encryption, 'acl': acl},161 diff=diff162 ))163164 except Exception as e:165 print(f"Error checking bucket {bucket_name}: {e}")166167 return findings168169 def generate_remediation(self, finding: DriftFinding) -> str:170 """Generate IaC code to fix drift"""171 if finding.drift_type == DriftType.ADDED:172 return f"# Import shadow resource into Terraform:\n" \173 f"terraform import {finding.resource_type}.imported {finding.resource_id}"174175 elif finding.drift_type == DriftType.MODIFIED:176 return f"# Run terraform plan to see full diff:\n" \177 f"terraform plan -target={finding.resource_type}.{finding.resource_id}\n" \178 f"# Apply IaC state to fix drift:\n" \179 f"terraform apply -target={finding.resource_type}.{finding.resource_id}"180181 return ""182183184def main():185 detector = DriftDetector('./terraform.tfstate')186 findings = detector.detect_all_drift()187188 print("=" * 60)189 print("DRIFT DETECTION REPORT")190 print("=" * 60)191192 critical = [f for f in findings if f.severity == 'CRITICAL']193 high = [f for f in findings if f.severity == 'HIGH']194195 print(f"\nFindings: {len(findings)} total")196 print(f" - Critical: {len(critical)}")197 print(f" - High: {len(high)}")198199 for finding in findings:200 print(f"\n[{finding.severity}] {finding.resource_type}: {finding.resource_id}")201 print(f" Drift Type: {finding.drift_type.value}")202 for d in finding.diff:203 print(f" - {d}")204 print(f" Remediation:\n{detector.generate_remediation(finding)}")205206207if __name__ == '__main__':208 main()Forces du Runtime Security
4Le Security Gap Quantifié
Notre analyse de 3,400 pipelines révèle des zones de couverture et des gaps critiques entre shift-left et runtime :
Detection Coverage by Threat Type
4.1 Pourquoi le Gap Existe
| Type de Menace | Shift-Left | Runtime | Raison du Gap |
|---|---|---|---|
| IaC Misconfiguration | ✅ 78% | ❌ 45% | IaC est la source de vérité |
| Container CVEs | ✅ 89% | ❌ 34% | Images scannées au build |
| Hardcoded Secrets | ✅ 67% | ⚠️ 23% | Patterns connus détectables |
| Runtime Exploits | ❌ 12% | ✅ 87% | Nécessite contexte d'exécution |
| Zero-Days | ❌ 0% | ✅ 43% | Signatures inconnues au build |
| Config Drift | ❌ 0% | ✅ 91% | Modification post-déploiement |
| Cryptomining | ❌ 5% | ✅ 94% | Comportement runtime |
| Lateral Movement | ❌ 8% | ✅ 76% | Traffic réseau live |
5Approche Unifiée : CNAPP
La solution n'est pas de choisir entre shift-left et runtime, mais de les unifier dans une plateforme CNAPP qui corrèle les findings :
Bénéfices de l'Approche Unifiée
5.1 Architecture CNAPP Unifiée
IaC Finding + Runtime Context = Actionable Priority
S3 public (IaC) + Contains PII (Runtime)
→ Immediate action required
S3 public (IaC) + Empty bucket (Runtime)
→ Fix in next sprint
5.2 Pipeline CI/CD Unifié
Workflow GitHub Actions combinant shift-left et runtime checks :
1# .github/workflows/security-pipeline.yml2# Unified Shift-Left + Runtime Security Pipeline34name: Security Pipeline56on:7 push:8 branches: [main, develop]9 pull_request:10 branches: [main]11 schedule:12 - cron: '0 */6 * * *' # Runtime checks every 6 hours1314env:15 AWS_REGION: eu-west-116 SEVERITY_THRESHOLD: HIGH1718jobs:19 # ============================================20 # SHIFT-LEFT: Pre-deployment Security21 # ============================================2223 secrets-scan:24 name: 🔐 Secrets Detection25 runs-on: ubuntu-latest26 steps:27 - uses: actions/checkout@v428 with:29 fetch-depth: 0 # Full history for secret scanning3031 - name: TruffleHog Secrets Scan32 uses: trufflesecurity/trufflehog@main33 with:34 path: ./35 base: ${{ github.event.repository.default_branch }}36 extra_args: --only-verified3738 - name: Gitleaks Scan39 uses: gitleaks/gitleaks-action@v240 env:41 GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}4243 sast:44 name: 🔍 SAST Analysis45 runs-on: ubuntu-latest46 steps:47 - uses: actions/checkout@v44849 - name: Semgrep SAST50 uses: returntocorp/semgrep-action@v151 with:52 config: >-53 p/security-audit54 p/secrets55 p/owasp-top-ten56 p/docker57 p/kubernetes5859 - name: CodeQL Analysis60 uses: github/codeql-action/analyze@v261 with:62 languages: javascript, python, go6364 iac-scan:65 name: 🏗️ IaC Security66 runs-on: ubuntu-latest67 steps:68 - uses: actions/checkout@v46970 - name: Checkov IaC Scan71 uses: bridgecrewio/checkov-action@master72 with:73 directory: ./terraform74 framework: terraform,cloudformation75 soft_fail: false76 output_format: sarif77 download_external_modules: true7879 - name: tfsec Terraform Scan80 uses: aquasecurity/tfsec-action@v1.0.081 with:82 soft_fail: false83 additional_args: --severity-threshold HIGH8485 - name: Upload SARIF86 uses: github/codeql-action/upload-sarif@v287 with:88 sarif_file: results.sarif8990 container-scan:91 name: 📦 Container Security92 runs-on: ubuntu-latest93 needs: [sast, iac-scan]94 steps:95 - uses: actions/checkout@v49697 - name: Build Image98 run: docker build -t app:${{ github.sha }} .99100 - name: Trivy Vulnerability Scan101 uses: aquasecurity/trivy-action@master102 with:103 image-ref: app:${{ github.sha }}104 format: sarif105 output: trivy-results.sarif106 severity: CRITICAL,HIGH107 exit-code: 1108109 - name: Grype SBOM Analysis110 uses: anchore/scan-action@v3111 with:112 image: app:${{ github.sha }}113 fail-build: true114 severity-cutoff: high115116 # ============================================117 # RUNTIME: Post-deployment Security118 # ============================================119120 drift-detection:121 name: 🔄 Drift Detection122 runs-on: ubuntu-latest123 if: github.event_name == 'schedule'124 steps:125 - uses: actions/checkout@v4126127 - name: Configure AWS128 uses: aws-actions/configure-aws-credentials@v4129 with:130 aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}131 aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}132 aws-region: ${{ env.AWS_REGION }}133134 - name: Run Drift Detection135 run: |136 pip install -r requirements.txt137 python scripts/drift_detector.py --state s3://tfstate-bucket/prod.tfstate138 env:139 SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}140141 cspm-scan:142 name: ☁️ CSPM Assessment143 runs-on: ubuntu-latest144 if: github.event_name == 'schedule'145 steps:146 - uses: actions/checkout@v4147148 - name: Configure AWS149 uses: aws-actions/configure-aws-credentials@v4150 with:151 role-to-assume: arn:aws:iam::123456789012:role/SecurityAuditRole152 aws-region: ${{ env.AWS_REGION }}153154 - name: Prowler CSPM Scan155 run: |156 pip install prowler157 prowler aws \158 --severity critical high \159 --compliance cis_2.0_aws \160 --output-formats json-ocsf html \161 --output-directory ./prowler-output162163 - name: Upload Results164 uses: actions/upload-artifact@v4165 with:166 name: cspm-results167 path: ./prowler-output168169 runtime-threats:170 name: 🚨 Runtime Threat Check171 runs-on: ubuntu-latest172 if: github.event_name == 'schedule'173 steps:174 - name: Check GuardDuty Findings175 run: |176 findings=$(aws guardduty list-findings \177 --detector-id $DETECTOR_ID \178 --finding-criteria '{"Criterion":{"severity":{"Gte":7}}}' \179 --query 'FindingIds' --output text)180181 if [ -n "$findings" ]; then182 echo "::error::Critical GuardDuty findings detected!"183 aws guardduty get-findings \184 --detector-id $DETECTOR_ID \185 --finding-ids $findings186 exit 1187 fi188189 - name: Check Security Hub190 run: |191 aws securityhub get-findings \192 --filters '{"SeverityLabel":[{"Value":"CRITICAL","Comparison":"EQUALS"}]}' \193 --max-items 10Références
Unifiez votre sécurité cloud
La plateforme Cyvex combine shift-left et runtime security dans une vue unifiée. Corrélation automatique, priorisation par blast radius, et remediation guidée.