Skip to content

Commit 57eb721

Browse files
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

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitlab/benchmarks/bp-runner.microbenchmarks.fail-on-breach.template.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -791,7 +791,7 @@ experiments:
791791
- max_rss_usage < 46.00 MB
792792
- name: packagesupdateimporteddependencies-import_one_stdlib
793793
thresholds:
794-
- execution_time < 0.02 ms
794+
- execution_time < 0.03 ms
795795
- max_rss_usage < 46.00 MB
796796
- name: packagesupdateimporteddependencies-import_one_stdlib_cache
797797
thresholds:
@@ -1076,7 +1076,7 @@ experiments:
10761076
- max_rss_usage < 33 MB
10771077
- name: forktime-configured
10781078
thresholds:
1079-
- execution_time < 13 ms
1079+
- execution_time < 17 ms
10801080
- max_rss_usage < 60 MB
10811081

10821082
# rand
Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,20 @@
22
# This file is autogenerated by pip-compile with Python 3.14
33
# by the following command:
44
#
5-
# pip-compile --allow-unsafe --no-annotate .riot/requirements/1428c37.in
5+
# pip-compile --allow-unsafe --no-annotate .riot/requirements/11335dd.in
66
#
77
attrs==26.1.0
88
babel==2.18.0
99
blinker==1.9.0
10-
click==8.3.1
10+
certifi==2026.2.25
11+
charset-normalizer==3.4.7
12+
click==8.3.2
1113
coverage[toml]==7.13.5
1214
flask==2.3.3
1315
flask-babel==4.0.0
16+
greenlet==3.4.0
1417
hypothesis==6.45.0
18+
idna==3.11
1519
iniconfig==2.3.0
1620
itsdangerous==2.2.0
1721
jinja2==3.1.6
@@ -21,13 +25,15 @@ opentracing==2.4.0
2125
packaging==26.0
2226
pluggy==1.6.0
2327
psycopg2-binary==2.9.11
24-
pygments==2.19.2
25-
pytest==9.0.2
28+
pygments==2.20.0
29+
pytest==9.0.3
2630
pytest-cov==7.1.0
2731
pytest-mock==3.15.1
2832
pytest-randomly==4.0.1
2933
pytz==2026.1.post1
34+
requests==2.33.1
3035
sortedcontainers==2.4.0
31-
sqlalchemy==2.0.48
36+
sqlalchemy==2.0.49
3237
typing-extensions==4.15.0
33-
werkzeug==3.1.7
38+
urllib3==2.6.3
39+
werkzeug==3.1.8
Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,20 @@
22
# This file is autogenerated by pip-compile with Python 3.12
33
# by the following command:
44
#
5-
# pip-compile --allow-unsafe --no-annotate .riot/requirements/1df8e9a.in
5+
# pip-compile --allow-unsafe --no-annotate .riot/requirements/148c37a.in
66
#
77
attrs==26.1.0
88
babel==2.18.0
99
blinker==1.9.0
10-
click==8.3.1
10+
certifi==2026.2.25
11+
charset-normalizer==3.4.7
12+
click==8.3.2
1113
coverage[toml]==7.13.5
1214
flask==3.1.3
1315
flask-babel==4.0.0
16+
greenlet==3.4.0
1417
hypothesis==6.45.0
18+
idna==3.11
1519
iniconfig==2.3.0
1620
itsdangerous==2.2.0
1721
jinja2==3.1.6
@@ -21,13 +25,15 @@ opentracing==2.4.0
2125
packaging==26.0
2226
pluggy==1.6.0
2327
psycopg2-binary==2.9.11
24-
pygments==2.19.2
25-
pytest==9.0.2
28+
pygments==2.20.0
29+
pytest==9.0.3
2630
pytest-cov==7.1.0
2731
pytest-mock==3.15.1
2832
pytest-randomly==4.0.1
2933
pytz==2026.1.post1
34+
requests==2.33.1
3035
sortedcontainers==2.4.0
31-
sqlalchemy==2.0.48
36+
sqlalchemy==2.0.49
3237
typing-extensions==4.15.0
33-
werkzeug==3.1.7
38+
urllib3==2.6.3
39+
werkzeug==3.1.8
Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,21 @@
22
# This file is autogenerated by pip-compile with Python 3.9
33
# by the following command:
44
#
5-
# pip-compile --allow-unsafe --no-annotate .riot/requirements/5cef9f9.in
5+
# pip-compile --allow-unsafe --no-annotate .riot/requirements/176aab2.in
66
#
77
attrs==26.1.0
88
babel==2.18.0
99
blinker==1.9.0
10+
certifi==2026.2.25
11+
charset-normalizer==3.4.7
1012
click==8.1.8
1113
coverage[toml]==7.10.7
1214
exceptiongroup==1.3.1
1315
flask==2.3.3
1416
flask-babel==4.0.0
17+
greenlet==3.2.5
1518
hypothesis==6.45.0
19+
idna==3.11
1620
importlib-metadata==8.7.1
1721
iniconfig==2.1.0
1822
itsdangerous==2.2.0
@@ -23,15 +27,17 @@ opentracing==2.4.0
2327
packaging==26.0
2428
pluggy==1.6.0
2529
psycopg2-binary==2.9.11
26-
pygments==2.19.2
30+
pygments==2.20.0
2731
pytest==8.4.2
2832
pytest-cov==7.1.0
2933
pytest-mock==3.15.1
3034
pytest-randomly==4.0.1
3135
pytz==2026.1.post1
36+
requests==2.32.5
3237
sortedcontainers==2.4.0
33-
sqlalchemy==2.0.48
38+
sqlalchemy==2.0.49
3439
tomli==2.4.1
3540
typing-extensions==4.15.0
36-
werkzeug==3.1.7
41+
urllib3==2.6.3
42+
werkzeug==3.1.8
3743
zipp==3.23.0
Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,20 @@
22
# This file is autogenerated by pip-compile with Python 3.13
33
# by the following command:
44
#
5-
# pip-compile --allow-unsafe --no-annotate .riot/requirements/6843c56.in
5+
# pip-compile --allow-unsafe --no-annotate .riot/requirements/18269eb.in
66
#
77
attrs==26.1.0
88
babel==2.18.0
99
blinker==1.9.0
10-
click==8.3.1
10+
certifi==2026.2.25
11+
charset-normalizer==3.4.7
12+
click==8.3.2
1113
coverage[toml]==7.13.5
1214
flask==3.1.3
1315
flask-babel==4.0.0
16+
greenlet==3.4.0
1417
hypothesis==6.45.0
18+
idna==3.11
1519
iniconfig==2.3.0
1620
itsdangerous==2.2.0
1721
jinja2==3.1.6
@@ -21,13 +25,15 @@ opentracing==2.4.0
2125
packaging==26.0
2226
pluggy==1.6.0
2327
psycopg2-binary==2.9.11
24-
pygments==2.19.2
25-
pytest==9.0.2
28+
pygments==2.20.0
29+
pytest==9.0.3
2630
pytest-cov==7.1.0
2731
pytest-mock==3.15.1
2832
pytest-randomly==4.0.1
2933
pytz==2026.1.post1
34+
requests==2.33.1
3035
sortedcontainers==2.4.0
31-
sqlalchemy==2.0.48
36+
sqlalchemy==2.0.49
3237
typing-extensions==4.15.0
33-
werkzeug==3.1.7
38+
urllib3==2.6.3
39+
werkzeug==3.1.8
Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,21 @@
22
# This file is autogenerated by pip-compile with Python 3.10
33
# by the following command:
44
#
5-
# pip-compile --allow-unsafe --no-annotate .riot/requirements/1335c92.in
5+
# pip-compile --allow-unsafe --no-annotate .riot/requirements/190e5df.in
66
#
77
attrs==26.1.0
88
babel==2.18.0
99
blinker==1.9.0
10-
click==8.3.1
10+
certifi==2026.2.25
11+
charset-normalizer==3.4.7
12+
click==8.3.2
1113
coverage[toml]==7.13.5
1214
exceptiongroup==1.3.1
1315
flask==2.3.3
1416
flask-babel==4.0.0
17+
greenlet==3.4.0
1518
hypothesis==6.45.0
19+
idna==3.11
1620
iniconfig==2.3.0
1721
itsdangerous==2.2.0
1822
jinja2==3.1.6
@@ -22,14 +26,16 @@ opentracing==2.4.0
2226
packaging==26.0
2327
pluggy==1.6.0
2428
psycopg2-binary==2.9.11
25-
pygments==2.19.2
26-
pytest==9.0.2
29+
pygments==2.20.0
30+
pytest==9.0.3
2731
pytest-cov==7.1.0
2832
pytest-mock==3.15.1
2933
pytest-randomly==4.0.1
3034
pytz==2026.1.post1
35+
requests==2.33.1
3136
sortedcontainers==2.4.0
32-
sqlalchemy==2.0.48
37+
sqlalchemy==2.0.49
3338
tomli==2.4.1
3439
typing-extensions==4.15.0
35-
werkzeug==3.1.7
40+
urllib3==2.6.3
41+
werkzeug==3.1.8

