chore(telemetry): introduce DependencyEntry model for dependency reporting#17092
Conversation
…ting Replace the flat dict[str, str] (name→version) in TelemetryWriter._imported_dependencies with a richer DependencyEntry model that supports optional reachability metadata. This enables SCA runtime reachability to attach CVE metadata to already-reported dependencies and trigger re-reporting through the existing app-dependencies-loaded telemetry event. No behavior change: dependencies without metadata produce the exact same wire format. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
- Add slots=True to DependencyEntry and ReachabilityMetadata dataclasses - Use lazy metadata (None instead of empty list) to avoid allocation per entry - Add _unsent_count counter for O(1) has_unsent_metadata() checks - Return DependencyEntry list from update_imported_dependencies (not dicts) to enable single-pass serialize+mark in _report_dependencies - Add _dirty_dependencies set to track entries needing re-report, replacing O(n) scan of all entries every periodic tick - Update tests and mini.py fixture for new return type Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Codeowners resolved as |
Manual __slots__ on @DataClass conflicts with fields that have defaults on Python < 3.10 (class-level descriptors clash with slot definitions). Drop __slots__ and use Optional[] instead of X | None union syntax. The other optimizations (lazy metadata, dirty-set, single-pass) provide the bulk of the performance gains. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Keep the hot path (update_imported_dependencies, _report_dependencies) nearly identical to main: return plain dicts, no metadata scanning, no dirty-set tracking. DependencyEntry is stored internally as the value type to support the future SCA PR, but the reporting logic stays simple since DD_APPSEC_SCA_ENABLED is not wired yet. Removed: _dirty_dependencies set, mark_initial_sent/mark_all_metadata_sent calls in writer, to_telemetry_dict calls in the reporting path, metadata in extended heartbeat. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ce0af6e564
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
mabdinur
left a comment
There was a problem hiding this comment.
I used Claude to this PR a first pass. Can we incorporate these fixes:
🔴 Must Fix Before Merge
#: 1
Issue: SCA metadata is never sent — mark_initial_sent() is never called, so
to_telemetry_dict() is never used in production. Any metadata from
attach_dependency_metadata is stored but silently dropped.
File: writer.py:311-343
What to do: Call entry.mark_initial_sent() after enqueuing, or add AIDEV-TODO if
this is intentionally deferred to the SCA PR.
────────────────────────────────────────
#: 2
Issue: _unsent_count goes stale — Calling mark_sent() on a ReachabilityMetadata
directly doesn't update the parent's counter. has_unsent_metadata() then lies.
File: dependency.py:87-104
What to do: Remove _unsent_count and scan the list instead (it's always short), or
make mark_sent() private.
────────────────────────────────────────
#: 3
Issue: json.dumps can crash the telemetry thread — If a value in
ReachabilityMetadata.value isn't JSON-serializable, to_telemetry_dict() raises
and
could kill the background thread.
File: dependency.py:33
What to do: Wrap in try/except, log the error, return "value": "{}" as fallback.
────────────────────────────────────────
#: 4
Issue: add_metadata with no CVE id appends duplicates forever — When value has no
"id" key, dedup is skipped and the list grows unboundedly.
File: dependency.py:94-104
What to do: Log a warning when cve_id is None, or deduplicate by full dict equality
as fallback.
🟡 Should Fix
#: 5
Issue: Dropped SCA events are invisible — When attach_dependency_metadata can't
find
the package yet, it returns False with no log. Events at startup are silently
lost.
File: writer.py:361
What to do: Add log.debug(...) with package_name and cve_id when returning False.
────────────────────────────────────────
#: 6
Issue: No integration test for attach_dependency_metadata — The unit tests cover
the
underlying function, but the writer-level method (with locking) is never tested.
File: tests/telemetry/test_writer.py
What to do: Add a test that calls telemetry_writer.attach_dependency_metadata(...)
and checks the result.
────────────────────────────────────────
#: 7
Issue: Empty version strings are swallowed — Failed version lookups produce
version="" with no log, sending ambiguous data to the backend.
File: data.py:88-90
What to do: Add log.debug(...) when version is empty.
🔵 Nice to Have
- Add AIDEV-TODO comments on needs_report(), mark_initial_sent(),
to_telemetry_dict() etc. if they're intentionally deferred — right now they look
like bugs. - test_get_application is now environment-sensitive — the old subprocess test was
more reliable. Consider restoring it.
✅ What's Good
- Lock usage in attach_dependency_metadata is correct — no data races.
- CVE deduplication works for the common case.
- Python 3.9 compat fixes (no slots=True, no X | None syntax) are correct.
- The AIDEV-NOTE on the None vs [] metadata default is helpful.
Suggested order of attack: Fix #1 (or explicitly defer it with a TODO) → Fix #2 and
#3 (correctness bugs) → Fix #4 and #5 (data loss) → Add #6 (test gap).
- Remove _unsent_count counter (stale when mark_sent called directly), scan metadata list instead (always short) - Wrap json.dumps in try/except to prevent telemetry thread crash - Reject metadata with no CVE id in add_metadata (return False) - Return False from attach_reachability_metadata on duplicate CVE - Rename mark_sent() -> _mark_sent() to prevent external misuse - Add log.debug for: empty versions, untracked packages, missing CVE id - Add AIDEV-TODO on methods deferred to the SCA PR - Add writer-level tests for attach_dependency_metadata (lazy upgrade) - Revert debug prints and hardcoded metadata from data.py/writer.py - Store plain str values in _imported_dependencies (zero overhead vs main), lazy upgrade to DependencyEntry only when SCA attaches metadata Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Store DependencyEntry in _imported_dependencies from the start instead of plain str values with lazy upgrade. This keeps the type consistent everywhere and removes isinstance checks in the heartbeat and attach_dependency_metadata paths. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
The benchmark script is maintained as a gist: https://gist.github.com/avara1986/eaa2389f4bf5ae18804772aaeb5e4f4a Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
|
|
updated PR with your suggestions, please @mabdinur review 🙏 |
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1e7bf1706f
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
|
✅ Tests 🎉 All green!❄️ No new flaky tests detected 🔗 Commit SHA: 0e07dd0 | Docs | Datadog PR Page | Was this helpful? React with 👍/👎 or give us feedback! |
mabdinur
left a comment
There was a problem hiding this comment.
Looks good. Since this is a significant change can we confirm that the telemetry payloads are s
|
/merge |
|
View all feedbacks in Devflow UI.
The expected merge time in
Tests failed on this commit 0255c80: What to do next?
|
|
/merge |
|
View all feedbacks in Devflow UI.
The expected merge time in
Tests failed on this commit 3671f72: What to do next?
|
|
/merge |
|
View all feedbacks in Devflow UI.
The expected merge time in
Tests failed on this commit 84aec35:
What to do next?
|
…rting (#17092) APPSEC-61908 ## Summary - Introduces `DependencyEntry` and `ReachabilityMetadata` dataclasses in `ddtrace/internal/telemetry/dependency.py` to replace the flat `dict[str, str]` used for `_imported_dependencies` in `TelemetryWriter` - The new model supports optional reachability metadata on each dependency, with sent/unsent tracking and deduplication by CVE id - Adds `attach_dependency_metadata()` method to `TelemetryWriter` for SCA to attach CVE metadata to already-reported dependencies, triggering re-reporting in the next `app-dependencies-loaded` telemetry cycle - **No behavior change**: dependencies without metadata produce the exact same wire format as before - **Reporting logic is identical to main**: `_report_dependencies` and `update_imported_dependencies` follow the same code path as main, the only difference is `DependencyEntry` as the internal storage type This is the first PR in a series to implement [Runtime SCA Reachability](https://docs.google.com/document/d/1HbaI2rhOP7XtiFUUPVLivEz_BtXupu3laOs6mVgrtiA). The follow-up PR will wire the SCA detection hook to call `attach_dependency_metadata()` when `DD_APPSEC_SCA_ENABLED=true`. ## Performance benchmark The overhead comes entirely from `DependencyEntry` dataclass construction instead of storing a plain version string. The reporting logic (`_report_dependencies`) is identical to main — no metadata scanning, no dirty-set tracking. That complexity will only be added in the future SCA PR, gated behind `DD_APPSEC_SCA_ENABLED`. Benchmark script: https://gist.github.com/avara1986/eaa2389f4bf5ae18804772aaeb5e4f4a ### `update_imported_dependencies` (Python 3.12, 200 iterations) | n_modules | main (median) | branch (median) | time overhead | mem overhead | |-----------|--------------|-----------------|---------------|-------------| | 50 | 46.5 us | 110.0 us | +137% | +47% | | 200 | 134.1 us | 350.1 us | +161% | +46% | | 500 | 336.6 us | 899.3 us | +167% | +48% | | 1000 | 671.2 us | 1,738.5 us | +159% | +48% | ### `_report_dependencies` (Python 3.12, 200 iterations) | n_modules | main (median) | branch (median) | time overhead | mem overhead | |-----------|--------------|-----------------|---------------|-------------| | 50 | 21.4 us | 51.1 us | +139% | +30% | | 200 | 74.5 us | 186.3 us | +150% | +31% | | 500 | 180.8 us | 461.2 us | +155% | +30% | | 1000 | 378.1 us | 915.5 us | +142% | +34% | ### Per-entry memory footprint | Type | B/entry (n=1000) | |------|-----------------| | `str` value (main) | 121.9 B | | `DependencyEntry` (branch) | 274.1 B | > **Note**: The overhead is constant per-dependency (~150 extra bytes, ~2.5x time) and comes entirely from constructing the `DependencyEntry` dataclass instead of storing a plain string. The reporting logic is identical to main. A typical application imports 50-200 dependencies, so the absolute cost is ~50-350 us per periodic tick — well within acceptable limits for a background telemetry task that runs every 10 seconds. Co-authored-by: christophe.papazian <[email protected]>
## 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 <[email protected]>
## 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 <[email protected]>
## 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 <[email protected]>
APPSEC-61908
Summary
DependencyEntryandReachabilityMetadatadataclasses inddtrace/internal/telemetry/dependency.pyto replace the flatdict[str, str]used for_imported_dependenciesinTelemetryWriterattach_dependency_metadata()method toTelemetryWriterfor SCA to attach CVE metadata to already-reported dependencies, triggering re-reporting in the nextapp-dependencies-loadedtelemetry cycle_report_dependenciesandupdate_imported_dependenciesfollow the same code path as main, the only difference isDependencyEntryas the internal storage typeThis is the first PR in a series to implement Runtime SCA Reachability. The follow-up PR will wire the SCA detection hook to call
attach_dependency_metadata()whenDD_APPSEC_SCA_ENABLED=true.Performance benchmark
The overhead comes entirely from
DependencyEntrydataclass construction instead of storing a plain version string. The reporting logic (_report_dependencies) is identical to main — no metadata scanning, no dirty-set tracking. That complexity will only be added in the future SCA PR, gated behindDD_APPSEC_SCA_ENABLED.Benchmark script: https://gist.github.com/avara1986/eaa2389f4bf5ae18804772aaeb5e4f4a
update_imported_dependencies(Python 3.12, 200 iterations)_report_dependencies(Python 3.12, 200 iterations)Per-entry memory footprint
strvalue (main)DependencyEntry(branch)