OverWatch — an agentless AWS CNAPP: it collapses a whole cloud estate into the ranked handful of internet → exposed workload → exploitable CVE → over-privileged role → crown-jewel attack paths and names the choke point that severs the most — plus an IaC static-analysis scanner.
- Overview
- Architecture at a glance
- IaC Security Scanner (
aws_offline_scanner.py) - Live Audit Scanner (
aws_live_scanner.py) - OverWatch — Hosted CNAPP Platform (multi-account, agentless)
- Architecture: hub-and-spoke
- Onboarding
- Air-gapped, zero-telemetry packaging
- Backend modules
- HTTP API surface
- Multi-tenancy (MSSP)
- Continuous scanning and drift digests (CTEM)
- Compliance breadth: 30+ frameworks
- Connectors
- External vulnerability ingest
- Supply-chain ingest: SBOM diff, license, VEX
- Shared Postgres state
- Web console
- When to Use Which Scanner
- Project Structure
- Requirements
- Disclaimer
- License
OverWatch is the product name for the CNAPP: a full Cloud-Native Application Protection Platform for AWS
(CSPM + CIEM + agentless CWPP + DSPM + AI-SPM + CDR) built around a unified security graph and
toxic-combination attack-path correlation, with multi-account onboarding, choke-point remediation, and
code-to-cloud mapping. Beyond posture it adds an AI-SPM pillar (the blast radius of an AI resource's
execution role, fused into the graph), CDR-lite streaming detection ingest (GuardDuty / Security Hub /
CloudTrail-anomaly events → ranked incidents), a cloud-forensics timeline, and a grounded-RAG copilot
that answers only from an account's own scan. It ships as the live scanner (aws_live_scanner.py) + its
aws_* / cnapp_* engine modules, and a hosted platform backend
(see OverWatch — Hosted CNAPP Platform).
This repository contains two complementary AWS security scanners:
| Scanner | File | Type | Input | Checks |
|---|---|---|---|---|
| OverWatch (Live CNAPP) | aws_live_scanner.py |
Live AWS API audit + security graph + attack-path CNAPP | Running AWS account (multi-account via AssumeRole) | 296 severity-mapped across 44 sections |
| IaC Security Scanner | aws_offline_scanner.py |
Static analysis | CloudFormation + Terraform files | 100+ (60+ TF regex + 42 CF structural) |
Use OverWatch to audit a running AWS estate — CIS/compliance posture, effective-permissions CIEM, agentless workload vulnerabilities, and the ranked attack paths + choke points. Use the IaC scanner to catch misconfigurations in CloudFormation templates and Terraform files before deployment.
OverWatch is a three-tier system — a React console over a fail-closed control plane over a pure, agentless scanning engine — that turns read-only AWS reads into a ranked handful of attack paths.
Logical architecture: React console → fail-closed control plane → pure scanning engine → read-only sources.
The differentiator — score the path, not the finding. A toxic combination is a conjunction (exposure × exploitability × privilege × data-path), so a missing factor collapses the score and the classic "high-CVSS but unexposed, no data path" false positive never surfaces.
An end-to-end attack path over traversable (solid) E_PATH edges; annotations (dashed) enrich nodes but are never walked as hops.
Runtime topology. One hardened, non-root container on a private subnet in the security VPC — reaching AWS via VPC endpoints and assuming read-only roles into each spoke account. No public-internet egress.
Hub-and-spoke: VPC-endpoint egress + sts:AssumeRole (with ExternalId) into spoke accounts.
The scan pipeline. Read-only collect → graph → reachability → deep-plane → correlate; every specialized posture engine (eff-perm, CIEM, CWPP, KSPM/KIEM, DSPM, AI-SPM, CDR/EDR/malware ingest) reads the same graph.
The pipeline and engine fan-out — annotations enrich; only E_PATH edges are traversed.
Zero-telemetry by construction. The overwhelming majority of egress is read-only boto3 to the customer's own
accounts; only three allowlisted files may open any other socket, enforced by a recursive-AST tripwire test.
Egress containment — internet / telemetry / phone-home is blocked.
Full detail: the System Design & Architecture guide (Word — 21 chapters, 13 tables, and these five figures) is regenerable, offline, from
docs/diagrams/.
The IaC scanner performs pure static analysis of AWS Infrastructure-as-Code files -- no AWS credentials required. It scans CloudFormation templates (YAML/JSON) and Terraform configuration files (.tf) for security misconfigurations mapped to the CIS AWS Benchmark and AWS Well-Architected Security Pillar.
- No AWS credentials required -- analyses files locally
- 100+ security checks -- 60+ Terraform regex rules + 42 CloudFormation structural checks
- 25+ AWS services covered -- S3, IAM, EC2, RDS, Lambda, CloudTrail, and more
- 3 output formats -- coloured console, JSON, interactive HTML
- Optional dependency --
pyyamlfor CloudFormation YAML files (JSON works without it)
# Scan a directory of IaC files
python -m engine.aws_offline_scanner /path/to/infra/
# Scan a single CloudFormation template
python -m engine.aws_offline_scanner template.yaml --html report.html
# Scan Terraform files with severity filter
python -m engine.aws_offline_scanner main.tf --json findings.json --severity HIGH
# Verbose mode
python -m engine.aws_offline_scanner /path/to/cf/ --verbose --severity MEDIUMusage: aws_offline_scanner.py [-h] [--json FILE] [--html FILE]
[--severity {CRITICAL,HIGH,MEDIUM,LOW,INFO}]
[-v] [--version]
target
positional arguments:
target File or directory containing CloudFormation templates or Terraform files
options:
--json FILE Write JSON report to FILE
--html FILE Write HTML report to FILE
--severity SEV Only report findings at this severity or above
-v, --verbose Show files as they are scanned
--version Show scanner version
| Service | Rule IDs | Count | Key Checks |
|---|---|---|---|
| S3 | AWS-S3-TF-001 to 006 | 6 | Public ACLs, Block Public Access settings |
| IAM | AWS-IAM-TF-001 to 004 | 4 | Wildcard actions/principals, password reset |
| EC2/SG | AWS-SG-TF-001 to 003, AWS-EC2-TF-001 to 003 | 6 | SSH/RDP from 0.0.0.0/0, IMDSv1, public IP, EBS encryption |
| RDS | AWS-RDS-TF-001 to 006 | 6 | Public access, encryption, backups, deletion protection, Multi-AZ |
| CloudTrail | AWS-CT-TF-001 to 003 | 3 | Log validation, multi-region, global events |
| KMS | AWS-KMS-TF-001 | 1 | Key rotation |
| CloudFront | AWS-CF-TF-001 to 002 | 2 | HTTPS enforcement, TLS version |
| ElastiCache | AWS-ECACHE-TF-001 to 002 | 2 | At-rest and in-transit encryption |
| ECS | AWS-ECS-TF-001 to 002 | 2 | Privileged mode, writable root filesystem |
| OpenSearch | AWS-OS-TF-001 to 002 | 2 | HTTPS enforcement, node-to-node encryption |
| Redshift | AWS-RS-TF-001 to 002 | 2 | Public access, encryption |
| ECR | AWS-ECR-TF-001 | 1 | Mutable image tags |
| DynamoDB | AWS-DDB-TF-001 | 1 | SSE encryption |
| Lambda | AWS-LAM-TF-001 | 1 | Reserved concurrency throttling |
| API Gateway | AWS-APIGW-TF-001 | 1 | Stage logging |
| Credentials | AWS-CRED-TF-001 to 002 | 2 | Hardcoded AWS keys, passwords |
| CloudWatch | AWS-CW-TF-001 to 003 | 3 | Log retention, KMS encryption, alarm actions |
| VPC | AWS-VPC-TF-001 to 002 | 2 | Flow logs, public subnet auto-assign |
| WAF | AWS-WAF-TF-001 | 1 | Default allow action |
| GuardDuty | AWS-GD-TF-001 | 1 | Detector disabled |
| Config | AWS-CFG-TF-001 to 002 | 2 | All resource types, global resources |
| Elastic Beanstalk | AWS-EB-TF-001 to 002 | 2 | HTTPS listener, managed updates |
| SageMaker | AWS-SM-TF-001 to 002 | 2 | Internet access, storage encryption |
| EBS | AWS-EBS-TF-001 | 1 | Volume encryption |
| Step Functions | AWS-SFN-TF-001 to 002 | 2 | Logging, X-Ray tracing |
| Bedrock | AWS-BR-TF-001 | 1 | Guardrails |
| Service | Resource Types | Key Checks |
|---|---|---|
| IAM | Role, Policy, ManagedPolicy, User | Wildcard principal/action, AdministratorAccess, inline user policies |
| S3 | Bucket | Public access block, versioning, encryption, logging |
| EC2 | SecurityGroup, Instance | Open ports (22/3389/0-65535), IMDSv2, public IP |
| RDS | DBInstance, DBCluster | Public access, encryption, backups, deletion protection, Multi-AZ |
| Lambda | Function | Reserved concurrency, KMS encryption, tracing |
| CloudTrail | Trail | Log validation, multi-region, S3 encryption |
| CloudFront | Distribution | HTTPS, TLS 1.2+, WAF, logging, origin protocol |
| ELB | Listener | HTTPS protocol enforcement |
| API Gateway | Stage | Logging, stage variables |
| KMS | Key | Rotation enabled |
| SQS | Queue | KMS encryption |
| SNS | Topic | KMS encryption |
| DynamoDB | Table | Point-in-time recovery, SSE |
| ElastiCache | ReplicationGroup | Encryption (rest + transit), auth token |
| EKS | Cluster | Endpoint public access, logging, encryption |
| ECS | TaskDefinition | Privileged mode, read-only root, logging |
| Cognito | UserPool | MFA, password policy |
| OpenSearch | Domain | HTTPS, node-to-node encryption, encryption at rest |
| Redshift | Cluster | Public access, encryption, logging |
| ECR | Repository | Image tag mutability, scan on push |
| Secrets Manager | Secret | KMS encryption |
| CloudWatch | Alarm | Alarm actions |
| Logs | LogGroup | Retention, KMS encryption |
| VPC/Subnet | VPC, Subnet, FlowLog | Flow logs, public IP auto-assign |
| WAFv2 | WebACL | Default action |
| GuardDuty | Detector | Enabled status |
| Config | ConfigurationRecorder | All supported types, global resources |
| Elastic Beanstalk | Environment | HTTPS, managed updates |
| SageMaker | NotebookInstance, Domain | Internet access, KMS encryption |
| Bedrock | Agent | Guardrails |
| EBS | Volume | Encryption |
| Step Functions | StateMachine | Logging, tracing |
The live scanner connects to a running AWS account via boto3, performing read-only security checks aligned to multiple compliance frameworks. It produces colour-coded terminal output with PASS/FAIL/WARN verdicts, posture scoring, JSON/HTML reports, and saves evidence artefacts to a timestamped output directory.
- Read-only by design -- never modifies AWS resources
- 296 severity-mapped checks across 44 audit sections (222 actionable, each with a detailed remediation write-up)
- Effective internet-exposure engine (CNAPP Phase 2) -- computes true reachability (
aws_exposure.py): a workload is flagged internet-exposed only when a public IP/EIP/IPv6 and an active IGW route and open security-group ingress and a permissive stateless NACL (inbound + ephemeral return) all line up — killing the "SG allows 0.0.0.0/0" false positive. sg-references, NAT routes, private subnets and blocked-return NACLs are correctly not exposed - First attack path --
ATTACK-01chains it end-to-end:Internet → exposed EC2 → instance-profile role → privilege escalation to admin(CRITICAL) - Network micro-segmentation (Phase-3) -- Layer A (
SEG-01..06, always-on, config-only, zero new grant) flags over-permissive security groups (world-open sensitive ports, wide ranges, unused SGs, SG-chains to a world-open group, unrestricted egress) as a distinct lens from the 4-gate reachability oracle — a config-level SG hole is caught even when the host is not currently reachable. Layer B (FLOW-01..03, opt-in--flow-logs) reads VPC Flow-Log observed traffic via CloudWatch Logs Insights to recommend evidence-based scope-downs ("only these /24s ever connected"), flag allowed-but-never-used ports, and surface top reject/recon talkers — annotating the graph'sEXPOSED_TOedges with observed evidence (kept out of the traversable attack-path set so the low-FP reachability guarantee is preserved), and failing open to aFLOW-00note when the optional resource-scopedlogs:StartQuerygrant is absent - Deep-plane ingestion (CNAPP Phase 3, buy-not-build) -- BUYS signal from AWS-native services as graph edges: Amazon Inspector CVEs with EPSS + CISA-KEV (
HAS_VULN), Macie crown-jewel S3 classification, IAM Access Analyzer authoritative external access, GuardDuty live detections (THREAT_ON). Every collector degrades to a graceful no-op when its service is disabled -- never a false positive - Flagship attack path --
ATTACK-02(CRITICAL):Internet → exposed EC2 → exploitable/KEV CVE → over-privileged role → crown-jewel S3 data-- the full toxic combination, condition-aware, escalated when a live GuardDuty threat sits on the chain - Attack-path correlation & choke points (CNAPP Phase 4) -- collapses the whole graph into a ranked handful of scored, explainable attack paths (gated-multiplicative scoring: any missing factor collapses the path, killing the "high-CVSS but unexposed" false positive), then computes choke points:
CHOKEPOINT-01names the single node whose fix severs the most paths to the most crown jewels. Rankedattack_paths+choke_pointsin the JSON report - Effective-permissions ceiling (CNAPP Phase 5) -- evaluates the real AWS decision chain (identity ∩ permission-boundary ∩ SCP, explicit-deny-wins, condition-aware) so an escalation edge a boundary or SCP provably neutralizes is dropped from the graph — the ranked paths reflect genuinely-reachable escalation, not merely granted permissions. Fail-open: absent boundary/SCP data behaves exactly as before (
aws_effperm.py) - Persistent state, drift & waivers (CNAPP Phase 5) --
--state cnapp.dbgives the scanner memory: finding lifecycle (open/resolved/reopened/mutated), coverage-gated resolve, MTTR, posture trend, and waivers (--suppresswith approver/expiry — an accepted risk leaves--fail-ongating but stays tracked; expired waivers auto-reactivate) (aws_state.py) - CIEM right-sizing (CNAPP Phase 5, opt-in
--ciem) -- flags dormant/over-permissioned principals (Access Analyzer unused-access → service-last-accessed) as LOWCIEM-01review candidates and down-ranks the exploit-likelihood of attack paths through them (impact untouched) (aws_unused.py) - Least-privilege policy generation (opt-in
--ciem,CIEM-02) -- for a flagged principal, generates a right-sized IAM policy from GRANTED (GAAD) minus USED (service-last-accessed): drops never-used services, narrowssvc:*to the used action set. Deterministic, honest (empty usage is never a deny-all; window stated; never auto-applied), and attached to theleast_privilegereport side-channel (aws_leastpriv.py) - DSPM surfaces + AWS-resident secrets -- crown-jewel data classification spans S3/RDS/Aurora/DocumentDB/Neptune/MemoryDB/FSx/Kinesis/Timestream/OpenSearch (tag-driven, +
DSPM-03public resource-policy), and a crownSecretnode (Secrets Manager + SSM SecureStrings) with aCAN_READ_DATAreader edge makesinternet → workload → role → secretan attack path (SECRET-05); SSM plaintext-parameter posture isSECRET-01/02. All metadata-only — never a secret-value or data read (aws_deepplane.py,aws_secrets.py) - Grounded security copilot (
aws_copilot.py) -- a scoped RAG assistant that answers natural-language questions using only the scan's ownfinding_catalog+ attack paths + choke points. Self-contained BM25 retrieval (no embeddings/network); offline extractive answers by default (composed purely from corpus fields, so it can't hallucinate), abstains on off-topic questions, and cites the check/path/choke ids it used. An optional injected LLM seam gets only the retrieved corpus as grounded context. Exposed atPOST /accounts/{id}/copilot+/org/copilot(viewer RBAC) - Agentless workload side-scan (CNAPP Phase 6, opt-in
--side-scan) -- the Wiz/Orca CWPP capability with no agent: inventory a workload's OS packages, match them against a vulnerability feed with ecosystem-correct (dpkg/rpm/apk) version comparison, and find on-disk secrets — addingHAS_VULNedges to the SAME graph so agentless CVEs light up ATTACK-02 even when Amazon Inspector is disabled (CWPP-01/02/03). Pure inventory/matching core + EBS Direct-API block plane; live disk extraction deferred (aws_sidescan.py,aws_sidescan_ebs.py) - Persistence backends (CNAPP Phase 6) --
--backend postgresql://...(Postgres state store, deferred to Phase 7 → runs stateless) and--graph-neptune-csv/--graph-neptune-cypherexport the security graph as Amazon Neptune bulk-load CSV or idempotent openCypher upserts (aws_state_dialect.py,aws_graph_neptune.py) - Remediation engine (CNAPP Phase 7, opt-in
--remediate) -- turns the ranked attack paths into a prioritized fix plan ("fix these K choke points to cut N% of critical paths"), reusing the correlation ranking, with remediation-as-code (Terraform/CloudFormation/CLI per finding) and exports (markdown runbook, JSON, GitHub issue, PR body). Read-only — it generates artifacts, never applies changes (aws_remediate.py) - Code-to-cloud (CNAPP Phase 7, opt-in
--iac-dir) -- maps a live cloud finding back to the IaC resource that created it (Terraform/CloudFormation) via a tiered confidence matcher that never guesses, so remediation targets the source and proposes the IaC diff atfile:line(aws_codetocloud.py) - Security graph & attack-path chains (CNAPP Phase 1) -- projects findings onto an ARN-keyed graph (
aws_graph.py), buildsCAN_ASSUME(trust) +CAN_PRIVESC_TO(privesc) edges, and surfaces transitive privilege-escalation chains (user → assume role → escalate to admin) plus roles assumable by any principal. Serialize with--graph graph.json(Neptune migration seed) - IAM privilege-escalation analysis -- builds each principal's effective permission set (via
GetAccountAuthorizationDetails) and detects known escalation paths with resource-aware scoping; condition-guarded paths are downgraded to WARN, boundary/SCP-neutralized paths dropped - Multi-account & multi-region --
--org/--accountsfan out across an AWS Organization via--assume-role;--all-regionssweeps every enabled region for regional sections - Compliance scorecard --
--complianceprints a per-framework control pass/fail rollup (CIS/PCI/HIPAA/SOC2/NIST), also embedded in the JSON report - 5 compliance frameworks -- CIS AWS v3.0, PCI DSS v4.0, HIPAA, SOC 2, NIST 800-53 Rev 5
- Risk scoring -- Posture score 0-100 with letter grade (A-F), severity-weighted
- AWS CLI remediation -- actionable CLI commands for every failed check
- Detailed finding reports -- every actionable check ships a full write-up (
aws_finding_detail.py): the risk (what it is / how it's exploited / why it matters), the business impact, and step-by-step remediation with real AWS CLI, plus the mapped compliance controls. The JSON report carries a deduped, severity-rankedfinding_catalog; the HTML report renders per-finding cards (risk -> impact -> numbered fix steps -> frameworks) above the full findings table. A check with no detailed entry falls back to its one-line CLI, so coverage grows without breaking rendering - 5 output formats -- coloured console, JSON, interactive HTML, SARIF 2.1.0 (GitHub code scanning), ASFF (AWS Security Hub)
- CI/CD gating --
--fail-on CRITICAL|HIGH|MEDIUM|LOWfor pipeline pass/fail control - Scan diff --
--baseline prev.jsonsurfaces only what's new or resolved since a previous run (superseded by--stateDB-backed lifecycle when both are given) - Evidence collection -- CSV/JSON artefact files saved per check
- Application scorecards (v2.39.0) -- FR-2 end to end: an application registry, findings attributed to an accountable owner, and a per-application scorecard that states its own coverage, segregates excepted findings rather than hiding them, refuses a peer rank it cannot normalise, and renders every withheld figure with the reason in place of the number
- Governance layer (v2.38.0) -- ownership attribution, SLA/MTTR, a corrected composite Cloud Risk Score, KRA metrics that report
NOT_ESTABLISHEDrather than a target they did not establish, a CI/CD guardrail gate with a declared failure mode, and forecasts that refuse to exist before their evidence does. Built from a review of the Phase II SRS; the six specification defects it fixed are recorded inCHANGELOG.mdand in four ratifiable documents underdocs/ - 5458 backend tests -- full test suite with mock boto3, no AWS credentials needed (incl. the hosted CNAPP platform and the coverage-close batches — WQL/Controls · policy-as-code · EDR/malware/DSPM ingest · non-AWS OCI registry — alongside exposure, deep-plane, attack-path-scoring, Phase-5 effective-permissions/state/CIEM, Phase-6 side-scan version-comparator/OSV-matching/EBS-block-plane/backend-export, Phase-7 remediation/code-to-cloud false-positive/false-negative catalogs, and the detailed finding write-ups /
finding_catalogrendering; a regression test backs every defect the adversarial-verification passes found)
- Python 3.10+ with
boto3installed (pip install boto3) - AWS credentials -- configured via
aws configure, environment variables, or IAM role - IAM permissions -- the executing identity needs the
SecurityAuditAWS-managed policy (read-only) - Default region --
eu-west-1; override via--regionorAWS_DEFAULT_REGIONenvironment variable
# Run full audit (all 44 sections, 296 severity-mapped checks)
python -m engine.aws_live_scanner
# Target a specific region
python -m engine.aws_live_scanner --region us-east-1
# Run specific sections only (comma-separated, single argument)
python -m engine.aws_live_scanner --sections IAM,S3,VPC
# Run only the IAM privilege-escalation path analysis
python -m engine.aws_live_scanner --sections IAMPRIVESC
# Save JSON + HTML reports and evidence artefacts
python -m engine.aws_live_scanner --json report.json --html report.html --output-dir ./audit_output
# CI/CD: emit SARIF for GitHub code scanning and fail the build on HIGH+ findings
python -m engine.aws_live_scanner --sarif results.sarif --fail-on HIGH
# Push findings into AWS Security Hub (ASFF)
python -m engine.aws_live_scanner --asff findings.asff.json
aws securityhub batch-import-findings --findings file://findings.asff.json
# CNAPP: all regions, compliance scorecard, and export the identity security graph
python -m engine.aws_live_scanner --all-regions --compliance --graph graph.json
# Multi-account: scan every account in the Organization via an assumable read-only role
python -m engine.aws_live_scanner --org --assume-role OrganizationAccountAccessRole --json org.json
# Multi-account: scan an explicit account list with an ExternalId
python -m engine.aws_live_scanner --accounts 111122223333,444455556666 --assume-role AuditRole --external-id my-id
# Show only what changed since the last scan
python -m engine.aws_live_scanner --json today.json --baseline yesterday.json
# Network micro-segmentation: Layer-A SG analysis (SEG-01..06) runs automatically inside
# EXPOSURE; add the opt-in Layer-B VPC Flow-Log overlay (FLOW-01..03) for evidence-based
# scope-down. Needs the optional resource-scoped logs:StartQuery grant; fails open otherwise.
python -m engine.aws_live_scanner --sections EXPOSURE --flow-logs
# Verbose mode
python -m engine.aws_live_scanner --verboseusage: aws_live_scanner.py [-h] [--region REGION] [--json FILE] [--html FILE]
[--sarif FILE] [--asff FILE] [--baseline FILE]
[--fail-on {CRITICAL,HIGH,MEDIUM,LOW}] [--output-dir DIR]
[--sections {IAM,S3,VPC,LOGGING,KMS,EC2,ECR,BACKUP,
RDS,GLACIER,SNS,SQS,CLOUDFRONT,ROUTE53,
BEDROCK,BEDROCK_AGENTS,LAMBDA,EKS,ECS,
SECRETS,WAF,ELASTICACHE,OPENSEARCH,
DYNAMODB,STEPFUNCTIONS,APIGATEWAY,ELB,
EBS,REDSHIFT,EFS,ACM,SAGEMAKER,COGNITO,
APIGATEWAYV2,IAMPRIVESC,EXPOSURE,VULN,THREAT,DATA,CORRELATE}]
[--all-regions] [--compliance] [--graph FILE]
[--org] [--accounts IDS] [--assume-role ROLE]
[--external-id ID] [-v] [--version]
options:
--region REGION AWS region to audit (default: eu-west-1)
--json FILE Write JSON report to FILE
--html FILE Write HTML report to FILE
--sarif FILE Write SARIF 2.1.0 findings to FILE (GitHub code scanning)
--asff FILE Write ASFF findings to FILE (AWS Security Hub import)
--baseline FILE Diff against a previous JSON report (new/resolved)
--fail-on SEVERITY Exit 1 only on a FAIL at/above this severity
--output-dir DIR Directory for evidence artefact files
--sections SECTIONS Run only the named sections (single comma-separated value)
--all-regions Sweep every enabled region for regional sections
--flow-logs Enable the opt-in VPC Flow-Log observed-traffic overlay
(FLOW-01/02/03) in EXPOSURE — needs an optional resource-scoped
logs:StartQuery grant; bills per GB scanned; fails open to FLOW-00
--compliance Print the per-framework compliance scorecard
--graph FILE Write the identity security graph to FILE (graph.json)
--org Scan every ACTIVE account in the AWS Organization (needs --assume-role)
--accounts IDS Comma-separated account IDs to scan (needs --assume-role)
--assume-role ROLE Role name/ARN to assume in each target account
--external-id ID STS ExternalId for the assumed role
-v, --verbose Print each check as it runs
--version Show scanner version
| Section | Check IDs | Description |
|---|---|---|
| IAM | IAM-01/02, IAM-04/05/06/10 | Root MFA + access keys, console users without MFA, password policy, stale access keys, IAM Access Analyzer |
| S3 | S3-01, S3-03, S3-05 | Account-level Block Public Access, per-bucket public access + ACLs + encryption |
| VPC | VPC-01, VPC-03 to 06 | Security groups with risky ports open to 0.0.0.0/0, VPC Flow Logs, NACL admin-port exposure, cross-account peering |
| Network Micro-Seg | SEG-01 to 06 (Layer A), FLOW-01 to 03 (Layer B, opt-in) | Static SG micro-segmentation (config-only, zero new grant) — world-open sensitive/non-web ports, overly-wide ranges, redundant/shadowed rules, unused SGs, SG-chaining to a world-open group, unrestricted-egress exfil paths; observed-traffic overlay (--flow-logs) — evidence-based SG scope-down, allowed-but-unused port removal, reject-talker recon signals from VPC Flow Logs via CloudWatch Logs Insights (fail-open when the optional logs:StartQuery grant is absent) |
| Logging | LOG-01/03/04/05 | CloudTrail multi-region + validation, AWS Config, GuardDuty, Security Hub |
| KMS | ENC-03 | KMS customer-managed key rotation |
| EC2 | EC2-04/05/06 | IMDSv2, public IP, EBS volume encryption |
| ECR | CNT-01 | ECR scan-on-push |
| Backup | BCK-01 | AWS Backup vaults and resource assignments |
| RDS | RDS-01 to 06 | Encryption, public access, backups, deletion protection, monitoring, public snapshots |
| Glacier | GLC-01 to 03 | Vault access policies, vault lock (WORM), SNS notifications |
| SNS | SNS-01 to 04 | SSE-KMS encryption, wildcard principal, HTTPS delivery, cross-account subscriptions |
| SQS | SQS-01 to 04 | SSE encryption, public access, DLQ, retention/visibility |
| CloudFront | CFN-01 to 05 | HTTPS-only, TLS version, WAF, access logging, origin protocol |
| Route 53 | R53-01 to 05 | Query logging, DNSSEC, transfer lock, health checks, DNS firewall |
| Bedrock | BDR-01 to 05 | Model logging, guardrails, KMS encryption, VPC endpoint, IAM least privilege |
| Bedrock Agents | AGT-01 to 05 | Agent KMS encryption, execution role, KB security, Lambda security, guardrail attached |
| Lambda | LMB-01 to 05 | Public access, VPC config, plaintext secrets in env vars, deprecated runtimes, concurrency |
| EKS | EKS-01 to 08 | Public API endpoint, control plane logging, secrets encryption, version, security groups, worker-node SSH, EKS-Fargate profile boundary, authentication mode |
| KSPM | KSPM-00 to 07 | Agentless CIS-EKS (K8s side) — anonymous RBAC bindings, wildcard/cluster-admin RBAC, default-SA automount, Pod Security Admission, default-deny NetworkPolicy, privileged/host pods (fail-open when the K8s API is unreachable) |
| KIEM | KIEM-01 to 04 | K8s identity/entitlement — over-broad AWS→K8s cluster-admin grants (EKS Access Entries), namespace-admin/secret-read, and IRSA / Pod-Identity cross-plane (ServiceAccount → AWS role → admin/crown) |
| ECS | ECS-01 to 08, FARGATE-01/02 | Privileged containers, root user, log drivers, plaintext secrets, writable rootfs, host-namespace/hostPath escapes, dangerous caps; running Fargate tasks folded into the attack-path graph + public-task-IP exposure |
| Secrets Manager | SEC-01 to 04 | Rotation enabled, rotation frequency, KMS (CMK vs managed), unused secrets |
| WAF | WAF-01 to 04 | Web ACL presence, logging, rules count, default action |
| ElastiCache | ELC-01 to 04 | Encryption at rest, encryption in transit, AUTH token, auto failover |
| OpenSearch | OSR-01 to 05 | HTTPS enforcement, encryption at rest, node-to-node, VPC deployment, fine-grained access |
| DynamoDB | DDB-01 to 04 | CMK encryption, point-in-time recovery, billing mode, deletion protection |
| Step Functions | SFN-01 to 03 | Execution logging, X-Ray tracing, KMS encryption |
| API Gateway | APIGW-01 to 04 | Stage access/execution logging, WAF association, cache data encryption, X-Ray tracing |
| Load Balancing | ELB-01 to 05 | Access logging, HTTP→HTTPS redirect, TLS policy strength, deletion protection, drop invalid headers |
| EBS | EBS-01 to 04 | Encryption by default, unencrypted volumes, unencrypted snapshots, public snapshots |
| Redshift | RS-01 to 05 | Encryption at rest, public access, audit logging, enhanced VPC routing, default admin username |
| EFS | EFS-01 to 03 | Encryption at rest, in-transit TLS policy, automatic backups |
| Certificate Manager | ACM-01 to 03 | Certificate expiry, key algorithm strength, unused certificates |
| SageMaker | SM-01 to 07 | Notebook direct internet access, root access, KMS volume encryption, VPC deployment; Studio-domain public egress + home-EFS CMK; endpoint-config storage CMK |
| Cognito | COG-01 to 04 | User-pool MFA enforcement, password policy strength, advanced security (threat protection), deletion protection |
| API Gateway v2 | AGW2-01 to 03 | HTTP API stage access logging, route authorization, default throttling |
| IAM Privilege Escalation | IAMPE-01 to 20 | Resource-aware escalation-path analysis across all principals (findings scoped account-wide vs resource-scoped): policy-version/attach/inline-policy abuse, login-profile & access-key hijack, trust-policy edits, PassRole→(EC2/Lambda/Glue/CFN/SageMaker), UpdateFunctionCode, SSM, sts:AssumeRole-on-*, full-admin |
| DSPM (crown-jewel data) | DSPM-01/02/03 | Tag-based data classification (no Macie needed) for RDS/Aurora/DocDB/Neptune/Redshift/DynamoDB/EFS/OpenSearch/Kinesis/MemoryDB/FSx/Timestream — crown-jewel + public reachability + CAN_READ_DATA reader edges; public resource-policy exposure |
| AWS-resident secrets | SECRET-01/02/05 | Plaintext SSM String params that look secret, SecureString managed-key-vs-CMK, and a crown Secret node + the internet→role→secret reader path (metadata only — never a GetSecretValue) |
| AI-SPM | AISPM-01/02/03, AIPATH-01 | Blast radius of an AI resource's execution role — privilege-escalation-capable (AC-6), reaches crown data (AC-3), no network isolation (SC-7), and and the conditional AIPATH-01 (an AI resource that pairs unrestricted egress with a role that can escalate or read crown data — reported as capability, not as an observed route: no inbound reachability is claimed) |
Every finding is tagged with applicable controls from:
| Framework | Coverage |
|---|---|
| CIS AWS Foundations Benchmark v3.0 | IAM, S3, VPC, Logging, KMS, EC2, RDS, CloudFront, Lambda, EKS |
| PCI DSS v4.0 | Requirements 1, 2, 3, 4, 6, 7, 8, 10, 11, 12 |
| HIPAA | 164.308, 164.312 (access control, audit, transmission, encryption) |
| SOC 2 Type II | CC6 (logical access), CC7 (monitoring), A1 (availability) |
| NIST 800-53 Rev 5 | AC, AU, CM, CP, IA, SC, SI families |
The scanner computes a posture score from 0-100:
Score = 100 − (CRITICAL × 15 + HIGH × 5 + MEDIUM × 2 + LOW × 0.5)
| Grade | Score Range |
|---|---|
| A | 90 -- 100 |
| B | 80 -- 89 |
| C | 70 -- 79 |
| D | 60 -- 69 |
| F | 0 -- 59 |
The live scanner emits SARIF 2.1.0 (GitHub code scanning) and ASFF (AWS
Security Hub), and gates pipelines via --fail-on:
# .github/workflows/aws-audit.yml
jobs:
aws-security-audit:
runs-on: ubuntu-latest
permissions:
security-events: write # required to upload SARIF
steps:
- uses: actions/checkout@v4
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::<ACCOUNT>:role/security-audit
aws-region: eu-west-1
- run: pip install boto3
- run: python -m engine.aws_live_scanner --sarif results.sarif --fail-on HIGH
- uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: results.sarif- SARIF maps severity → level (CRITICAL/HIGH →
error) and setssecurity-severityso findings surface in the GitHub Security tab. - ASFF imports into Security Hub:
python -m engine.aws_live_scanner --asff f.json && aws securityhub batch-import-findings --findings file://f.json(≤100/call).
Shift-left IaC gate (no cloud, no hub). The offline IaC scanner
(aws_offline_scanner.py) is the charter-clean PR gate — pure local static analysis, no AWS
credentials, no network. It emits SARIF 2.1.0 with a real file:line (annotations land on the
offending Terraform/CFN line, not a file-level note) and gates via --fail-on <severity> and
--policy <file> (the pure-Python policy engine over the findings). The
overwatch-iac-gate composite Action wraps it — egress-free
(the opposite of the overwatch-image-scan ingest action: it POSTs nothing):
- uses: ./.overwatch/.github/actions/overwatch-iac-gate
with: { path: infra/, fail-on: HIGH, policy: infra/policies.json }Exit codes: 0 clean · 1 gate breach (a finding ≥ threshold or a fired policy) · 2
usage/environment error. An ide/ VS Code reference stub runs the same offline scanner
on the open file and shows the findings inline as diagnostics (shell-only, network-clean).
- Drift tracking:
--baseline prev.jsonprints only NEW and RESOLVED findings.
OverWatch is the product name for this CNAPP: the attack-path-graph layer that collapses a whole AWS estate into the ranked handful of internet → exposed workload → exploitable CVE → over-privileged role → crown-jewel-data paths, and names the choke point that severs the most.
Beyond the CLI, OverWatch ships a self-hosted platform backend for onboarding and continuously scanning many AWS accounts — the Wiz/Orca model, agentless and with no access keys.
One hub (an EC2 instance running the FastAPI
service + worker + Postgres/SQLite state, in a dedicated security account) reaches
every onboarded spoke account by assuming a read-only cross-account role
(CnappScannerRole) that trusts only the hub role under a per-account sts:ExternalId
(confused-deputy guard). Region/AZ are irrelevant to the API scan — control-plane
endpoints are reachable from anywhere; the hub's own AZ never constrains coverage.
Deploy the single-account CloudFormation stack
(deploy/cnapp-scanner-role.yaml) via a one-click
Launch-Stack URL, apply the equivalent Terraform module
(deploy/terraform/scanner-role/ — a structural test keeps it
byte-equivalent to the CFN), or connect an entire AWS Organization with a service-managed
StackSet (deploy/cnapp-stackset.md) that auto-enrolls
current and future member accounts. The role attaches only SecurityAudit +
ViewOnlyAccess (read-only of configuration/IAM — never workload data).
OverWatch makes zero telemetry / phone-home / update-check
calls — every egress is an AWS API or an operator-configured opt-in seam (see
NETWORK.md, enforced by tests/test_zero_telemetry.py). It packages for fully
offline deployment: a multi-stage Dockerfile that installs from a pre-downloaded
wheelhouse (--no-index), scripts/build_offline_bundle.sh
(one transfer tarball), and docs/AIRGAP_RUNBOOK.md (AWS via VPC
endpoints, vuln bundle from disk). Listed on AWS Marketplace as a self-hosted container product
(deploy/marketplace/), priced on accounts under management (metered) with
a contract SKU for air-gapped buyers.
Pure, dependency-injected, offline-testable:
| Module | Role |
|---|---|
cnapp_onboarding.py |
Mint ExternalId (stored as a secret reference), build the CFN launch URL/CLI |
cnapp_validate.py |
validate_connection: assume → account-match hard stop → read canary → org list; health + backoff |
cnapp_registry.py |
AccountRegistry (accounts / scan_jobs / connection_health) over the dual-dialect state store |
cnapp_service.py |
PlatformService facade + serialize_scanner + org rollup |
cnapp_worker.py |
Async scan-job execution (traps engine exit, pre-validates creds, TOCTOU re-check) |
cnapp_api.py |
FastAPI routes + workspace-scoped RBAC (fail-closed; Principal from an injected IdP-claims hook or the legacy-role shim; account_gate tenant isolation → 404), guarded import |
cnapp_workspace.py |
Multi-tenancy (MSSP) — WorkspaceStore: workspaces / members / platform-admins CRUD + the account↔workspace binding read helpers the isolation gate uses |
cnapp_metering.py |
Usage metering — fail-open MeteringStore (append-only, exactly-once ledger); billable = accounts under management (account.active monthly gauge) + idempotent reconcile |
cnapp_connectors.py |
Connector framework — route findings to Jira / Slack / PagerDuty / Splunk / webhook (pure renderers + injected http_post seam + rules engine + idempotent delivery ledger) |
compliance_crosswalk.py |
Compliance breadth — sourced NIST 800-53 → 30+ framework crosswalk loader (accuracy-gated, fail-open); aws_live_scanner.crosswalk_scorecard derives per-framework coverage |
aws_ingest.py |
External-vuln ingest — pure SARIF/CycloneDX/SPDX parsers (per-tool adapters) → own each CVE onto a graph node → enrich from OverWatch's own OSV/EPSS/KEV bundle → re-run reachability so CVEs rank by attack-path exploitability, not CVSS |
aws_copilot.py |
Grounded-RAG copilot — self-contained BM25 retrieval over the account's own scan (findings / paths / choke points); extractive-by-default (abstains rather than hallucinating), optional injected LLM seam |
aws_aispm.py |
AI-SPM pillar — pure classifiers for the blast radius of an AI resource's execution role (privesc-capable / reaches-crown-data / no-network-isolation) fused onto the graph post-clobber; emits AISPM-01..03 + the conditional AIPATH-01 |
aws_cdr.py |
CDR-lite — normalize GuardDuty / Security Hub ASFF / CloudTrail-anomaly detections, fold onto the stored graph as THREAT_ON, re-run reachability so a detection on an internet→crown/admin path escalates to a ranked incident (reuses aws_ingest predicates; no aws_correlate change) |
aws_edr.py |
Runtime-sensor (EDR/CWPP) ingest — the in-charter substitute for a runtime sensor: normalize the customer's EXISTING CrowdStrike / Falco / GuardDuty-Runtime / OCSF output → detections fold through the aws_cdr THREAT_ON/incident path; a sensor-inventory feed drives runtime_monitored coverage. Pure; reuses aws_cdr/aws_correlate read-only (no sensor, no aws_correlate edit) |
aws_malware.py |
Malware-finding ingest — normalize the customer's EXISTING malware scan output (Amazon GuardDuty Malware Protection, ClamAV, YARA) → fold as THREAT_ON → reachability-ranked malware incident (MAL-01/02/03, incl. the malware∩DSPM cross-engine hit in a crown store). EICAR/test-signature suppression. Pure; reuses aws_cdr. Native scanning over file bytes stays deferred (EBS filesystem parse) |
aws_dspm.py |
DSPM read-surface — normalize the already-collected sensitivity signal (Macie score / classification tags) into data_types (PII/PCI/PHI/…) + a tier at read time, and compute the crown-jewel data inventory + exposure + a classification-coverage gap over the STORED graph. Pure; no new scan, no file-content read, aws_correlate untouched |
aws_forensics.py |
Cloud-forensics timeline — reconstruct who-did-what-when around a resource from read-only CloudTrail management events, correlated with graph / findings / detections; fail-open FORENSIC-00 behind an injected seam |
aws_wql.py |
WQL graph query — a typed, bounded JSON query language compiled to the frozen aws_graph traversal primitives (no free Gremlin / Neptune / eval). parse() is the security boundary (rejects any unknown field/op/predicate/prop → WQLError); evaluate() returns deterministic (kind,id)-sorted node rows. Mirrored byte-for-byte in frontend/src/lib/wql.ts for SAMPLE==LIVE, guarded by a cross-language parity fixture |
aws_controls.py |
Saved-query-as-Control — pure transform of a matched WQL result into a display-only WARN finding (status=WARN → never touches the FAIL-only posture score); mirrored in frontend/src/lib/controls.ts |
aws_policy.py |
Policy-as-code engine (the in-charter Rego/OPA substitute — a Go binary would break zero-telemetry/air-gap) — a closed, typed pure-Python DSL extending Controls with a finding-catalog predicate (compliance-as-code: match by check_id/section/severity/compliance) + a graph clause (WQL) combined any/all; a firing rule overlays a display-only WARN POLICY-xx finding. Config-driven (CNAPP_POLICIES); mirrored in frontend/src/lib/policy.ts |
aws_registry_oci.py |
Non-AWS OCI registry pull adapter — pure Docker Registry v2 client (parse-ref / WWW-Authenticate Bearer dance / manifest + manifest-list / tag enumeration / layer-blob pull) driven entirely by injected aws_layer_fetch seams; fail-closed on a partial rootfs (zstd/undecodable/truncated/over-budget); covers GHCR / Docker Hub / Harbor / ACR |
aws_registry_connectors.py |
Registry-connector config layer — parse/validate CNAPP_REGISTRIES, resolve a secretsmanager:///ssm:// secret-ref to credentials transiently, host-qualify + host-consistency-guard refs, orchestrate enumerate→pull→side-scan, and mask the secret out of API views. Pure |
All routes delegate to PlatformService; admin routes stay on the private
hub control plane. POST /accounts (onboard → launch URL), POST /accounts/{id}/validate,
GET /accounts, POST /scans, GET /scans/{job_id},
GET /accounts/{id}/summary|issues|findings|paths|graph, GET /org/overview, GET /org/findings;
graph query (WQL) — POST /accounts/{id}/graph/query (typed query → matching nodes; malformed → 400),
GET /accounts/{id}/graph/blast-radius, GET /projects[/{id}], GET /controls, GET /policies;
copilot — POST /accounts/{id}/copilot, POST /org/copilot;
ingest — POST /accounts/{id}/ingest, GET /accounts/{id}/vulns[/{cve}], GET /org/vulns;
CDR — POST /accounts/{id}/detections, GET /accounts/{id}/{detections,incidents},
POST /accounts/{id}/detections/refresh, GET /org/incidents;
runtime (EDR) — POST /accounts/{id}/edr/ingest (ingest tier; sensors + detections),
GET /accounts/{id}/edr/coverage, GET /org/edr/coverage;
malware — POST /accounts/{id}/malware/ingest (ingest tier; GuardDuty-Malware / ClamAV / YARA);
data (DSPM) — GET /accounts/{id}/data/inventory, GET /org/data/inventory;
forensics — GET /accounts/{id}/forensics/timeline;
supply-chain — GET /accounts/{id}/sbom/{subjects,snapshots,diff},
GET /accounts/{id}/components, GET /accounts/{id}/license-findings, GET /accounts/{id}/vex;
non-AWS registries — GET /registries (secret-masked), GET /registries/images (cached rows),
POST /registries/{id}/scan (admin — a live agentless pull + side-scan);
connectors — POST/GET /connectors, PUT/DELETE /connectors/{id},
POST /connectors/{id}/{enable,rotate-secret,test}, .../rules CRUD,
POST /connectors/rules/preview (dry-run), POST /accounts/{id}/notify,
GET /connectors/{id}/deliveries, GET /notifications;
multi-tenancy — POST/GET /workspaces, GET/PUT/DELETE /workspaces/{ws},
GET/POST /workspaces/{ws}/members, DELETE /workspaces/{ws}/members/{principal},
GET/POST/DELETE /admin/platform-admins, GET /workspaces/{ws}/usage,
GET /admin/usage, POST /admin/usage/reconcile.
One hub serves many tenant workspaces, each isolated (a principal in
workspace A can never read/write/scan/meter workspace B — a cross-tenant object returns 404) and
metered (billable = accounts under management). RBAC is workspace-scoped: an injected
current_principal hook maps an authenticated caller (IdP/JWT claims) to {workspace_id: role}
memberships + an optional platform-superadmin; the target workspace travels in an X-Workspace-Id
header. A default workspace holds every account of a single-tenant deployment, so the console
and the legacy single-role auth hook keep working unchanged.
Set a per-account scan cadence (hourly /
daily / weekly / custom) and OverWatch scans on a schedule — a scheduler_tick enqueues due
accounts (a failing scan backs off exponentially, never flapping) and drains the queue. Each
scan folds into the finding-lifecycle store (drift / trend / MTTR), and one drift digest
— "what changed since last scan" (new / resolved / reopened, posture Δ, newly-exposed on an
attack path, SLA breaches, compliance drift) — is delivered per scan through the connectors, to
the connectors that opt in (config.digest). Digests are idempotent per window (a separate
digest_log ledger; a failed one is retried) and gated to material change so a quiet scan
sends nothing. The console shows a "What changed" + posture-trend card, a per-account cadence
selector, and a digest toggle per connector. Routes: POST /scans/schedule-tick,
PUT /accounts/{id}/schedule, GET /accounts/{id}/{trend,mttr,drift}, GET /digests,
POST /accounts/{id}/digest/preview.
Every check is tagged
with a NIST 800-53 Rev 5 control, so instead of hand-tagging every check against every
regime, OverWatch cross-walks its 38 in-use NIST controls to 34 more frameworks (ISO
27001:2022, FedRAMP, NIST 800-171 / CMMC, NIST CSF 2.0, PCI-DSS 4.0, SWIFT, DORA, HIPAA,
HITRUST, GDPR/CCPA, CSA CCM v4, CIS Controls v8, COBIT, NIS2, ATT&CK-Cloud, …) and derives
coverage transitively — each derived control reads "satisfied via NIST 800-53 AC-3
(crosswalk)" with a per-mapping confidence tier and authoritative source citations.
The crosswalk was sourced from published authoritative mappings and web-verified by an
adversarial review (which caught + fixed real mapping errors); a CI accuracy validator gates
the shipped data (no fabricated ids, no native-framework targets). The 5 directly-tested
frameworks keep their per-check tags and are byte-identical; derived data is informational
(confirm scope with your assessor). Routes (viewer): GET /compliance/frameworks,
GET /compliance/crosswalk, GET /accounts/{id}/compliance?min_confidence=, GET /org/compliance.
Surfaced in the console Compliance screen (directly-tested vs crosswalk-derived, family +
confidence filters, per-control provenance, source links).
Notify your own tools (agentless, read-only on targets). OverWatch can
route findings to the operator's own Jira Cloud, Slack, PagerDuty (Events v2), Splunk
HEC, or a signed generic webhook, under a rules engine (severity floor / section / check
glob / account glob / on-attack-path / framework). It makes no AWS call against a
scanned account — the only outbound is HTTP to the operator's endpoints. Every credential is
stored only as a secretsmanager:///ssm:// reference (never plaintext, never returned
over the API); connectors are admin-only, disabled by default, and delivery is
idempotent (a re-scan never re-sends an unchanged finding, and a failed send is retried).
Configured from the console Settings → Integrations screen (add/test/enable, per-connector
routing rules with a dry-run preview, and a deliveries audit log).
Rank CVEs by reachability, not CVSS. Upload the
output of any SCA scanner — SARIF (Trivy / Grype / Snyk), CycloneDX (with a
vulnerabilities[] array or VEX analysis.state), or SPDX / Syft SBOMs — and OverWatch
owns each CVE against the AWS resource it belongs to, dedups it across sources (one
row per (account, node_id, cve); N reporters = a sources set-union), and enriches it
from the same OSV / EPSS / KEV bundle a native side-scan uses (so KEV/EPSS/exploit are
byte-identical, never inferred from the doc). The owned CVE is attached as a HAS_VULN edge
and the attack-path engine is re-run — a membership check on the stored paths would
structurally miss the path a newly-reported KEV reveals — so an ingested KEV on an
internet-exposed, path-to-crown host earns the identical CRITICAL as a native one, while a
high-CVSS but unreachable CVE ranks as noise. VEX not_affected / false_positive is
suppress-but-track (owner VEX outranks a scanner's "exploitable"; the row is retained and
counted, never a silent hole); CodeQL SARIF (SAST, no CVE) is excluded. Reachable survivors
flow into the existing connectors as two check-level aggregates (VULN-ING-KEV / VULN-ING)
and a newly-reachable KEV rides the drift-digest newly_on_path signal. Read-only on scanned
accounts — it works purely off the uploaded doc + the stored graph. Routes: POST /accounts/{id}/ingest (admin), GET /accounts/{id}/vulns (ranked, faceted by KEV / on-path /
source / band), GET /accounts/{id}/vulns/{cve}, GET /accounts/{id}/ingest/docs, POST /accounts/{id}/vulns/refresh (admin), GET /org/vulns. Surfaced in the console
Vulnerabilities screen — a reachability-anchored inventory (the reachability chip, not
CVSS, is the visual weight) with a SARIF/CycloneDX/SPDX upload affordance and an Overview
roll-up (N external CVEs · M reachable · K reachable-KEV).
The same ingest seam also owns the software bill of materials itself, not just the
CVEs inside it. Each uploaded CycloneDX / SPDX / Syft SBOM is persisted as an
account-scoped snapshot (aws_ingest.py walks the doc — including bom-ref-less and
nested components — and mints a canonical purl identity per component), so OverWatch can
answer three supply-chain questions no single scan can:
- SBOM diff over time (
aws_sbom_diff.py) — compare two snapshots of the same subject (image / repo) and get added / removed / changed components, where "changed" surfaces both version and license transitions. Snapshots are keyedf"{account}:{doc_id}"so two accounts pushing the same image never collide, and a tie on ingest epoch breaks deterministically (newestsnapshot_idwins). - License policy (
aws_license.py) — every component's declared license is normalized to an SPDX id, bucketed into a category (permissive / weak-copyleft / strong-copyleft / network-copyleft / proprietary / unknown), and run against a policy (deny / warn / allow) whose compoundAND-expressions are parenthesized so a mixed(MIT AND GPL-3.0)clause can't be silently mis-judged. Surfaced as license findings per account. - VEX (
aws_vex.py) — ingest OpenVEX and CSAF-VEX documents; a vendornot_affected/fixedstatement is applied product-wide only (never purl-blindly, so one component's VEX can't over-suppress an unrelated CVE), recorded to avex_statementsledger, and folded into the same suppress-but-track path as inline CycloneDX VEX.
State lives in four new tables (sbom_snapshots, sbom_components, sbom_snapshot_cves,
vex_statements; SCHEMA_VERSION 8, dual sqlite/Postgres). Because CI/CD pipelines push
SBOMs with a machine token, ingest gets its own RBAC tier below admin
(viewer < ingest < admin): a pipeline can POST /accounts/{id}/ingest but can't read another
tenant's graph or touch connectors. A shell-only GitHub Action
(.github/actions/overwatch-image-scan/) builds an
image SBOM and pushes it in one CI step. Read routes (viewer):
GET /accounts/{id}/sbom/{subjects,snapshots,diff}, GET /accounts/{id}/components,
GET /accounts/{id}/license-findings, GET /accounts/{id}/vex. Surfaced in the console
Supply Chain screen (snapshot picker + diff, component inventory with license chips,
license-policy findings, VEX ledger).
Agentless ECR registry enumeration + opt-in layer-pull. Beyond CI-pushed SBOMs, OverWatch
scans the registry itself. Tier A (always on, no new grant) widens the native ECR
scan-finding sweep from the newest image to the newest-N tagged images per repo (bounded per
repo + a per-scan aggregate budget). Tier B (opt-in two-key: the --side-scan-images
flag and the CnappImageLayerPull grant — repo-scoped, tag-gated on cnapp:imagescan) pulls
image layers, reconstructs the rootfs, and runs OverWatch's own SBOM→OSV pipeline
(Inspector-independent) — fail-closed on a partial/corrupt rootfs, and the layer bytes are
fetched through one hardened, allowlisted aws_layer_fetch.http_get (HTTPS + *.amazonaws.com
only, re-validated on every redirect). Native (ecr-native-scan) and own-SBOM (ecr-sidescan)
CVEs converge on the same ECRImage node (split by a scan_source chip) and persist as
diffable snapshots; a registry-only image (not deployed) carries HAS_VULN but never enters
an attack path — it ranks shift-left, never a false CRITICAL (aws_correlate.py stays frozen).
Read routes GET /accounts/{id}/registry/{repos,images}; a Registry tab on the Supply Chain
screen (deployed vs registry-only, scan-source chips, a "not reachable" label).
Non-AWS registry connectors (GHCR / Docker Hub / Harbor / ACR). The same agentless pull +
own-SBOM→OSV pipeline extends to a customer's non-AWS OCI registries. An operator declares a
connector in CNAPP_REGISTRIES ([{connector_id,type,host?,auth?,username?,secret_ref?,images?, repositories?}], JSON or file path; disabled-by-default; a credential is a secretsmanager:// /
ssm:// reference, never plaintext) and aws_registry_oci.py speaks the standard Docker
Registry v2 Bearer-realm token dance — one adapter covers GHCR, Docker Hub, Harbor, and ACR
(including ACR's service-principal, which authenticates as plain Basic), so there is no
per-registry auth code; GCR / Artifact Registry is deferred (its OAuth needs RS256 signing, absent
from the pure-Python air-gap wheelhouse). The pull reuses the SAME hardened egress file
(aws_layer_fetch.registry_request / registry_blob_get) — so the zero-telemetry allowlist does
not grow — under a per-call host allowlist (the config host + the auth realm learned at
runtime; never "any https"); a blob 3xx is followed only to a non-SSRF-target HTTPS host
(IMDS / cloud-metadata / loopback / any private address refused, IP-literal encodings normalised),
and the Authorization header is stripped on any cross-host hop. Pulled images run through the
registry-agnostic scan_pulled_layers (shared with ECR, so the fail-closed-on-partial-rootfs
contract can't diverge — a zstd/undecodable/truncated layer set fails closed, never a false
clean), and a host-consistency guard refuses any ref that resolves off the connector's host so a
credential can never travel to a third-party registry. Results are display-only (never posture
or the frozen attack-path graph). Routes GET /registries, GET /registries/images,
POST /registries/{id}/scan (admin); a Registries console screen (aws_registry_connectors.py
config layer; frontend/src/routes/Registries.tsx).
Opening the state store with a postgresql:// URL runs
the whole state plane (finding lifecycle/drift/waivers + the account registry) on a
real Postgres via psycopg3 — the shared store for the hub. Both stores route through
one Backend abstraction (cnapp_backend.py); the sqlite path is byte-identical and
a missing driver / unreachable server fails loudly rather than falling back to a
local file.
WQL gives the security graph a queryable surface without a Gremlin/Neptune backend (both
would break the self-hosted / air-gap charter). A query is a closed, typed JSON object —
no free text, no regex, no eval — compiled by aws_wql.py to the frozen aws_graph
traversal primitives. parse() is the security boundary: any unknown field, op, predicate, or
non-whitelisted prop is rejected with WQLError (→ HTTP 400), and limit/max_hops are
clamped, so a query can never escape the grammar or run unbounded.
Predicates: kind · has_prop · prop (eq/neq/in/gt/gte/lt/lte/exists/contains) ·
prop_glob · has_edge (in/out) · reachable_from internet · reaches crown|admin ·
crown_jewel, composed with and/or/not. evaluate() returns deterministic,
(kind,id) code-point-sorted node rows. POST /accounts/{id}/graph/query runs it live;
the whole compiler is mirrored byte-for-byte in frontend/src/lib/wql.ts so SAMPLE mode
(querying the graph fixture client-side) returns identical rows — a cross-language parity
fixture (scripts/gen_wql_parity.py → wql_parity.json) fails CI on any drift.
Controls turn a saved WQL query into governance: configure CNAPP_CONTROLS
([{id,name,query,severity?,description?}], a JSON list or file path; fail-safe) and each
matching query overlays a display-only WARN finding into the catalog. Controls are read
at request time and are strictly display-only — they never re-run scoring (the FAIL-only
posture score stays baked at scan time) and never trigger notifications. GET /controls
returns the org roll-up (match count per account, WARN/PASS), and the console's Query
screen is a guided predicate-builder + raw-JSON editor + saved-Controls library over it.
Policy-as-code (aws_policy.py) is the custom-rule engine — the in-charter substitute for
Rego/OPA (a Go binary would break the zero-telemetry / air-gap charter). A policy is a closed,
typed JSON rule (config-driven via CNAPP_POLICIES) that combines a graph condition (a WQL
query) and/or a finding-catalog condition — the new compliance-as-code surface: match by
check_id glob / section / severity / status / compliance framework+control, e.g. "every check
mapped to PCI-DSS 3.4 must PASS". The two clauses combine any/all; a firing rule overlays a
display-only WARN POLICY-xx finding (never touching the posture score). GET /policies
returns the org roll-up, and policy.ts mirrors the engine for SAMPLE==LIVE. The same engine
gates PRs offline via aws_offline_scanner.py --policy (see CI/CD).
OverWatch is agentless — it never deploys a runtime/eBPF sensor (that would break the
zero-telemetry charter). Instead it ingests the customer's EXISTING runtime sensor and
correlates it onto the attack-path graph — the in-charter substitute for runtime protection.
Two push-ingest feeds via POST /accounts/{id}/edr/ingest (machine ingest tier):
- Detections (CrowdStrike Falcon · Falco · GuardDuty Runtime Monitoring · generic OCSF 2004)
normalize to the shared model and fold through the
aws_cdrTHREAT_ONpath (out ofE_PATH, soaws_correlatestays byte-frozen): a runtime alert on an internet→crown/admin workload escalates to a ranked incident (EDR-02, its own "Runtime" section, never mislabeled as a GuardDuty cloud threat). - Sensor inventory ("which workloads have a healthy sensor right now") drives a
runtime_monitorednode prop, overlaid at read time (a side table —graph_fullstays the pristine scan seed). This powersGET /accounts/{id}/edr/coverage(monitored/total per kind- the UNMONITORED workloads ranked by attack-path exposure) and the Runtime console screen,
and it finally makes the Batch-2 WQL query real: "workloads with no endpoint sensor" now
returns the true gap set (
EDR-01, a display-onlyWARN— coverage gaps never tank posture).
- the UNMONITORED workloads ranked by attack-path exposure) and the Runtime console screen,
and it finally makes the Batch-2 WQL query real: "workloads with no endpoint sensor" now
returns the true gap set (
It substitutes for runtime detection and coverage visibility; it explicitly does not do real-time blocking, in-kernel telemetry, or DAST — it makes your EDR reachability-aware and tells you exactly which workloads it's blind to.
Both engines ship the buildable-now, agentless surface; native scanning over file bytes stays
deferred behind the pre-committed EBS-filesystem-parse seam (aws_sidescan_fs.DissectExtractor).
- DSPM (
aws_dspm.py) — OverWatch already classifies crown-jewel data stores agentlessly during a scan (Amazon Macie automated scores + data-classification tags →crown_jewel+sensitivity, across 13 store kinds, withCAN_READ_DATAreader edges). This is the read-time surface over that signal: it normalizes the heterogeneoussensitivityinto explicitdata_types(PII/PCI/PHI/FINANCIAL/SECRET) + asensitivity_tier, and computes the sensitive-data inventory + exposure + a classification-coverage gap (crown stores whose data type is unknown — the honest "enable Macie / tag it" signal,DSPM-GAPWARN) — all from the STORED graph, no new scan,aws_correlateuntouched. The new props are WQL-queryable (GET /accounts/{id}/data/inventory+ the Data Security console screen), so "publicly exposed stores holding PII" is a real query. - Malware (
aws_malware.py) — ingest the customer's EXISTING malware scan output (GuardDuty Malware Protection, ClamAV, YARA; EICAR-suppressed) → fold asTHREAT_ON→ reachability-ranked malware incident:MAL-01(on a critical attack path),MAL-02(on a reachable workload),MAL-03(a malicious object IN a Macie-classified crown store — the malware∩DSPM win). It reuses the exactaws_cdrincident spine;aws_correlatestays byte-frozen. OverWatch running its own YARA/ClamAV over file bytes is the piece that waits on the filesystem parse.
A React 19 + Vite + TypeScript + Tailwind SPA over
the hub API — the operator surface, styled to match the exported HTML report. Twelve
screens over one design system, led by Overview (posture dashboard, org ↔ account scope),
Attack Paths (ranked toxic-combination worklist + an interactive graph explorer
showing each path's gated-multiplicative score breakdown), Findings (unified
deduped queue with source sub-tabs + a risk → business-impact → step-by-step
remediation detail panel), and Cloud Accounts with a keyless 5-step onboarding
wizard (server-minted ExternalId + CloudFormation / Org StackSet) — plus
Vulnerabilities, Runtime (EDR sensor coverage + ranked blind-spots + runtime & malware
incidents), Data Security (DSPM sensitive-data inventory — data types, exposure, readers +
classification gaps), Supply Chain, Inventory, Query (WQL console — guided predicate
builder + raw JSON + saved Controls), Projects, Identity, Compliance, Remediation,
Reports, and Settings → Integrations. Every view is a
shareable deep link — scope and each open panel live in the URL, so a pasted
/attack-paths?scope=…&path=… reopens exactly what you were looking at — and an
interactive product tour replays five canned scenarios over the live console
(navigating, setting scope, spotlighting real elements, opening real panels) for a
zero-AWS guided walkthrough. It runs on engine-shaped sample fixtures with zero
AWS, and a VITE_DATA_SOURCE=live build flips every screen to the live hub.
cnapp_api.create_hosted_app(service, static_dir="frontend/dist") serves the API under
/api and the SPA at / (with a history-API fallback) as one deployable. See
frontend/README.md.
cd frontend && npm install && npm run dev # http://localhost:5173 (sample data, no AWS)Status: backend + onboarding + validation + registry + scan orchestration + live Postgres state + the web console (Overview / Attack Paths / Findings / Vulnerabilities / Supply Chain / Cloud Accounts + onboarding wizard, plus Inventory / Identity / Compliance / Remediation / Reports / Settings — with shareable deep links and an interactive product tour) + the connector framework (Jira / Slack / PagerDuty / Splunk / webhook + Settings screen) + compliance breadth (30+ frameworks via the NIST-800-53 crosswalk) + continuous scheduled scanning + drift digests (CTEM cadence + lifecycle drift/trend/MTTR + per-scan digests through the connectors) + reachability-verified vulnerability ingest (SARIF / CycloneDX / SPDX) + supply-chain ingest (SBOM diff + SPDX license policy + OpenVEX/CSAF-VEX, with an ingest-only RBAC tier and a GitHub Action) + agentless ECR registry enumeration + opt-in layer-pull (Tier-A native scan findings across all tagged images; Tier-B own-SBOM CVEs behind a two-key grant, converging on one node and persisting as diffable snapshots) shipped. Remaining: a Postgres connection pool.
| Scenario | Recommended Scanner |
|---|---|
| Pre-deployment IaC review (CloudFormation / Terraform) | IaC Scanner (aws_offline_scanner.py) |
| Live AWS account security audit | Live Scanner (aws_live_scanner.py) |
| CI/CD pipeline gate for infrastructure code | IaC Scanner |
| Compliance assessment against CIS AWS Benchmark | Live Scanner |
| No AWS credentials available, only code to review | IaC Scanner |
| Comprehensive audit of a production account | Both -- IaC Scanner on templates, Live Scanner on live account |
AWS-Security-Scanner/
├── engine/ # the scanning engine — collection, checks, correlation, scoring
│ ├── aws_offline_scanner.py # IaC Security Scanner (CloudFormation + Terraform, no credentials)
│ ├── aws_live_scanner.py # Live Audit Scanner v3.0.0 (44 sections, graph, exposure+L7, deep-plane, correlate, effperm, state, ciem, sidescan, KSPM/KIEM, flow-logs, backends, remediate, codetocloud, finding-detail, engine-EOL, winvuln, DSPM, secrets, least-priv, AI-SPM, CDR, forensics, copilot, vuln-ingest, supply-chain, registry)
│ ├── aws_remediate.py # Remediation engine — prioritized plan + remediation-as-code + exports, pure (read-only)
│ ├── aws_codetocloud.py # Code-to-cloud — IaC index + tiered T1–T5 matcher (TF/CFN → finding source), pure
│ ├── aws_graph_neptune_loader.py # Neptune live loader — bulk-load/openCypher runners (mock-tested), pure builders
│ ├── aws_exposure.py # Internet-reachability oracle — 4-gate AND, pure/testable (stdlib)
│ ├── aws_deepplane.py # Deep-plane parsers (Inspector/Macie/GuardDuty/AA) + CAN_READ_DATA (stdlib)
│ ├── aws_correlate.py # Attack-path correlation engine — enumerate/score/rank/choke (stdlib)
│ ├── aws_graph.py # SecurityGraph — nodes/edges, bounded traversal, graph.json (stdlib)
│ ├── aws_effperm.py # Effective-permissions solver — identity∩boundary∩SCP, deny-wins (stdlib)
│ ├── aws_unused.py # CIEM unused-access/right-sizing — Access-Analyzer+SLAD, dormancy (stdlib)
│ ├── aws_sidescan.py # Agentless CWPP core — inventory + dpkg/rpm/apk vercmp + OSV match + secrets → HAS_VULN (stdlib)
│ ├── aws_sidescan_ebs.py # EBS Direct-API block plane — plan/delta/checksum/sparse/cleanup (stdlib; live I/O deferred)
│ ├── aws_graph_neptune.py # Neptune export — Gremlin bulk-CSV + openCypher MERGE + round-trip (stdlib)
│ ├── aws_kube.py # Agentless KSPM/KIEM — CIS-EKS + K8s RBAC via read-only K8s API, cross-plane pod→AWS-role (stdlib)
│ ├── aws_flowlog.py # VPC Flow-Log overlay — observed-traffic SG scope-down/unused-port/reject-talker, opt-in (stdlib)
│ ├── aws_secrets.py # AWS-resident secrets — SSM/Secrets-Manager posture classifiers (metadata only, pure)
│ ├── aws_leastpriv.py # Least-privilege policy generation — GRANTED-minus-USED right-sizing from SLAD, pure
│ ├── aws_ingest.py # External-vuln ingest — SARIF/CycloneDX/SPDX parsers → own onto graph → reachability re-rank, pure
│ ├── aws_copilot.py # Grounded-RAG copilot — self-contained BM25 over the scan's own corpus, extractive/abstains, pure
│ ├── aws_aispm.py # AI-SPM — AI execution-role blast-radius classifiers (privesc/reaches-crown/no-net-iso), pure
│ ├── aws_cdr.py # CDR-lite — normalize GuardDuty/ASFF/CloudTrail detections → THREAT_ON → reachability-ranked incidents, pure
│ ├── aws_edr.py # Runtime-sensor (EDR/CWPP) ingest — CrowdStrike/Falco/GuardDuty-Runtime/OCSF → detections + runtime_monitored coverage, pure
│ ├── aws_malware.py # Malware-finding ingest — GuardDuty-Malware/ClamAV/YARA → THREAT_ON → MAL-01/02/03 incidents (malware∩DSPM), pure
│ ├── aws_dspm.py # DSPM read-surface — normalize sensitivity → data_types/tier + crown-jewel data inventory + classification-gap, pure
│ ├── aws_forensics.py # Cloud-forensics timeline — read-only CloudTrail events correlated with graph/findings/detections, pure
│ ├── aws_wql.py # WQL — typed, bounded JSON query language compiled to the frozen aws_graph primitives (parse = security boundary), pure
│ ├── aws_controls.py # Saved-query-as-Control — matched WQL result → display-only WARN finding, pure
│ └── aws_policy.py # Policy-as-code — pure typed DSL (compliance-as-code + graph clause) → POLICY-xx WARN, pure
├── store/ # persistence — imported by both, imports neither
│ ├── aws_state.py # Persistent state store — lifecycle/drift/MTTR/waivers (pure sqlite3)
│ └── aws_state_dialect.py # Postgres/SQLite dialect — DDL/upsert/parse_state_url/row-shim (stdlib)
├── ide/ # VS Code reference stub (shift-left IaC diagnostics via the offline scanner) — spec + stub, unpublished
│ ├── cnapp_onboarding.py · cnapp_validate.py · cnapp_registry.py · cnapp_service.py · cnapp_worker.py · cnapp_api.py · cnapp_connectors.py · cnapp_secrets.py # Hosted platform backend
│ └── (cnapp_backend.py lives in store/ with aws_state — the persistence trio is mutually recursive)
├── frontend/ # OverWatch web console — React 19 + Vite + TS + Tailwind SPA (Overview / Attack Paths / Findings / Cloud Accounts + onboarding wizard)
├── deploy/ # CloudFormation scanner-role + Org StackSet + hub-role templates
├── tests/
│ ├── test_live_scanner.py # 69 unit tests (mock boto3, no credentials needed)
│ ├── test_cnapp_phase1.py # 32 unit tests (graph, chains, trust, org fan-out, compliance)
│ ├── test_exposure.py # 35 unit tests (internet-exposure FP/FN catalog + attack path)
│ ├── test_deepplane.py # 44 unit tests (deep-plane FP/FN catalog + collectors + flagship)
│ ├── test_correlate.py # 22 unit tests (path enumeration + scoring + choke points)
│ ├── test_effperm.py # 32 unit tests (eval-order truth table, SCP/boundary scenarios)
│ ├── test_state.py # 22 unit tests (lifecycle, coverage-gated resolve, waivers, MTTR)
│ ├── test_unused.py # 21 unit tests (dormancy, right-sizing, down-rank, collection)
│ ├── test_phase5_integration.py # 17 tests (ceiling edge-pruning + defect regressions)
│ ├── test_sidescan.py # 63 unit tests (dpkg/rpm/apk vercmp, parsers, OSV match, secrets, edges)
│ ├── test_sidescan_ebs.py # 21 unit tests (plan/delta-zeroing/checksum/SparseImage/cleanup)
│ ├── test_graph_neptune.py # 14 unit tests (Gremlin CSV, openCypher, round-trip)
│ ├── test_state_dialect.py # 22 unit tests (URL parse, qmark→pyformat, upsert, DDL parity)
│ ├── test_phase6_integration.py # 15 tests (ATTACK-02-from-agentless pillar + defect regressions)
│ └── samples/ # Sample IaC files and reports
├── scripts/
│ └── validate_live.py # Read-only live-account validation harness
├── docs/
│ ├── overwatch-logo.png # brand logo (README banner) + overwatch-mark* (console + report)
│ └── banner.svg
├── CLAUDE.md # Developer documentation
├── CHANGELOG.md # Release notes
├── SECURITY.md # Security policy / responsible disclosure
├── .gitignore
├── LICENSE # GPL-3.0
└── README.md
| Scanner | Requirements |
|---|---|
IaC Scanner (aws_offline_scanner.py) |
Python 3.10+, optional pyyaml for CF YAML templates |
Live Scanner (aws_live_scanner.py) |
Python 3.10+, boto3, AWS credentials with SecurityAudit IAM policy |
These tools are for authorised security assessments only. The live scanner performs read-only API calls and never modifies AWS resources. The IaC scanner performs pure static analysis with no AWS connectivity. Always ensure you have explicit authorisation before scanning.
GPL-3.0 License -- see LICENSE.