.riot/requirements/1ab3731.txt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#
2+
# This file is autogenerated by pip-compile with Python 3.12
3+
# by the following command:
4+
#
5+
# pip-compile --allow-unsafe --no-annotate .riot/requirements/1ab3731.in
6+
#
7+
attrs==26.1.0
8+
coverage[toml]==7.13.5
9+
hypothesis==6.45.0
10+
iniconfig==2.3.0
11+
mock==5.2.0
12+
opentracing==2.4.0
13+
packaging==26.0
14+
pluggy==1.6.0
15+
pygments==2.20.0
16+
pytest==9.0.3
17+
pytest-cov==7.1.0
18+
pytest-mock==3.15.1
19+
sortedcontainers==2.4.0

.riot/requirements/1d488a9.txt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#
2+
# This file is autogenerated by pip-compile with Python 3.13
3+
# by the following command:
4+
#
5+
# pip-compile --allow-unsafe --no-annotate .riot/requirements/1d488a9.in
6+
#
7+
attrs==26.1.0
8+
coverage[toml]==7.13.5
9+
hypothesis==6.45.0
10+
iniconfig==2.3.0
11+
mock==5.2.0
12+
opentracing==2.4.0
13+
packaging==26.0
14+
pluggy==1.6.0
15+
pygments==2.20.0
16+
pytest==9.0.3
17+
pytest-cov==7.1.0
18+
pytest-mock==3.15.1
19+
sortedcontainers==2.4.0
Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,20 @@
22
# This file is autogenerated by pip-compile with Python 3.12
33
# by the following command:
44
#
5-
# pip-compile --allow-unsafe --no-annotate .riot/requirements/1f1aeb9.in
5+
# pip-compile --allow-unsafe --no-annotate .riot/requirements/1dbdbea.in
66
#
77
attrs==26.1.0
88
babel==2.18.0
99
blinker==1.9.0
10-
click==8.3.1
10+
certifi==2026.2.25
11+
charset-normalizer==3.4.7
12+
click==8.3.2
1113
coverage[toml]==7.13.5
1214
flask==2.3.3
1315
flask-babel==4.0.0
16+
greenlet==3.4.0
1417
hypothesis==6.45.0
18+
idna==3.11
1519
iniconfig==2.3.0
1620
itsdangerous==2.2.0
1721
jinja2==3.1.6
@@ -21,13 +25,15 @@ opentracing==2.4.0
2125
packaging==26.0
2226
pluggy==1.6.0
2327
psycopg2-binary==2.9.11
24-
pygments==2.19.2
25-
pytest==9.0.2
28+
pygments==2.20.0
29+
pytest==9.0.3
2630
pytest-cov==7.1.0
2731
pytest-mock==3.15.1
2832
pytest-randomly==4.0.1
2933
pytz==2026.1.post1
34+
requests==2.33.1
3035
sortedcontainers==2.4.0
31-
sqlalchemy==2.0.48
36+
sqlalchemy==2.0.49
3237
typing-extensions==4.15.0
33-
werkzeug==3.1.7
38+
urllib3==2.6.3
39+
werkzeug==3.1.8

.riot/requirements/1e5dd1a.txt

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
#
2+
# This file is autogenerated by pip-compile with Python 3.10
3+
# by the following command:
4+
#
5+
# pip-compile --allow-unsafe --no-annotate .riot/requirements/1e5dd1a.in
6+
#
7+
attrs==26.1.0
8+
coverage[toml]==7.13.5
9+
exceptiongroup==1.3.1
10+
hypothesis==6.45.0
11+
iniconfig==2.3.0
12+
mock==5.2.0
13+
opentracing==2.4.0
14+
packaging==26.0
15+
pluggy==1.6.0
16+
pygments==2.20.0
17+
pytest==9.0.3
18+
pytest-cov==7.1.0
19+
pytest-mock==3.15.1
20+
sortedcontainers==2.4.0
21+
tomli==2.4.1
22+
typing-extensions==4.15.0

0 commit comments

Comments
 (0)