AWS Inspector v2 Security Testing: Vulnerability Assessment Gaps, ECR/Lambda/EC2 Coverage, and Finding Validation

AWS Inspector v2 provides automated vulnerability scanning for EC2, ECR container images, and Lambda functions. Organizations enable it, see the dashboard turn green, and assume they're covered. But Inspector v2 has well-defined coverage gaps: it doesn't scan running containers (only images at push time), Lambda layers have partial coverage, suppression rules can silence critical findings indefinitely, and the EPSS/CVSS scoring doesn't always reflect actual exploitability in your specific configuration. This guide covers testing Inspector v2's completeness rather than trusting it.

Table of Contents

  1. Coverage Audit: What Inspector Scans vs Doesn't
  2. IAM Privilege Escalation to Inspector Findings
  3. Suppression Rule Security Audit
  4. ECR Container Image Scanning Gaps
  5. Lambda Vulnerability Assessment Gaps
  6. EC2 Coverage and SSM Dependency
  7. SBOM Extraction and Third-Party Validation
  8. Inspector v2 Hardening

Coverage Audit: What Inspector Scans vs Doesn't

Resource TypeInspector v2 CoverageWhat It Misses
EC2 instancesOS packages, network reachabilityRunning containers on EC2, custom-compiled binaries, non-SSM instances
ECR imagesOS packages, language dependencies at push timeVulns introduced during runtime, running containers with drift, non-ECR registries
Lambda functionsLambda layer packages, function code dependenciesContainer-based Lambda images (partial), custom runtimes, layer versions not attached
ECS/EKS workloadsVia ECR image scansRunning pod drift from image, init containers, sidecars from non-ECR sources
RDS / databasesNot coveredDatabase engine CVEs entirely missing
Code vulnerabilitiesBasic code scanning (limited languages)Business logic, injection, OWASP Top 10 application flaws
# Check Inspector v2 enabled regions and resource types
aws inspector2 get-configuration --region us-east-1 | \
  python3 -c "
import json,sys
config = json.load(sys.stdin)
print('EC2 scanning:', config.get('ec2Configuration',{}).get('scanMode','NOT_ENABLED'))
print('ECR scanning:', config.get('ecrConfiguration',{}).get('rescanDuration','NOT_ENABLED'))
print('Lambda scanning:', config.get('lambdaConfiguration',{}).get('scanMode','NOT_ENABLED'))
"

# Check accounts enrolled in delegated admin (multi-account)
aws inspector2 list-delegated-admin-accounts

# Find EC2 instances NOT covered by Inspector (no SSM agent or not in enabled account)
aws inspector2 list-coverage \
  --filter-criteria '{"ec2InstanceTags":[{"comparison":"NOT_EQUALS","key":"inspector-enabled","value":"true"}]}' \
  --query 'coveredResources[*].resourceId'

# More reliable: find EC2 instances running without SSM agent
aws ec2 describe-instances \
  --query 'Reservations[*].Instances[*].[InstanceId,State.Name]' \
  --output text | while read id state; do
    ssm_status=$(aws ssm describe-instance-information \
      --filters "Key=InstanceIds,Values=$id" \
      --query 'InstanceInformationList[0].PingStatus' --output text 2>/dev/null)
    [ "$ssm_status" = "None" ] && echo "NO SSM: $id"
done

IAM Privilege Escalation to Inspector Findings

Inspector findings contain CVE details, affected packages, and remediation recommendations. A principal with inspector2:ListFindings can enumerate all vulnerabilities across an account — high-value reconnaissance for an attacker with partial AWS access.

# Check who can access Inspector findings
aws iam get-account-authorization-details | \
  python3 -c "
