Attack Surface Mapping Techniques
Uncharted entry points are the precondition for every successful breach: attackers enumerate what defenders haven’t. Attack surface mapping is the systematic identification and cataloging of all potential entry points, data flows, and external dependencies within a web application — converting invisible exposure into a versioned, auditable inventory. Without it, threat modeling operates on assumptions rather than facts. This guide is part of Threat Modeling Fundamentals & Methodology and covers how to combine manual architectural analysis with automated discovery pipelines to keep pace with ephemeral cloud environments. The techniques here feed directly into defining trust boundaries across service edges and inform STRIDE threat identification at each architectural layer.
Threat Anatomy
An attacker’s enumeration phase mirrors the defender’s mapping process — the difference is timing. Before exploitation, adversaries probe DNS records, crawl API schemas, scan cloud-provider IP ranges, and fingerprint third-party integrations. Each undiscovered asset is a potential pivot point that bypasses perimeter controls entirely.
The MITRE ATT&CK framework categorises this reconnaissance under T1595 (Active Scanning) and T1590 (Gather Victim Network Information). Specific sub-techniques — T1595.001 (Scanning IP Blocks), T1595.002 (Vulnerability Scanning), and T1590.004 (Network Topology) — map directly to the gaps that incomplete attack surface inventories leave open.
The attack sequence in a poorly mapped environment follows a predictable pattern:
The three highest-risk discovery gaps that enable this sequence are:
| Discovery Gap | MITRE Technique | Attacker Capability Gained |
|---|---|---|
| Unregistered cloud instances (shadow IT) | T1583.006 (Web Services) | Bypasses WAF and perimeter controls entirely |
| Undocumented internal API routes | T1590.002 (DNS / IP) | Horizontal movement to privileged data |
| Stale third-party webhook receivers | T1190 (Exploit Public App) | Supply-chain injection into trusted pipelines |
| Unmapped inter-service channels | T1021 (Remote Services) | Lateral movement within service mesh |
For the automated attack surface discovery with OWASP ZAP workflow that catches runtime gaps specifically, see the dedicated in-depth guide.
Prerequisites & Scope
Before beginning a mapping exercise, the following must be in place:
- Infrastructure-as-Code source of truth. Terraform state files, CloudFormation templates, or Pulumi stacks must be accessible and reflect current deployed state. Mappings derived from stale IaC are unreliable.
- API specification access. OpenAPI 3.x or Swagger 2.x specs for all owned APIs — including internal service-to-service interfaces, not just public-facing ones.
- Dependency manifests.
package.json,pom.xml,go.mod, orrequirements.txtfor every service in scope. - Network topology documentation. VPC peering diagrams, load balancer configs, DNS zone files, and CDN routing rules.
- Defined scope boundary. Agreement on what is in-scope: first-party services only, or inclusive of third-party SaaS integrations and outbound data pipelines.
- Staging environment access. Read-only access to a production-equivalent staging environment for dynamic scanning.
Mitigation Architecture
The secure mapping posture treats discovery as a continuous, automated pipeline rather than a periodic audit. The contrast between a periodic-audit approach and a continuous-discovery approach determines whether the inventory reflects current reality.
| Aspect | Periodic Audit (Vulnerable Pattern) | Continuous Discovery (Hardened Pattern) |
|---|---|---|
| Trigger | Quarterly manual review | Every PR merge + nightly scheduled scan |
| Scope | Known assets only | Known + dynamic enumeration of shadow assets |
| IaC drift detection | None | Schema diff gates that fail builds on unauthorized new resources |
| Third-party integrations | Manually cataloged at onboarding | Webhook registry + outbound traffic analysis in staging |
| Audit trail | Point-in-time snapshot | Append-only versioned inventory with cryptographic signing |
| Compliance evidence | Generated on demand | Continuous, query-able at any time |
The hardened pattern embeds discovery at three integration points: the IaC provisioning gate (pre-deploy), the CI/CD pipeline (post-merge), and a nightly scheduled scan (drift detection between deployments).
Step-by-Step Implementation
Step 1: Define Asset Classification Schema
Establish exposure tiers before any discovery runs. Every asset discovered must immediately receive a classification — without this, discovery outputs become unactionable lists.
OWASP ASVS V1.1.1 / NIST SP 800-53 CM-8
from enum import Enum
from dataclasses import dataclass, field
from typing import Optional
class ExposureLevel(str, Enum):
PUBLIC = "Public" # Reachable from the open internet
PARTNER = "Partner" # Restricted to allowlisted external IPs/tokens
INTERNAL = "Internal" # Reachable only within VPC/service mesh
CONFIDENTIAL = "Confidential" # Restricted data tier — explicit allow-list only
@dataclass
class AssetRecord:
resource_type: str
identifier: str
exposure_level: ExposureLevel
network_cidr: list[str] = field(default_factory=list)
tags: dict[str, str] = field(default_factory=dict)
auth_required: bool = True
owner_team: Optional[str] = None
discovery_source: str = "iac_parse" # iac_parse | openapi | dynamic | manual
def is_high_risk(self) -> bool:
"""Flag assets needing immediate threat model coverage."""
return (
self.exposure_level == ExposureLevel.PUBLIC
and not self.auth_required
)
Step 2: Parse IaC State for Infrastructure Assets
Extract resource identifiers, CIDR blocks, and IAM roles from Terraform state. This gives a deterministic view of provisioned infrastructure before any runtime analysis.
NIST SP 800-53 CA-7 (Continuous Monitoring)
import json
import sys
from pathlib import Path
def extract_public_assets(tfstate_path: str) -> list[dict]:
"""Parse Terraform state to identify public-facing infrastructure."""
with open(tfstate_path, "r") as f:
state = json.load(f)
assets: list[dict] = []
target_types = {
"aws_security_group",
"aws_lb",
"aws_api_gateway_stage",
"aws_lambda_function_url",
"aws_cloudfront_distribution",
}
for resource in state.get("resources", []):
r_type = resource.get("type", "")
if r_type not in target_types:
continue
attrs = resource.get("instances", [{}])[0].get("attributes", {})
is_public = bool(
attrs.get("publicly_accessible")
or attrs.get("invoke_mode") == "BUFFERED"
or attrs.get("ingress")
)
assets.append({
"resource_type": r_type,
"identifier": attrs.get("id", "unknown"),
"exposure_level": "Public" if is_public else "Internal",
"network_cidr": attrs.get("cidr_blocks", []),
"tags": attrs.get("tags", {}),
"auth_required": not attrs.get("authorization_type") == "NONE",
})
return assets
if __name__ == "__main__":
if len(sys.argv) != 2:
print("Usage: python iac_extractor.py <tfstate.json>", file=sys.stderr)
sys.exit(1)
try:
inventory = extract_public_assets(sys.argv[1])
print(json.dumps(inventory, indent=2))
except (json.JSONDecodeError, KeyError) as e:
print(f"Extraction failed: {e}", file=sys.stderr)
sys.exit(1)
Step 3: Enumerate API Routes from OpenAPI Specs
Parse OpenAPI definitions to catalog every route, HTTP method, and authentication requirement. Flag routes present in the deployed service but absent from the spec — these undocumented endpoints are high-priority mapping targets.
OWASP ASVS V13.1.1 (API Surface Security)
import * as fs from "fs";
import * as yaml from "js-yaml";
import * as path from "path";
interface EndpointRecord {
route: string;
method: string;
auth_required: boolean;
auth_schemes: string[];
parameters: string[];
stride_category: string;
}
function strideCategory(method: string, auth_required: boolean): string {
if (!auth_required && ["POST", "PUT", "DELETE", "PATCH"].includes(method)) {
return "Tampering";
}
if (!auth_required && method === "GET") {
return "Information Disclosure";
}
return "Review Required";
}
function enumerateEndpoints(specPath: string): EndpointRecord[] {
const raw = fs.readFileSync(path.resolve(specPath), "utf8");
const spec = yaml.load(raw) as Record<string, unknown>;
const paths = (spec.paths ?? {}) as Record<string, Record<string, unknown>>;
const results: EndpointRecord[] = [];
const httpMethods = new Set(["get","post","put","delete","patch","head","options"]);
for (const [route, methods] of Object.entries(paths)) {
for (const [method, details] of Object.entries(methods)) {
if (!httpMethods.has(method.toLowerCase())) continue;
const op = details as Record<string, unknown>;
const securityEntries = (op.security ?? []) as Array<Record<string, unknown>>;
const authSchemes = securityEntries.flatMap((s) => Object.keys(s));
const auth_required = authSchemes.length > 0;
const params = ((op.parameters ?? []) as Array<{name: string}>).map((p) => p.name);
results.push({
route,
method: method.toUpperCase(),
auth_required,
auth_schemes: auth_required ? authSchemes : ["None"],
parameters: params,
stride_category: strideCategory(method.toUpperCase(), auth_required),
});
}
}
return results;
}
const specFile = process.argv[2] ?? "openapi.yaml";
try {
const results = enumerateEndpoints(specFile);
console.log(JSON.stringify(results, null, 2));
} catch (err) {
console.error(`Failed to parse spec: ${(err as Error).message}`);
process.exit(1);
}
Step 4: Integrate Discovery into CI/CD
Gate deployments on inventory diff validation. The pipeline runs discovery on every merge to main and compares output against the baseline — any unauthorized asset creation fails the build.
SOC 2 CC6.1 / NIST SP 800-53 SI-7
name: Attack Surface Discovery Pipeline
on:
push:
branches: [main]
schedule:
- cron: "0 2 * * *" # Nightly drift detection
permissions:
contents: read
id-token: write # OIDC for AWS credential federation
jobs:
surface-mapping:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Extract IaC Inventory
run: |
python scripts/iac_extractor.py infra/terraform.tfstate \
> artifacts/iac-inventory.json
- name: Enumerate API Routes
run: |
npx ts-node scripts/enumerate-endpoints.ts openapi.yaml \
> artifacts/api-inventory.json
- name: Diff Against Baseline
run: |
python scripts/inventory_diff.py \
--baseline inventories/baseline.json \
--current artifacts/iac-inventory.json \
--fail-on-new-public # Fail build if new public assets appear
--fail-on-auth-removed # Fail build if auth is removed from existing route
- name: Schema Validation
run: |
npx ajv-cli validate \
-s schemas/asset-inventory.schema.json \
-d artifacts/iac-inventory.json
- name: Upload Signed Inventory
env:
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
run: |
# Sign with cosign for tamper-evident audit trail
cosign sign-blob --key env://COSIGN_KEY artifacts/iac-inventory.json \
> artifacts/iac-inventory.json.sig
curl -sX POST https://threat-registry.internal/api/v1/assets \
-H "Authorization: Bearer $REGISTRY_TOKEN" \
-H "Content-Type: application/json" \
-d @artifacts/iac-inventory.json
Edge Cases & Bypass Patterns
Incomplete mapping consistently leaves the same categories of assets undiscovered. Each represents a class of bypass that renders perimeter controls ineffective.
1. Ephemeral compute — serverless and container-on-demand
Lambda function URLs, Cloud Run services, and Azure Container Apps can be provisioned with public HTTP endpoints in seconds. Unless your IaC scanner parses aws_lambda_function_url and google_cloud_run_service resource types explicitly, these appear as gaps. Enforce IaC-only provisioning via SCPs (Service Control Policies) that deny lambda:CreateFunctionUrlConfig on resources without the iac-managed=true tag.
2. Developer preview and staging environments promoted to production traffic
Staging endpoints created for feature branches often receive production DNS entries and TLS certificates but lack WAF coverage or authentication enforcement. Map all certificate SANs (Subject Alternative Names) via Certificate Transparency logs — any SAN not in the official asset register is an undiscovered entry point.
3. Outbound webhooks as inbound attack vectors
Third-party SaaS platforms that push events to your application via webhook are inbound HTTP endpoints from your perimeter’s perspective. They are rarely documented in OpenAPI specs and frequently omit signature verification. Catalog all webhook receiver routes by grepping for framework-specific patterns (@app.route, router.post, app.post) and cross-referencing against the OpenAPI spec.
4. Internal microservice routes exposed via misconfigured service mesh
When trust boundaries between services rely solely on namespace isolation, a misconfigured Istio VirtualService or Kubernetes NetworkPolicy gap can expose internal-only gRPC and REST routes externally. Map all VirtualService and ServiceEntry resources as part of the surface inventory — not just Ingress and Service objects of type LoadBalancer.
5. SDK and dependency-introduced endpoints
Monitoring SDKs, feature flag clients, and APM agents frequently open listening ports or register HTTP routes for health checks, debug endpoints, or configuration polling. Dependencies like Spring Boot Actuator, Django Debug Toolbar, and Prometheus client libraries expose /actuator, /debug, and /metrics routes that must be enumerated in the asset inventory and explicitly disabled or access-restricted in production.
Automated Testing & CI Validation
The following test verifies that the inventory diff gate correctly detects new public assets and raises an error rather than silently passing.
import json
import pytest
from scripts.inventory_diff import compute_diff, DiffResult
BASELINE = [
{"identifier": "lb-abc123", "exposure_level": "Public", "auth_required": True},
{"identifier": "sg-def456", "exposure_level": "Internal", "auth_required": True},
]
def test_detects_new_public_asset():
current = BASELINE + [
{"identifier": "lambda-url-xyz", "exposure_level": "Public", "auth_required": False}
]
result: DiffResult = compute_diff(baseline=BASELINE, current=current)
assert len(result.new_public_assets) == 1
assert result.new_public_assets[0]["identifier"] == "lambda-url-xyz"
assert result.should_fail_build is True
def test_detects_auth_removal():
current = [
{"identifier": "lb-abc123", "exposure_level": "Public", "auth_required": False},
{"identifier": "sg-def456", "exposure_level": "Internal", "auth_required": True},
]
result: DiffResult = compute_diff(baseline=BASELINE, current=current)
assert len(result.auth_downgraded) == 1
assert result.should_fail_build is True
def test_passes_on_identical_inventory():
result: DiffResult = compute_diff(baseline=BASELINE, current=BASELINE)
assert result.should_fail_build is False
assert result.new_public_assets == []
assert result.auth_downgraded == []
Compliance Mapping
| Framework | Control | Requirement | Satisfied By |
|---|---|---|---|
| SOC 2 | CC6.1 | Logical and physical access controls with documented network boundaries | Versioned asset register with exposure levels and auth requirements per resource |
| SOC 2 | CC7.1 | Monitoring of system components | CI/CD diff gate + nightly scan producing audit-trail entries |
| OWASP ASVS | V1.1.1 | Verify high-level architecture documentation and security analysis is updated | IaC-derived inventory committed to version control on every deploy |
| OWASP ASVS | V13.1.1 | Verify all application components are identified and secured | OpenAPI enumeration script producing route-level auth coverage map |
| NIST SP 800-53 | CM-8 | Information System Component Inventory | Append-only signed inventory with resource type, exposure, and owner |
| NIST SP 800-53 | CA-7 | Continuous Monitoring | Scheduled nightly scan + build gate on every push to main |
| ISO 27001 | A.8.1.2 | Inventory of Assets | Version-controlled asset register with classification, ownership, and exposure tags |
| PCI-DSS | Req 1.2 | Network boundary controls | Validated CIDR documentation, segmentation diagrams, and third-party dependency log |
Common Pitfalls Checklist
Frequently Asked Questions
How frequently should attack surface mapping run in agile environments?
Continuous mapping via CI/CD hooks is mandatory — every merge to main must trigger an inventory diff. Full architectural reviews should occur quarterly or after major infrastructure changes. The nightly scheduled scan catches drift that occurs outside the deployment pipeline: manual console actions, external SaaS configuration changes, and DNS record updates made outside IaC.
What is the difference between attack surface mapping and vulnerability scanning?
Mapping identifies and catalogs all potential entry points, assets, and data flows to establish scope. Vulnerability scanning tests those identified points for known weaknesses or misconfigurations. Mapping must precede scanning: running a scanner against an incomplete asset inventory produces false confidence because the scanner can only test what it is pointed at. Undiscovered assets are unreachable by design.
Can automated discovery tools fully replace manual architectural review?
No. Automated tools excel at runtime enumeration, dependency tracking, and configuration drift detection. Manual review remains essential for understanding business logic flows, implicit trust relationships between services, undocumented legacy integrations, and threat prioritization decisions that require organizational context. The correct model is automated discovery as the baseline, with human review focused on the diff and the highest-risk assets.
How does attack surface mapping satisfy SOC 2 and OWASP ASVS requirements?
SOC 2 CC6.1 requires documented network boundaries and endpoint authentication matrices — the asset register produced by IaC parsing and OpenAPI enumeration satisfies this directly. OWASP ASVS V1.1.1 requires verified architecture documentation updated on changes — the CI/CD gate that commits a new inventory artifact on every deploy provides the versioned evidence. NIST SP 800-53 CM-8 is satisfied by the append-only signed inventory with resource type, exposure, and owner fields.
Related
- Automated Attack Surface Discovery with OWASP ZAP — runtime enumeration and spider-based discovery workflow
- Defining Trust Boundaries — mapping trust tiers to the assets this page discovers
- Mapping Trust Boundaries in Cloud-Native Apps — applying boundary definitions to Kubernetes and service mesh environments
- STRIDE Framework Implementation — using the asset inventory as input to STRIDE threat identification
- Threat Prioritization & Risk Scoring — scoring discovered assets by exposure and EPSS/DREAD risk