Skip to content

chore(telemetry): introduce DependencyEntry model for dependency reporting#17092

Merged
gh-worker-dd-mergequeue-cf854d[bot] merged 13 commits into
mainfrom
avara1986/telemetry-dependency-model
Mar 30, 2026
Merged

chore(telemetry): introduce DependencyEntry model for dependency reporting#17092
gh-worker-dd-mergequeue-cf854d[bot] merged 13 commits into
mainfrom
avara1986/telemetry-dependency-model

Conversation

@avara1986

@avara1986 avara1986 commented Mar 24, 2026

Copy link
Copy Markdown
Member

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. 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.

avara1986 and others added 2 commits March 24, 2026 16:31
…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]>
@cit-pr-commenter-54b7da

cit-pr-commenter-54b7da Bot commented Mar 25, 2026

Copy link
Copy Markdown

Codeowners resolved as

ddtrace/internal/telemetry/data.py                                      @DataDog/apm-python
ddtrace/internal/telemetry/dependency.py                                @DataDog/apm-python
ddtrace/internal/telemetry/writer.py                                    @DataDog/apm-python
tests/telemetry/test_data.py                                            @DataDog/apm-python
tests/telemetry/test_dependency.py                                      @DataDog/apm-python

@avara1986 avara1986 added the changelog/no-changelog A changelog entry is not required for this PR. label Mar 25, 2026
avara1986 and others added 3 commits March 25, 2026 11:43
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]>
@avara1986 avara1986 changed the title feat(telemetry): introduce DependencyEntry model for dependency reporting cbl(telemetry): introduce DependencyEntry model for dependency reporting Mar 25, 2026
@avara1986 avara1986 changed the title cbl(telemetry): introduce DependencyEntry model for dependency reporting chore(telemetry): introduce DependencyEntry model for dependency reporting Mar 25, 2026
@avara1986
avara1986 marked this pull request as ready for review March 25, 2026 13:09
@avara1986
avara1986 requested a review from a team as a code owner March 25, 2026 13:09

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread ddtrace/internal/telemetry/writer.py Outdated
Comment thread ddtrace/internal/telemetry/dependency.py Outdated

@mabdinur mabdinur left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

avara1986 and others added 2 commits March 25, 2026 19:56
- 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]>
@avara1986
avara1986 requested a review from a team as a code owner March 25, 2026 19:15
@avara1986
avara1986 requested a review from emmettbutler March 25, 2026 19:15
@avara1986

avara1986 commented Mar 25, 2026

Copy link
Copy Markdown
Member Author
┌────────────┬─────────────────────────────────────────────────────┬────────────────────────────────────────────────────────────────────────────────────────────┬──────────┐
  │     #      │                        Issue                        │                                           Status                                           │ Evidence │
  ├────────────┼─────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────┼──────────┤
  │ 🔴 1       │ mark_initial_sent() never called                    │ Fixed — AIDEV-TODO added (line 69) explaining it's deferred to SCA PR                      │          │
  ├────────────┼─────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────┼──────────┤
  │ 🔴 2       │ _unsent_count goes stale                            │ Fixed — _unsent_count removed entirely, has_unsent_metadata() scans the list               │          │
  ├────────────┼─────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────┼──────────┤
  │ 🔴 3       │ json.dumps can crash telemetry thread               │ Fixed — wrapped in try/except (TypeError, ValueError) with log.debug fallback (line 38-40) │          │
  ├────────────┼─────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────┼──────────┤
  │ 🔴 4       │ add_metadata with no CVE id appends duplicates      │ Fixed — returns False when cve_id is None (line 120) with log.debug                        │          │
  ├────────────┼─────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────┼──────────┤
  │ 🟡 5       │ Dropped SCA events invisible                        │ Fixed — log.debug("Cannot attach metadata: package %r not yet tracked", ...) (line 170)    │          │
  ├────────────┼─────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────┼──────────┤
  │ 🟡 6       │ No writer-level test for attach_dependency_metadata │ Fixed — TestWriterAttachDependencyMetadata class added (line 169) with 2 tests             │          │
  ├────────────┼─────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────┼──────────┤
  │ 🟡 7       │ Empty version strings swallowed                     │ Fixed — log.debug("Empty version for dependency %s (module: %s)", ...) (line 95)           │          │
  ├────────────┼─────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────┼──────────┤
  │ 🔵         │ AIDEV-TODO on deferred methods                      │ Fixed — AIDEV-TODO on line 69                                                              │          │
  ├────────────┼─────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────┼──────────┤
  │ P2 (codex) │ attach_reachability_metadata returns True on dedup  │ Fixed — add_metadata returns bool, propagated through attach_reachability_metadata         │          │
  ├────────────┼─────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────┼──────────┤
  │ Extra      │ mark_sent() public API risk                         │ Fixed — renamed to _mark_sent() (line 45), only called from mark_all_metadata_sent         │          │
  └────────────┴─────────────────────────────────────────────────────┴────────────────────────────────────────────────────────────────────────────────────────────┴──────────┘