import json,sys
data = json.load(sys.stdin)
dangerous_actions = ['inspector2:ListFindings','inspector2:GetFindings','inspector2:BatchGetFindingDetails']
for policy in data.get('Policies', []):
    for pv in policy.get('PolicyVersionList', []):
        if not pv.get('IsDefaultVersion'): continue
        doc = pv.get('Document', {})
        for stmt in doc.get('Statement', []):
            actions = stmt.get('Action', [])
            if isinstance(actions, str): actions = [actions]
            if any(a in actions or '*' in a for a in dangerous_actions):
                print(f\"Policy: {policy['PolicyName']} — grants inspector2 read\")
"

# Test: if you have inspector2:ListFindings and inspector2:BatchGetFindingDetails
# you can enumerate all CVEs in all scanned workloads:
aws inspector2 list-findings \
  --filter-criteria '{"findingStatus":[{"comparison":"EQUALS","value":"ACTIVE"}]}' \
  --query 'findings[*].{id:findingArn,cve:packageVulnerabilityDetails.vulnerabilityId,severity:severity.label,resource:resources[0].id}' \
  --output table | head -30

# Check if findings are accessible cross-account via delegated admin
aws inspector2 list-findings \
  --query 'findings[*].awsAccountId' --output text | sort | uniq -c

Suppression Rule Security Audit

# List all suppression rules — critical findings may be silenced indefinitely
aws inspector2 list-filters \
  --action SUPPRESS \
  --query 'filters[*].{name:name,reason:reason,criteria:filterCriteria}' | \
  python3 -c "
import json,sys
filters = json.load(sys.stdin)
for f in filters:
    print(f\"Suppression: {f['name']}\")
    print(f\"  Reason: {f.get('reason','NO REASON PROVIDED')}\")
    criteria = f.get('criteria', {})
    for key, val in criteria.items():
        print(f\"  Criteria: {key} = {val}\")
    print()
"

# Check for overly broad suppression rules:
# - Rules that suppress by severity (silencing all CRITICAL findings)
# - Rules with no expiration date
# - Rules with vague reasons like 'accepted risk' or 'false positive'

# Identify suppressed CRITICAL/HIGH findings that are actually exploitable
aws inspector2 list-findings \
  --filter-criteria '{
    "findingStatus":[{"comparison":"EQUALS","value":"SUPPRESSED"}],
    "severity":[{"comparison":"EQUALS","value":"CRITICAL"}]
  }' \
  --query 'findings[*].{cve:packageVulnerabilityDetails.vulnerabilityId,score:inspectorScore,resource:resources[0].id}' | \
  python3 -c "
import json,sys
findings = json.load(sys.stdin)
print(f'Suppressed CRITICAL findings: {len(findings)}')
for f in findings[:10]:
    print(f\"  CVE: {f.get('cve','?')} Score: {f.get('score','?')} Resource: {f.get('resource','?')}\")
"

ECR Container Image Scanning Gaps

# Inspector v2 scans ECR images at push time with optional continuous monitoring
# Key gap: an image scanned clean on Monday may have new CVEs disclosed on Tuesday
# Inspector's continuous rescan catches this IF the image is still referenced

# Check ECR rescan settings (should be CONTINUOUS not LIFETIME or DAYS_90)
aws inspector2 get-configuration | \
  python3 -c "
import json,sys
cfg = json.load(sys.stdin)
ecr = cfg.get('ecrConfiguration', {})
print('ECR rescan duration:', ecr.get('rescanDuration','NOT_CONFIGURED'))
# LIFETIME = rescan as long as image exists = BEST
# DAYS_180 or DAYS_90 = stops rescanning after N days
# Manual only = only scans at push = WORST
"

# Find ECR images with no recent scan results (potential blind spot)
aws ecr describe-repositories --query 'repositories[*].repositoryName' --output text | \
  tr '\t' '\n' | while read repo; do
    latest=$(aws ecr describe-images --repository-name "$repo" \
      --query 'sort_by(imageDetails, &imagePushedAt)[-1].[imageDigest,imagePushedAt]' \
      --output text 2>/dev/null)
    if [ -z "$latest" ]; then continue; fi
    digest=$(echo "$latest" | cut -f1)
    scan=$(aws ecr describe-image-scan-findings \
      --repository-name "$repo" \
      --image-id imageDigest=$digest \
      --query 'imageScanStatus.status' --output text 2>/dev/null)
    echo "$repo: latest image scan = $scan"
done

# Check for images in non-ECR registries used in EKS/ECS (Inspector won't scan these)
kubectl get pods --all-namespaces -o json | \
  python3 -c "
import json,sys
pods = json.load(sys.stdin)['items']
external = set()
for p in pods:
    for c in p['spec'].get('containers', []) + p['spec'].get('initContainers', []):
        img = c.get('image', '')
        if not any(x in img for x in ['.dkr.ecr.', 'amazonaws.com']):
            external.add(img.split(':')[0])
for img in sorted(external):
    print(f'Non-ECR image: {img}')
"

Lambda Vulnerability Assessment Gaps

# Inspector v2 scans Lambda function code and layers for known CVEs
# Gap: only scans functions in enabled regions, only active $LATEST or published versions

# Check Lambda scanning is enabled
aws inspector2 get-configuration | python3 -c "
import json,sys
cfg = json.load(sys.stdin)
lmb = cfg.get('lambdaConfiguration', {})
print('Lambda scan mode:', lmb.get('scanMode', 'NOT_CONFIGURED'))
# PACKAGE_ONLY = dependency scanning only (most common)
# CODE_AND_PACKAGE = includes basic code analysis
"

# Find Lambda functions with Inspector findings
aws inspector2 list-findings \
  --filter-criteria '{"resourceType":[{"comparison":"EQUALS","value":"AWS_LAMBDA_FUNCTION"}]}' \
  --query 'findings[*].{fn:resources[0].id,cve:packageVulnerabilityDetails.vulnerabilityId,severity:severity.label}' \
  --output table

# Check Lambda layers — Inspector scans layers attached to functions
# But layers NOT attached to any function are not scanned
aws lambda list-layers --query 'Layers[*].LayerName' --output text | \
  tr '\t' '\n' | while read layer; do
    # Check if any function uses this layer
    fns=$(aws lambda list-functions \
      --query "Functions[?contains(Layers[*].Arn, \`$layer\`)].FunctionName" \
      --output text 2>/dev/null)
    [ -z "$fns" ] && echo "Orphaned layer (not scanned): $layer"
done

# Test for runtime EOL — Inspector reports vulnerabilities but not EOL runtimes
# EOL runtimes may have unpatched CVEs that will never be fixed
aws lambda list-functions \
  --query 'Functions[*].[FunctionName,Runtime]' --output text | \
  grep -E "python2|nodejs12|nodejs14|ruby2.5|java8$|go1.x" | \
  while read fn rt; do echo "EOL runtime: $fn ($rt)"; done

EC2 Coverage and SSM Dependency

# Inspector v2 EC2 scanning requires SSM agent — instances without SSM are blind spots

# Check SSM agent status across all EC2 instances
aws ssm describe-instance-information \
  --query 'InstanceInformationList[*].[InstanceId,PingStatus,PlatformType]' \
  --output table

# Find EC2 instances where SSM is offline or never connected
aws ec2 describe-instances \
  --filters "Name=instance-state-name,Values=running" \
  --query 'Reservations[*].Instances[*].InstanceId' --output text | \
  tr '\t' '\n' | while read id; do
    status=$(aws ssm describe-instance-information \
      --filters "Key=InstanceIds,Values=$id" \
      --query 'InstanceInformationList[0].PingStatus' --output text 2>/dev/null)
    [ "$status" != "Online" ] && echo "Inspector blind spot: $id (SSM: $status)"
done

# Check Inspector EC2 network reachability scan
# Inspector checks if vulns are network-reachable (port open + CVE in exposed package)
# Test: does Inspector correctly identify network-exposed CVEs?
aws inspector2 list-findings \
  --filter-criteria '{
    "resourceType":[{"comparison":"EQUALS","value":"AWS_EC2_INSTANCE"}],
    "findingType":[{"comparison":"EQUALS","value":"NETWORK_REACHABILITY"}]
  }' \
  --query 'findings[*].{instance:resources[0].id,port:networkReachabilityDetails.openPortRange.begin,severity:severity.label}' \
  --output table | head -20

# Verify: for a finding claiming port 443 is reachable with CVE-XXXX,
# actually test whether the CVE is exploitable via that port
# Inspector's 'network reachable' is a best-effort assessment, not pentest validation

SBOM Extraction and Third-Party Validation

# Inspector v2 can export SBOM (Software Bill of Materials) per resource
# Cross-validate with external CVE databases to catch findings Inspector missed

# Export SBOM for a specific ECR image
aws inspector2 create-sbom-export \
  --report-format CYCLONEDX_1_4 \
  --s3-destination bucket="my-security-bucket",keyPrefix="sbom/",kmsKeyArn="arn:aws:kms:..." \
  --filter-criteria '{"resourceType":[{"comparison":"EQUALS","value":"AWS_ECR_CONTAINER_IMAGE"}]}'

# After export, download and analyze with grype or trivy for comparison
aws s3 cp s3://my-security-bucket/sbom/sbom.json /tmp/sbom.json
grype sbom:/tmp/sbom.json | grep -E "Critical|High" | head -20

# Compare grype findings vs Inspector findings for the same image
aws inspector2 list-findings \
  --filter-criteria '{"resourceId":[{"comparison":"EQUALS","value":"YOUR_IMAGE_URI"}]}' \
  --query 'findings[*].packageVulnerabilityDetails.vulnerabilityId' \
  --output text | tr '\t' '\n' | sort > /tmp/inspector-cves.txt

# Diff shows CVEs grype finds that Inspector missed (database lag, coverage gaps)
grype "IMAGE_URI" -q --only-fixed 2>/dev/null | awk '{print $5}' | sort > /tmp/grype-cves.txt
comm -23 /tmp/grype-cves.txt /tmp/inspector-cves.txt

Inspector v2 Hardening

Inspector v2 Security Hardening Checklist:
Security TestMethodRisk
Uninstructed accounts not enrolledinspector2:list-members — missing accountsCritical
Suppressed CRITICAL CVEs with no reasonlist-filters --action SUPPRESSCritical
EC2 with SSM offline = no scanningssm:describe-instance-informationHigh
inspector2:ListFindings too permissiveIAM authorization details reviewHigh
ECR rescan on push onlyget-configuration ecrConfiguration.rescanDurationHigh
Non-ECR images in EKS podskubectl pod image auditHigh
Lambda EOL runtimeslist-functions runtime filterMedium

Automate AWS Inspector v2 Coverage Validation

Ironimo tests whether your AWS Inspector deployment actually covers your attack surface — checking for SSM gaps, suppression rule misuse, non-ECR images, and cross-validating findings against independent vulnerability databases.

Start free scan