Commit 57eb721
authored
feat(sca): runtime SCA reachability (#17156)
## Description
Implements **Runtime SCA Reachability** — the tracer reports which vulnerable symbols (functions/methods in third-party libraries with known CVEs) have actually been invoked at runtime, reducing false positives from static SCA analysis.
**RFC**: https://docs.google.com/document/d/1xDw9iG6h41VCEgJGTqoJdruRaNS4pYgNifO6nhiizWA/edit?tab=t.a9gurws0d8ua
### How it works
When `DD_APPSEC_SCA_ENABLED=true`:
1. **CVE data loading** — At tracer startup, reads `_cve_data.json` containing vulnerability targets (symbol + CVE + version constraint). Filters against installed packages.
2. **Runtime instrumentation** — For each applicable CVE target, applies bytecode injection (`inject_hook`) to the vulnerable function. Supports both eager (already-imported modules) and lazy (via `ModuleWatchdog` for deferred imports).
3. **CVE registration** — Immediately registers all applicable CVEs on their dependencies with `reached: []` in the telemetry `app-dependencies-loaded` payload. The backend knows which CVEs apply before any symbol is hit.
4. **Reachability detection** — When an instrumented function executes, the hook captures the caller's file path, method name, and line number, then attaches it to the CVE's `reached` array. Only the first occurrence is reported per CVE (RFC: "reporting a single occurrence is sufficient").
5. **Telemetry reporting** — On each heartbeat (default 60s), dependencies with new reachability data are re-reported with all their metadata via `app-dependencies-loaded`.
### Telemetry payload (RFC v3)
```json
{
"request_type": "app-dependencies-loaded",
"payload": {
"dependencies": [
{
"name": "requests",
"version": "2.31.0",
"metadata": [
{
"type": "reachability",
"value": "{\"id\":\"CVE-2024-35195\",\"reached\":[{\"path\":\"myapp/views.py\",\"method\":\"handle_request\",\"line\":42}]}"
}
]
}
]
}
}
```
Before any symbol is hit, CVEs are reported with `"reached":[]`. When a symbol executes, the first caller info is added to the `reached` array.
## Performance
Benchmark: `scripts/perf_bench_heartbeat_cycles.py`
Measures `collect_report()` execution time per heartbeat cycle under different scenarios. The overhead is paid only in the background telemetry thread (every 60s), never in user request paths.
- **SCA OFF (main)**: baseline on `main` branch — old `update_imported_dependencies()` path, no `DependencyTracker`, no re-report scan
- **SCA OFF**: this branch with `DD_APPSEC_SCA_ENABLED=false` — new `DependencyTracker` with `metadata=None`, re-report scan skipped
- **SCA ON**: this branch with `DD_APPSEC_SCA_ENABLED=true` — new `DependencyTracker` with `metadata=[]`
- **Overhead**: `(SCA ON − SCA OFF) / SCA OFF`
### 1,000 dependencies (typical large application)
| Heartbeat Cycle | SCA OFF (main) | SCA OFF | SCA ON | Overhead |
|---|---|---|---|---|
| First heartbeat (all new) | 4.56ms | 4.63ms | 5.10ms | **+10.1%** |
| Idle (nothing to report) | 0.2us | 72.2us | 239.6us | **+231.8%** |
| CVE registration (100 CVEs, reached=[]) | 0.3us | 73.5us | 1.20ms | **+1527.5%** |
| SCA hits (100 hits on 50 deps) | 0.3us | 73.7us | 1.44ms | **+1857.1%** |
### 10,000 dependencies (extreme scale)
| Heartbeat Cycle | SCA OFF (main) | SCA OFF | SCA ON | Overhead |
|---|---|---|---|---|
| First heartbeat (all new) | 53.15ms | 57.18ms | 62.12ms | **+8.6%** |
| Idle (nothing to report) | 0.3us | 742.6us | 2.20ms | **+195.7%** |
| CVE registration (1,000 CVEs, reached=[]) | 0.3us | 720.7us | 13.51ms | **+1774.4%** |
| SCA hits (1,000 hits on 500 deps) | 0.3us | 718.9us | 15.56ms | **+2064.2%** |
### Payload size
| Scenario (1,000 deps) | Entries | Size |
|---|---|---|
| First heartbeat SCA OFF | 1,000 | 62 KB |
| First heartbeat SCA ON | 1,000 | 62 KB |
| CVE registration (100 CVEs) | 50 | 10 KB |
| SCA hits (100 hits) | 50 | 16 KB |
### Memory overhead
| Scenario (1,000 deps) | SCA OFF | SCA ON | Delta |
|---|---|---|---|
| Idle heartbeat | 155.3 KB | 192.5 KB | +37.2 KB |
| CVE registration (100 CVEs) | 136.4 KB | 205.8 KB | +69.4 KB |
| SCA hits (100 hits) | 136.2 KB | 208.5 KB | +72.3 KB |
> **Note on overhead**: The percentage overhead for idle, CVE registration, and SCA hits appears very high because the SCA OFF baseline includes mock/benchmark harness overhead (~72us at 1K deps) that dominates the measurement. In absolute terms:
>
> - **First heartbeat**: nearly identical across all three columns (4.56ms → 4.63ms → 5.10ms at 1K deps), confirming the `DependencyTracker` refactor adds **minimal overhead** to initial dependency discovery.
> - **Idle heartbeat with SCA OFF**: ~72us includes benchmark mock overhead; measured independently at ~8us (lock acquisition + `get_newly_imported_modules()` + lazy config check). The re-report scan is **completely skipped** when SCA is disabled.
> - **SCA ON worst case** at 1,000 dependencies: **1.44ms per 60s heartbeat** — 0.002% of the cycle.
> - **SCA ON worst case** at 10,000 dependencies with 1,000 CVEs: **16ms** — 0.027% of the heartbeat interval.
>
> All overhead runs entirely in the background telemetry thread and does not affect user request latency.
### SLO benchmark
A CI-integrated benchmark suite is available at `benchmarks/telemetry_dependencies/` with 8 scenarios covering first/idle/cve/hits phases at 100 and 1,000 dependencies. It is triggered automatically when `ddtrace/internal/telemetry/dependency*.py` or `ddtrace/appsec/sca/*` files change and compares performance between the base branch and this PR.
## Risks
- **Bytecode injection**: Uses the existing `inject_hook` infrastructure from dd-trace-py. The hook is exception-safe (wrapped in try/except) and never raises in user code.
- **Memory**: `DependencyEntry` objects add ~150 bytes per dependency vs plain strings. At 1,000 deps this is ~150KB total — negligible.
- **Lock contention**: The `DependencyTracker._lock` is held briefly during `attach_metadata` calls from the SCA hook. After the first hit per CVE (max reached=1), subsequent hook invocations short-circuit before any lock acquisition.
## Additional Notes
- Merge this PR first: DataDog/datadog-lambda-python#761
- Previous model PR benchmarks: #17092
- The static CVE JSON (`_cve_data.json`) will be replaced by Remote Config in the long-term solution
Co-authored-by: alberto.vara <alberto.vara@datadoghq.com>1 parent 306a0a0 commit 57eb721
46 files changed
Lines changed: 3688 additions & 86 deletions
File tree
- .gitlab/benchmarks
- .riot/requirements
- benchmarks
- telemetry_dependencies
- ddtrace
- appsec
- sca
- internal/sca
- scripts
- tests/appsec
- architectures
- integrations/flask_tests
- sca
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 2 additions & 2 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
791 | 791 | | |
792 | 792 | | |
793 | 793 | | |
794 | | - | |
| 794 | + | |
795 | 795 | | |
796 | 796 | | |
797 | 797 | | |
| |||
1076 | 1076 | | |
1077 | 1077 | | |
1078 | 1078 | | |
1079 | | - | |
| 1079 | + | |
1080 | 1080 | | |
1081 | 1081 | | |
1082 | 1082 | | |
| |||
Lines changed: 12 additions & 6 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2 | 2 | | |
3 | 3 | | |
4 | 4 | | |
5 | | - | |
| 5 | + | |
6 | 6 | | |
7 | 7 | | |
8 | 8 | | |
9 | 9 | | |
10 | | - | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
11 | 13 | | |
12 | 14 | | |
13 | 15 | | |
| 16 | + | |
14 | 17 | | |
| 18 | + | |
15 | 19 | | |
16 | 20 | | |
17 | 21 | | |
| |||
21 | 25 | | |
22 | 26 | | |
23 | 27 | | |
24 | | - | |
25 | | - | |
| 28 | + | |
| 29 | + | |
26 | 30 | | |
27 | 31 | | |
28 | 32 | | |
29 | 33 | | |
| 34 | + | |
30 | 35 | | |
31 | | - | |
| 36 | + | |
32 | 37 | | |
33 | | - | |
| 38 | + | |
| 39 | + | |
Lines changed: 12 additions & 6 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2 | 2 | | |
3 | 3 | | |
4 | 4 | | |
5 | | - | |
| 5 | + | |
6 | 6 | | |
7 | 7 | | |
8 | 8 | | |
9 | 9 | | |
10 | | - | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
11 | 13 | | |
12 | 14 | | |
13 | 15 | | |
| 16 | + | |
14 | 17 | | |
| 18 | + | |
15 | 19 | | |
16 | 20 | | |
17 | 21 | | |
| |||
21 | 25 | | |
22 | 26 | | |
23 | 27 | | |
24 | | - | |
25 | | - | |
| 28 | + | |
| 29 | + | |
26 | 30 | | |
27 | 31 | | |
28 | 32 | | |
29 | 33 | | |
| 34 | + | |
30 | 35 | | |
31 | | - | |
| 36 | + | |
32 | 37 | | |
33 | | - | |
| 38 | + | |
| 39 | + | |
Lines changed: 10 additions & 4 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2 | 2 | | |
3 | 3 | | |
4 | 4 | | |
5 | | - | |
| 5 | + | |
6 | 6 | | |
7 | 7 | | |
8 | 8 | | |
9 | 9 | | |
| 10 | + | |
| 11 | + | |
10 | 12 | | |
11 | 13 | | |
12 | 14 | | |
13 | 15 | | |
14 | 16 | | |
| 17 | + | |
15 | 18 | | |
| 19 | + | |
16 | 20 | | |
17 | 21 | | |
18 | 22 | | |
| |||
23 | 27 | | |
24 | 28 | | |
25 | 29 | | |
26 | | - | |
| 30 | + | |
27 | 31 | | |
28 | 32 | | |
29 | 33 | | |
30 | 34 | | |
31 | 35 | | |
| 36 | + | |
32 | 37 | | |
33 | | - | |
| 38 | + | |
34 | 39 | | |
35 | 40 | | |
36 | | - | |
| 41 | + | |
| 42 | + | |
37 | 43 | | |
Lines changed: 12 additions & 6 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2 | 2 | | |
3 | 3 | | |
4 | 4 | | |
5 | | - | |
| 5 | + | |
6 | 6 | | |
7 | 7 | | |
8 | 8 | | |
9 | 9 | | |
10 | | - | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
11 | 13 | | |
12 | 14 | | |
13 | 15 | | |
| 16 | + | |
14 | 17 | | |
| 18 | + | |
15 | 19 | | |
16 | 20 | | |
17 | 21 | | |
| |||
21 | 25 | | |
22 | 26 | | |
23 | 27 | | |
24 | | - | |
25 | | - | |
| 28 | + | |
| 29 | + | |
26 | 30 | | |
27 | 31 | | |
28 | 32 | | |
29 | 33 | | |
| 34 | + | |
30 | 35 | | |
31 | | - | |
| 36 | + | |
32 | 37 | | |
33 | | - | |
| 38 | + | |
| 39 | + | |
Lines changed: 12 additions & 6 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2 | 2 | | |
3 | 3 | | |
4 | 4 | | |
5 | | - | |
| 5 | + | |
6 | 6 | | |
7 | 7 | | |
8 | 8 | | |
9 | 9 | | |
10 | | - | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
11 | 13 | | |
12 | 14 | | |
13 | 15 | | |
14 | 16 | | |
| 17 | + | |
15 | 18 | | |
| 19 | + | |
16 | 20 | | |
17 | 21 | | |
18 | 22 | | |
| |||
22 | 26 | | |
23 | 27 | | |
24 | 28 | | |
25 | | - | |
26 | | - | |
| 29 | + | |
| 30 | + | |
27 | 31 | | |
28 | 32 | | |
29 | 33 | | |
30 | 34 | | |
| 35 | + | |
31 | 36 | | |
32 | | - | |
| 37 | + | |
33 | 38 | | |
34 | 39 | | |
35 | | - | |
| 40 | + | |
| 41 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
Lines changed: 12 additions & 6 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2 | 2 | | |
3 | 3 | | |
4 | 4 | | |
5 | | - | |
| 5 | + | |
6 | 6 | | |
7 | 7 | | |
8 | 8 | | |
9 | 9 | | |
10 | | - | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
11 | 13 | | |
12 | 14 | | |
13 | 15 | | |
| 16 | + | |
14 | 17 | | |
| 18 | + | |
15 | 19 | | |
16 | 20 | | |
17 | 21 | | |
| |||
21 | 25 | | |
22 | 26 | | |
23 | 27 | | |
24 | | - | |
25 | | - | |
| 28 | + | |
| 29 | + | |
26 | 30 | | |
27 | 31 | | |
28 | 32 | | |
29 | 33 | | |
| 34 | + | |
30 | 35 | | |
31 | | - | |
| 36 | + | |
32 | 37 | | |
33 | | - | |
| 38 | + | |
| 39 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
0 commit comments