@avara1986
avara1986 requested a review from mabdinur March 25, 2026 21:38
@avara1986

Copy link
Copy Markdown
Member Author

updated PR with your suggestions, please @mabdinur review 🙏

@avara1986

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread ddtrace/internal/telemetry/dependency.py
Comment thread ddtrace/internal/telemetry/writer.py Outdated
@datadog-official

datadog-official Bot commented Mar 27, 2026

Copy link
Copy Markdown
Contributor

✅ Tests

🎉 All green!

❄️ No new flaky tests detected
🧪 All tests passed

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 0e07dd0 | Docs | Datadog PR Page | Was this helpful? React with 👍/👎 or give us feedback!

@mabdinur mabdinur left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good. Since this is a significant change can we confirm that the telemetry payloads are s

Comment thread ddtrace/internal/telemetry/data.py
@avara1986

Copy link
Copy Markdown
Member Author

/merge

@gh-worker-devflow-routing-ef8351

gh-worker-devflow-routing-ef8351 Bot commented Mar 27, 2026

Copy link
Copy Markdown

View all feedbacks in Devflow UI.

2026-03-27 14:07:56 UTC ℹ️ Start processing command /merge


2026-03-27 14:08:01 UTC ℹ️ MergeQueue: pull request added to the queue

The expected merge time in main is approximately 60m (p90).


2026-03-27 14:48:11 UTCMergeQueue: The checks failed on this merge request

Tests failed on this commit 0255c80:

What to do next?

  • Investigate the failures and when ready, re-add your pull request to the queue!
  • If your PR checks are green, try to rebase/merge. It might be because the CI run is a bit old.
  • Any question, go check the FAQ.

@avara1986

Copy link
Copy Markdown
Member Author

/merge

@gh-worker-devflow-routing-ef8351

gh-worker-devflow-routing-ef8351 Bot commented Mar 27, 2026

Copy link
Copy Markdown

View all feedbacks in Devflow UI.

2026-03-27 15:03:41 UTC ℹ️ Start processing command /merge


2026-03-27 15:03:50 UTC ℹ️ MergeQueue: pull request added to the queue

The expected merge time in main is approximately 59m (p90).


2026-03-27 15:50:11 UTCMergeQueue: The checks failed on this merge request

Tests failed on this commit 3671f72:

What to do next?

  • Investigate the failures and when ready, re-add your pull request to the queue!
  • If your PR checks are green, try to rebase/merge. It might be because the CI run is a bit old.
  • Any question, go check the FAQ.

@avara1986

Copy link
Copy Markdown
Member Author

/merge

@gh-worker-devflow-routing-ef8351

gh-worker-devflow-routing-ef8351 Bot commented Mar 27, 2026

Copy link
Copy Markdown

View all feedbacks in Devflow UI.

2026-03-27 15:51:09 UTC ℹ️ Start processing command /merge


2026-03-27 15:51:14 UTC ℹ️ MergeQueue: pull request added to the queue

The expected merge time in main is approximately 59m (p90).


2026-03-27 16:38:35 UTCMergeQueue: The checks failed on this merge request

Tests failed on this commit 84aec35:

What to do next?

  • Investigate the failures and when ready, re-add your pull request to the queue!
  • If your PR checks are green, try to rebase/merge. It might be because the CI run is a bit old.
  • Any question, go check the FAQ.

@gh-worker-dd-mergequeue-cf854d
gh-worker-dd-mergequeue-cf854d Bot merged commit 6b2a632 into main Mar 30, 2026
357 checks passed
@gh-worker-dd-mergequeue-cf854d
gh-worker-dd-mergequeue-cf854d Bot deleted the avara1986/telemetry-dependency-model branch March 30, 2026 08:59
brettlangdon pushed a commit that referenced this pull request Mar 30, 2026
…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]>
gh-worker-dd-mergequeue-cf854d Bot pushed a commit that referenced this pull request Apr 17, 2026
## 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]>
dubloom pushed a commit that referenced this pull request Apr 21, 2026
## 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]>
emmettbutler pushed a commit that referenced this pull request May 6, 2026
## 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]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

changelog/no-changelog A changelog entry is not required for this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants