src2sink 1.0.2__tar.gz → 2.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {src2sink-1.0.2/src2sink.egg-info → src2sink-2.0.0}/PKG-INFO +59 -25
- {src2sink-1.0.2 → src2sink-2.0.0}/README.md +58 -20
- {src2sink-1.0.2 → src2sink-2.0.0}/pyproject.toml +35 -8
- {src2sink-1.0.2 → src2sink-2.0.0}/src2sink/aggregators/api_client_discovery.py +180 -11
- {src2sink-1.0.2 → src2sink-2.0.0}/src2sink/aggregators/index_v2.py +1 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/src2sink/aggregators/library_source_map.py +3 -1
- {src2sink-1.0.2 → src2sink-2.0.0}/src2sink/aggregators/openapi_match.py +21 -6
- {src2sink-1.0.2 → src2sink-2.0.0}/src2sink/aggregators/service_call_collect.py +144 -11
- src2sink-2.0.0/src2sink/aggregators/service_call_report.py +220 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/src2sink/aggregators/taint_buckets.py +4 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/src2sink/aggregators/taint_writers.py +41 -1
- {src2sink-1.0.2 → src2sink-2.0.0}/src2sink/build_metabase_v2.py +203 -57
- {src2sink-1.0.2 → src2sink-2.0.0}/src2sink/extractors/ast_walk.py +27 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/src2sink/extractors/base.py +2 -2
- {src2sink-1.0.2 → src2sink-2.0.0}/src2sink/extractors/config.py +14 -12
- {src2sink-1.0.2 → src2sink-2.0.0}/src2sink/extractors/file_context.py +3 -0
- src2sink-2.0.0/src2sink/extractors/http_out.py +324 -0
- src2sink-2.0.0/src2sink/extractors/patterns.py +408 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/src2sink/extractors/regex_extractors.py +207 -19
- src2sink-2.0.0/src2sink/extractors/symbols.py +78 -0
- src2sink-2.0.0/src2sink/extractors/ts_extractors.py +212 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/src2sink/extractors/unified.py +8 -1
- src2sink-2.0.0/src2sink/graph_common.py +360 -0
- src2sink-2.0.0/src2sink/known_api_clients.py +223 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/src2sink/repo_utils.py +9 -9
- {src2sink-1.0.2 → src2sink-2.0.0}/src2sink/sanitize.py +7 -2
- src2sink-2.0.0/src2sink/schema.py +89 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/src2sink/trace.py +109 -27
- {src2sink-1.0.2 → src2sink-2.0.0}/src2sink/trace_batch.py +26 -6
- {src2sink-1.0.2 → src2sink-2.0.0/src2sink.egg-info}/PKG-INFO +59 -25
- {src2sink-1.0.2 → src2sink-2.0.0}/src2sink.egg-info/SOURCES.txt +13 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/src2sink.egg-info/requires.txt +0 -4
- {src2sink-1.0.2 → src2sink-2.0.0}/tests/test_aggregators_extra.py +11 -11
- src2sink-2.0.0/tests/test_ast_walk.py +221 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/tests/test_build_internals.py +33 -6
- {src2sink-1.0.2 → src2sink-2.0.0}/tests/test_characterization.py +6 -6
- src2sink-2.0.0/tests/test_cross_repo_caller_coverage.py +731 -0
- src2sink-2.0.0/tests/test_demand_side_discovery.py +303 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/tests/test_dependency_pinning.py +85 -1
- src2sink-2.0.0/tests/test_detection_fingerprint_gate.py +103 -0
- src2sink-2.0.0/tests/test_detection_version.py +116 -0
- src2sink-2.0.0/tests/test_gradle_version_catalogs.py +223 -0
- src2sink-2.0.0/tests/test_graph_common.py +197 -0
- src2sink-2.0.0/tests/test_http_guard_evidence.py +199 -0
- src2sink-2.0.0/tests/test_import_graph.py +157 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/tests/test_internal_groups.py +2 -2
- {src2sink-1.0.2 → src2sink-2.0.0}/tests/test_limits.py +107 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/tests/test_openapi.py +9 -9
- src2sink-2.0.0/tests/test_phase1_config.py +110 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/tests/test_phase2_graphs.py +23 -23
- {src2sink-1.0.2 → src2sink-2.0.0}/tests/test_phase4_aggregators.py +4 -4
- {src2sink-1.0.2 → src2sink-2.0.0}/tests/test_phase4_extractor_snapshots.py +45 -6
- {src2sink-1.0.2 → src2sink-2.0.0}/tests/test_phase4_regression.py +2 -2
- src2sink-2.0.0/tests/test_prescreen.py +160 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/tests/test_redos_bounds.py +115 -6
- {src2sink-1.0.2 → src2sink-2.0.0}/tests/test_sanitize.py +60 -0
- src2sink-2.0.0/tests/test_sql_payload_out.py +255 -0
- src2sink-2.0.0/tests/test_sql_sink_evidence.py +582 -0
- src2sink-2.0.0/tests/test_sql_source_construction.py +272 -0
- src2sink-2.0.0/tests/test_trace_fleet_scaling.py +173 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/tests/test_trace_render.py +41 -12
- {src2sink-1.0.2 → src2sink-2.0.0}/tests/test_zz_security_coverage_gate.py +48 -1
- {src2sink-1.0.2 → src2sink-2.0.0}/tests/test_zz_untrusted_interpolation_guard.py +14 -2
- src2sink-1.0.2/src2sink/aggregators/service_call_report.py +0 -118
- src2sink-1.0.2/src2sink/extractors/http_out.py +0 -108
- src2sink-1.0.2/src2sink/extractors/patterns.py +0 -145
- src2sink-1.0.2/src2sink/extractors/ts_extractors.py +0 -123
- src2sink-1.0.2/src2sink/graph_common.py +0 -213
- src2sink-1.0.2/src2sink/known_api_clients.py +0 -108
- src2sink-1.0.2/src2sink/schema.py +0 -57
- src2sink-1.0.2/tests/test_graph_common.py +0 -74
- src2sink-1.0.2/tests/test_phase1_config.py +0 -53
- src2sink-1.0.2/tests/test_prescreen.py +0 -81
- {src2sink-1.0.2 → src2sink-2.0.0}/LICENSE +0 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/setup.cfg +0 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/src2sink/__init__.py +0 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/src2sink/aggregators/__init__.py +0 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/src2sink/aggregators/auth_cards.py +0 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/src2sink/aggregators/crypto_cards.py +0 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/src2sink/aggregators/data_stores.py +0 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/src2sink/aggregators/graphs.py +0 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/src2sink/aggregators/openapi_discovery.py +0 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/src2sink/aggregators/openapi_edges.py +0 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/src2sink/aggregators/openapi_models.py +0 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/src2sink/aggregators/openapi_report.py +0 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/src2sink/aggregators/payload_producers.py +0 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/src2sink/aggregators/phase3.py +0 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/src2sink/aggregators/pii_cross_repo.py +0 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/src2sink/aggregators/pii_flow_v2.py +0 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/src2sink/aggregators/pii_lifecycle.py +0 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/src2sink/aggregators/pii_lifecycle_report.py +0 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/src2sink/aggregators/pii_touchpoint_collect.py +0 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/src2sink/aggregators/pii_touchpoint_utils.py +0 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/src2sink/aggregators/queues.py +0 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/src2sink/aggregators/ropa.py +0 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/src2sink/aggregators/service_call_index.py +0 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/src2sink/aggregators/service_call_models.py +0 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/src2sink/aggregators/service_calls.py +0 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/src2sink/aggregators/taint_catalogs.py +0 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/src2sink/aggregators/traces_index.py +0 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/src2sink/constants.py +0 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/src2sink/curate_internal_libraries.py +0 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/src2sink/extractors/__init__.py +0 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/src2sink/extractors/go.py +0 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/src2sink/extractors/java.py +0 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/src2sink/extractors/javascript.py +0 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/src2sink/extractors/kotlin.py +0 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/src2sink/extractors/node_factory.py +0 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/src2sink/extractors/python_extractor.py +0 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/src2sink/internal_groups.py +0 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/src2sink/library_taint_java.py +0 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/src2sink/limits.py +0 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/src2sink/models/__init__.py +0 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/src2sink/models/auth_model.py +0 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/src2sink/models/crypto_agility.py +0 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/src2sink/models/pii_lifecycle.py +0 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/src2sink/models/ropa.py +0 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/src2sink/prescreen.py +0 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/src2sink/py.typed +0 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/src2sink/record_fleet_baseline.py +0 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/src2sink/renderers/__init__.py +0 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/src2sink/renderers/markdown.py +0 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/src2sink/renderers/mermaid.py +0 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/src2sink/safe_paths.py +0 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/src2sink/vocabulary.py +0 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/src2sink.egg-info/dependency_links.txt +0 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/src2sink.egg-info/entry_points.txt +0 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/src2sink.egg-info/top_level.txt +0 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/tests/test_api_client_discovery.py +0 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/tests/test_build_main.py +0 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/tests/test_config_hygiene.py +0 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/tests/test_curate.py +0 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/tests/test_field_precision.py +0 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/tests/test_module_imports.py +0 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/tests/test_output_neutralisation.py +0 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/tests/test_phase0_smoke.py +0 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/tests/test_phase1_extractors.py +0 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/tests/test_phase3_pii.py +0 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/tests/test_phase4_fleet_regression.py +0 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/tests/test_phase4_taint_caps.py +0 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/tests/test_post_phase4_backlog.py +0 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/tests/test_renderers.py +0 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/tests/test_repo_utils_helpers.py +0 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/tests/test_run_manifest.py +0 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/tests/test_security_input_handling.py +0 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/tests/test_source_map_fix.py +0 -0
- {src2sink-1.0.2 → src2sink-2.0.0}/tests/test_trace_batch.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: src2sink
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.0
|
|
4
4
|
Summary: Cross-repository source-to-sink metabase for LLM-assisted SAST
|
|
5
5
|
Author: Brett Crawley
|
|
6
6
|
License-Expression: MIT
|
|
@@ -20,10 +20,6 @@ Requires-Python: >=3.14
|
|
|
20
20
|
Description-Content-Type: text/markdown
|
|
21
21
|
License-File: LICENSE
|
|
22
22
|
Requires-Dist: defusedxml>=0.7.1
|
|
23
|
-
Requires-Dist: ipykernel>=7.2.0
|
|
24
|
-
Requires-Dist: jupyterlab>=4.5.7
|
|
25
|
-
Requires-Dist: openpyxl>=3.1.5
|
|
26
|
-
Requires-Dist: pandas>=3.0.2
|
|
27
23
|
Requires-Dist: tree-sitter>=0.25.2
|
|
28
24
|
Requires-Dist: tree-sitter-go>=0.25.0
|
|
29
25
|
Requires-Dist: tree-sitter-java>=0.23.5
|
|
@@ -33,11 +29,12 @@ Requires-Dist: tree-sitter-python>=0.25.0
|
|
|
33
29
|
Requires-Dist: tree-sitter-typescript>=0.23.2
|
|
34
30
|
Dynamic: license-file
|
|
35
31
|
|
|
36
|
-

|
|
37
33
|
# Source-Code Metabase
|
|
38
34
|
|
|
39
35
|
[](https://github.com/mimecast/src2sink/actions/workflows/ci.yml)
|
|
40
36
|
[](https://github.com/mimecast/src2sink/blob/main/LICENSE)
|
|
37
|
+
[](https://github.com/mimecast/src2sink/attestations)
|
|
41
38
|
|
|
42
39
|
A structured, human-readable knowledge base of the analysed source-code
|
|
43
40
|
ecosystem. Designed to be loaded as **context for LLM SAST** so that
|
|
@@ -46,7 +43,6 @@ sinks in another, internal libraries that act as transparent pass-
|
|
|
46
43
|
throughs to dangerous APIs.
|
|
47
44
|
|
|
48
45
|
**Canonical field definitions:** [`SCHEMA.md`](https://github.com/mimecast/src2sink/blob/main/SCHEMA.md)
|
|
49
|
-
**Roadmap / gaps:** [`NEXT_STEPS.md`](https://github.com/mimecast/src2sink/blob/main/NEXT_STEPS.md)
|
|
50
46
|
|
|
51
47
|
---
|
|
52
48
|
|
|
@@ -92,7 +88,8 @@ about the full path, not a single repo in isolation.
|
|
|
92
88
|
| `taint/config-data-stores.md` + `.jsonl` | JDBC/Mongo/Redis/S3 from config |
|
|
93
89
|
| `taint/config-security.md` + `.jsonl` | Security-sensitive config keys |
|
|
94
90
|
| `taint/config-crypto.md` + `.jsonl` | Cipher suites / signing algorithms from config |
|
|
95
|
-
| `graphs/service-call-graph.md` + `service-call-edges.jsonl` | Cross-repo HTTP edges (`http-in` ↔ enriched `http-out
|
|
91
|
+
| `graphs/service-call-graph.md` + `service-call-edges.jsonl` | Cross-repo HTTP edges (`http-in` ↔ enriched `http-out`, plus api-client-binding hops) |
|
|
92
|
+
| `graphs/service-call-unmatched.jsonl` | Outbound calls that resolved to nothing — the negative-coverage signal |
|
|
96
93
|
| `graphs/queue-graph.md` + `.jsonl` | Topic producers / consumers |
|
|
97
94
|
| `graphs/data-store-graph.md` + `.jsonl` | Config-discovered stores ↔ repos |
|
|
98
95
|
| `graphs/payload-endpoint-producers.md` + `.jsonl` | Registered API clients → target services |
|
|
@@ -109,7 +106,6 @@ about the full path, not a single repo in isolation.
|
|
|
109
106
|
|------|---------|
|
|
110
107
|
| `SCHEMA.md` | Field and node-family definitions |
|
|
111
108
|
| `README.md` | This file |
|
|
112
|
-
| `NEXT_STEPS.md` | Gaps and follow-ups |
|
|
113
109
|
| `internal-libraries/<coord>.md` | Per-library taint tables (hand-curated) |
|
|
114
110
|
| `conventions/*.md` | Auth, crypto agility, PII naming (mixed) |
|
|
115
111
|
|
|
@@ -191,26 +187,55 @@ pull request, and weekly (advisories move even when the code does not):
|
|
|
191
187
|
checkout. Known false positives carry inline `# nosec` / `# nosemgrep`
|
|
192
188
|
annotations with a stated reason rather than blanket rule exclusions.
|
|
193
189
|
|
|
190
|
+
### Release gates
|
|
191
|
+
|
|
192
|
+
A separate workflow, [`.github/workflows/release.yml`](https://github.com/mimecast/src2sink/blob/main/.github/workflows/release.yml),
|
|
193
|
+
runs when a `v*` tag is pushed — and on the 1st of each month as an unattended
|
|
194
|
+
rehearsal that builds and attests without publishing, so upstream breakage
|
|
195
|
+
surfaces before a release depends on it.
|
|
196
|
+
|
|
197
|
+
| Stage | What it does |
|
|
198
|
+
|---|---|
|
|
199
|
+
| `build` | builds sdist + wheel from the tagged tree, **fails if the tag and the packaged version disagree**, `twine check`s the metadata |
|
|
200
|
+
| `provenance` | generates SLSA provenance in an isolated builder the build steps cannot reach (Build L3) |
|
|
201
|
+
| `publish` | uploads to PyPI over OIDC Trusted Publishing — no API token exists in this repository — after a human approves the deployment |
|
|
202
|
+
| `release` | creates the GitHub release with the version's changelog section as the body, and attaches the artefacts plus the provenance |
|
|
203
|
+
|
|
204
|
+
Publishing runs only after provenance succeeds, so a failure there stops a
|
|
205
|
+
release before the irreversible step. Permissions are split per job: only
|
|
206
|
+
`publish` can mint an OIDC token, only `release` can write to the repository.
|
|
207
|
+
|
|
194
208
|
Publishing a version to PyPI: [`docs/releasing.md`](https://github.com/mimecast/src2sink/blob/main/docs/releasing.md).
|
|
195
209
|
Known small gaps: [`docs/todo.md`](https://github.com/mimecast/src2sink/blob/main/docs/todo.md).
|
|
196
210
|
|
|
197
211
|
### Verifying a release
|
|
198
212
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
213
|
+
The SLSA badge above is a claim, like every badge — it links to
|
|
214
|
+
[this repository's attestations](https://github.com/mimecast/src2sink/attestations),
|
|
215
|
+
which is the evidence. Do not take either on trust; the commands below check the
|
|
216
|
+
artefact you actually downloaded.
|
|
217
|
+
|
|
218
|
+
Releases carry signed build provenance tying each artefact to the source commit,
|
|
219
|
+
tag, and workflow run that produced it — so a file claiming to be src2sink can be
|
|
220
|
+
checked rather than trusted. From 1.0.3 the provenance is generated by an
|
|
221
|
+
isolated builder (**SLSA Build L3**); 1.0.2 is L2; 1.0.0 and 1.0.1 have none.
|
|
202
222
|
|
|
203
223
|
```sh
|
|
204
|
-
#
|
|
205
|
-
|
|
224
|
+
# SLSA provenance, attached to the GitHub release (L3)
|
|
225
|
+
slsa-verifier verify-artifact src2sink-1.0.3-py3-none-any.whl \
|
|
226
|
+
--provenance-path multiple.intoto.jsonl \
|
|
227
|
+
--source-uri github.com/mimecast/src2sink --source-tag v1.0.3
|
|
228
|
+
|
|
229
|
+
# GitHub attestation, for the same artefact
|
|
230
|
+
gh attestation verify src2sink-1.0.3-py3-none-any.whl --repo mimecast/src2sink
|
|
206
231
|
|
|
207
|
-
# installed from PyPI (PEP 740 attestation)
|
|
208
|
-
python -m pypi_attestations verify pypi --repo mimecast/src2sink src2sink-1.0.
|
|
232
|
+
# installed from PyPI instead (PEP 740 attestation)
|
|
233
|
+
python -m pypi_attestations verify pypi --repo mimecast/src2sink src2sink-1.0.3-*.whl
|
|
209
234
|
```
|
|
210
235
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
236
|
+
What the claim does and does not cover is in
|
|
237
|
+
[`docs/slsa.md`](https://github.com/mimecast/src2sink/blob/main/docs/slsa.md) —
|
|
238
|
+
the Build track says nothing about dependencies or source trustworthiness.
|
|
214
239
|
|
|
215
240
|
> **Incremental behaviour:** each re-run compares the repo's current `git HEAD` SHA
|
|
216
241
|
> against the SHA stored in the existing per-repo JSON. Repos that haven't changed are
|
|
@@ -293,8 +318,18 @@ resolved; anything still flagged afterwards has no matching `pom.xml` on disk
|
|
|
293
318
|
|
|
294
319
|
### Register another API client library
|
|
295
320
|
|
|
296
|
-
|
|
297
|
-
`
|
|
321
|
+
Add a binding to `api-clients.json` (see
|
|
322
|
+
[`docs/api-clients-json.md`](https://github.com/mimecast/src2sink/blob/main/docs/api-clients-json.md)
|
|
323
|
+
for the field reference and the `--discover-api-clients` drafting flow), then
|
|
324
|
+
re-run `src2sink-build --api-clients api-clients.json` (or `--graphs-only`).
|
|
325
|
+
|
|
326
|
+
This is how the tool sees hops that regular SAST cannot: a repo that calls a
|
|
327
|
+
service through its published client library has no URL, host, or service name
|
|
328
|
+
anywhere in its source, so the binding is the only thing that knows the call
|
|
329
|
+
crosses a service boundary. If a supplied `api-clients.json` loads zero bindings
|
|
330
|
+
the build fails rather than quietly producing a client-blind metabase; check
|
|
331
|
+
`api_clients_binding_count` in `run-manifest.json` and the "API-client binding
|
|
332
|
+
coverage" table in `graphs/service-call-graph.md` to confirm detection is live.
|
|
298
333
|
|
|
299
334
|
---
|
|
300
335
|
|
|
@@ -352,7 +387,7 @@ Treat **low** as “investigate”, not “confirmed”.
|
|
|
352
387
|
## Dependencies
|
|
353
388
|
|
|
354
389
|
v2 requires **tree-sitter** and per-language grammars (via
|
|
355
|
-
`uv sync`).
|
|
390
|
+
`uv sync`).
|
|
356
391
|
Do not add public PyPI fallbacks.
|
|
357
392
|
|
|
358
393
|
---
|
|
@@ -360,7 +395,6 @@ Do not add public PyPI fallbacks.
|
|
|
360
395
|
## See also
|
|
361
396
|
|
|
362
397
|
- [`SCHEMA.md`](https://github.com/mimecast/src2sink/blob/main/SCHEMA.md) — complete v2 node/edge vocabulary.
|
|
363
|
-
- [`docs/implementation-plan.md`](https://github.com/mimecast/src2sink/blob/main/docs/implementation-plan.md) — **phased plan and progress record**.
|
|
364
398
|
- [`docs/architecture.md`](https://github.com/mimecast/src2sink/blob/main/docs/architecture.md) — components, data flow, trust boundaries.
|
|
365
399
|
- [`docs/threat-model.md`](https://github.com/mimecast/src2sink/blob/main/docs/threat-model.md) — STRIDE risk register.
|
|
366
400
|
- [`docs/security-privacy-gap-analysis.md`](https://github.com/mimecast/src2sink/blob/main/docs/security-privacy-gap-analysis.md) — requirements, abuse cases, SRTM.
|
|
@@ -369,7 +403,7 @@ Do not add public PyPI fallbacks.
|
|
|
369
403
|
- [`docs/api-clients-json.md`](https://github.com/mimecast/src2sink/blob/main/docs/api-clients-json.md) — the internal-service binding file.
|
|
370
404
|
- [`metabase-usage.md`](https://github.com/mimecast/src2sink/blob/main/metabase-usage.md) — using the output as LLM context.
|
|
371
405
|
- [`docs/releasing.md`](https://github.com/mimecast/src2sink/blob/main/docs/releasing.md) — publishing a version to PyPI.
|
|
372
|
-
- [`docs/slsa.md`](https://github.com/mimecast/src2sink/blob/main/docs/slsa.md) — build
|
|
406
|
+
- [`docs/slsa.md`](https://github.com/mimecast/src2sink/blob/main/docs/slsa.md) — build provenance: what is claimed, how to verify it, and what it does not cover.
|
|
373
407
|
- [`CHANGELOG.md`](https://github.com/mimecast/src2sink/blob/main/CHANGELOG.md) — what shipped in each version.
|
|
374
|
-
- [`
|
|
408
|
+
- [`docs/todo.md`](https://github.com/mimecast/src2sink/blob/main/docs/todo.md) — known small gaps.
|
|
375
409
|
- `/ai-sast-scanner` skill — SAST prompt this metabase feeds.
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-

|
|
2
2
|
# Source-Code Metabase
|
|
3
3
|
|
|
4
4
|
[](https://github.com/mimecast/src2sink/actions/workflows/ci.yml)
|
|
5
5
|
[](https://github.com/mimecast/src2sink/blob/main/LICENSE)
|
|
6
|
+
[](https://github.com/mimecast/src2sink/attestations)
|
|
6
7
|
|
|
7
8
|
A structured, human-readable knowledge base of the analysed source-code
|
|
8
9
|
ecosystem. Designed to be loaded as **context for LLM SAST** so that
|
|
@@ -11,7 +12,6 @@ sinks in another, internal libraries that act as transparent pass-
|
|
|
11
12
|
throughs to dangerous APIs.
|
|
12
13
|
|
|
13
14
|
**Canonical field definitions:** [`SCHEMA.md`](https://github.com/mimecast/src2sink/blob/main/SCHEMA.md)
|
|
14
|
-
**Roadmap / gaps:** [`NEXT_STEPS.md`](https://github.com/mimecast/src2sink/blob/main/NEXT_STEPS.md)
|
|
15
15
|
|
|
16
16
|
---
|
|
17
17
|
|
|
@@ -57,7 +57,8 @@ about the full path, not a single repo in isolation.
|
|
|
57
57
|
| `taint/config-data-stores.md` + `.jsonl` | JDBC/Mongo/Redis/S3 from config |
|
|
58
58
|
| `taint/config-security.md` + `.jsonl` | Security-sensitive config keys |
|
|
59
59
|
| `taint/config-crypto.md` + `.jsonl` | Cipher suites / signing algorithms from config |
|
|
60
|
-
| `graphs/service-call-graph.md` + `service-call-edges.jsonl` | Cross-repo HTTP edges (`http-in` ↔ enriched `http-out
|
|
60
|
+
| `graphs/service-call-graph.md` + `service-call-edges.jsonl` | Cross-repo HTTP edges (`http-in` ↔ enriched `http-out`, plus api-client-binding hops) |
|
|
61
|
+
| `graphs/service-call-unmatched.jsonl` | Outbound calls that resolved to nothing — the negative-coverage signal |
|
|
61
62
|
| `graphs/queue-graph.md` + `.jsonl` | Topic producers / consumers |
|
|
62
63
|
| `graphs/data-store-graph.md` + `.jsonl` | Config-discovered stores ↔ repos |
|
|
63
64
|
| `graphs/payload-endpoint-producers.md` + `.jsonl` | Registered API clients → target services |
|
|
@@ -74,7 +75,6 @@ about the full path, not a single repo in isolation.
|
|
|
74
75
|
|------|---------|
|
|
75
76
|
| `SCHEMA.md` | Field and node-family definitions |
|
|
76
77
|
| `README.md` | This file |
|
|
77
|
-
| `NEXT_STEPS.md` | Gaps and follow-ups |
|
|
78
78
|
| `internal-libraries/<coord>.md` | Per-library taint tables (hand-curated) |
|
|
79
79
|
| `conventions/*.md` | Auth, crypto agility, PII naming (mixed) |
|
|
80
80
|
|
|
@@ -156,26 +156,55 @@ pull request, and weekly (advisories move even when the code does not):
|
|
|
156
156
|
checkout. Known false positives carry inline `# nosec` / `# nosemgrep`
|
|
157
157
|
annotations with a stated reason rather than blanket rule exclusions.
|
|
158
158
|
|
|
159
|
+
### Release gates
|
|
160
|
+
|
|
161
|
+
A separate workflow, [`.github/workflows/release.yml`](https://github.com/mimecast/src2sink/blob/main/.github/workflows/release.yml),
|
|
162
|
+
runs when a `v*` tag is pushed — and on the 1st of each month as an unattended
|
|
163
|
+
rehearsal that builds and attests without publishing, so upstream breakage
|
|
164
|
+
surfaces before a release depends on it.
|
|
165
|
+
|
|
166
|
+
| Stage | What it does |
|
|
167
|
+
|---|---|
|
|
168
|
+
| `build` | builds sdist + wheel from the tagged tree, **fails if the tag and the packaged version disagree**, `twine check`s the metadata |
|
|
169
|
+
| `provenance` | generates SLSA provenance in an isolated builder the build steps cannot reach (Build L3) |
|
|
170
|
+
| `publish` | uploads to PyPI over OIDC Trusted Publishing — no API token exists in this repository — after a human approves the deployment |
|
|
171
|
+
| `release` | creates the GitHub release with the version's changelog section as the body, and attaches the artefacts plus the provenance |
|
|
172
|
+
|
|
173
|
+
Publishing runs only after provenance succeeds, so a failure there stops a
|
|
174
|
+
release before the irreversible step. Permissions are split per job: only
|
|
175
|
+
`publish` can mint an OIDC token, only `release` can write to the repository.
|
|
176
|
+
|
|
159
177
|
Publishing a version to PyPI: [`docs/releasing.md`](https://github.com/mimecast/src2sink/blob/main/docs/releasing.md).
|
|
160
178
|
Known small gaps: [`docs/todo.md`](https://github.com/mimecast/src2sink/blob/main/docs/todo.md).
|
|
161
179
|
|
|
162
180
|
### Verifying a release
|
|
163
181
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
182
|
+
The SLSA badge above is a claim, like every badge — it links to
|
|
183
|
+
[this repository's attestations](https://github.com/mimecast/src2sink/attestations),
|
|
184
|
+
which is the evidence. Do not take either on trust; the commands below check the
|
|
185
|
+
artefact you actually downloaded.
|
|
186
|
+
|
|
187
|
+
Releases carry signed build provenance tying each artefact to the source commit,
|
|
188
|
+
tag, and workflow run that produced it — so a file claiming to be src2sink can be
|
|
189
|
+
checked rather than trusted. From 1.0.3 the provenance is generated by an
|
|
190
|
+
isolated builder (**SLSA Build L3**); 1.0.2 is L2; 1.0.0 and 1.0.1 have none.
|
|
167
191
|
|
|
168
192
|
```sh
|
|
169
|
-
#
|
|
170
|
-
|
|
193
|
+
# SLSA provenance, attached to the GitHub release (L3)
|
|
194
|
+
slsa-verifier verify-artifact src2sink-1.0.3-py3-none-any.whl \
|
|
195
|
+
--provenance-path multiple.intoto.jsonl \
|
|
196
|
+
--source-uri github.com/mimecast/src2sink --source-tag v1.0.3
|
|
197
|
+
|
|
198
|
+
# GitHub attestation, for the same artefact
|
|
199
|
+
gh attestation verify src2sink-1.0.3-py3-none-any.whl --repo mimecast/src2sink
|
|
171
200
|
|
|
172
|
-
# installed from PyPI (PEP 740 attestation)
|
|
173
|
-
python -m pypi_attestations verify pypi --repo mimecast/src2sink src2sink-1.0.
|
|
201
|
+
# installed from PyPI instead (PEP 740 attestation)
|
|
202
|
+
python -m pypi_attestations verify pypi --repo mimecast/src2sink src2sink-1.0.3-*.whl
|
|
174
203
|
```
|
|
175
204
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
205
|
+
What the claim does and does not cover is in
|
|
206
|
+
[`docs/slsa.md`](https://github.com/mimecast/src2sink/blob/main/docs/slsa.md) —
|
|
207
|
+
the Build track says nothing about dependencies or source trustworthiness.
|
|
179
208
|
|
|
180
209
|
> **Incremental behaviour:** each re-run compares the repo's current `git HEAD` SHA
|
|
181
210
|
> against the SHA stored in the existing per-repo JSON. Repos that haven't changed are
|
|
@@ -258,8 +287,18 @@ resolved; anything still flagged afterwards has no matching `pom.xml` on disk
|
|
|
258
287
|
|
|
259
288
|
### Register another API client library
|
|
260
289
|
|
|
261
|
-
|
|
262
|
-
`
|
|
290
|
+
Add a binding to `api-clients.json` (see
|
|
291
|
+
[`docs/api-clients-json.md`](https://github.com/mimecast/src2sink/blob/main/docs/api-clients-json.md)
|
|
292
|
+
for the field reference and the `--discover-api-clients` drafting flow), then
|
|
293
|
+
re-run `src2sink-build --api-clients api-clients.json` (or `--graphs-only`).
|
|
294
|
+
|
|
295
|
+
This is how the tool sees hops that regular SAST cannot: a repo that calls a
|
|
296
|
+
service through its published client library has no URL, host, or service name
|
|
297
|
+
anywhere in its source, so the binding is the only thing that knows the call
|
|
298
|
+
crosses a service boundary. If a supplied `api-clients.json` loads zero bindings
|
|
299
|
+
the build fails rather than quietly producing a client-blind metabase; check
|
|
300
|
+
`api_clients_binding_count` in `run-manifest.json` and the "API-client binding
|
|
301
|
+
coverage" table in `graphs/service-call-graph.md` to confirm detection is live.
|
|
263
302
|
|
|
264
303
|
---
|
|
265
304
|
|
|
@@ -317,7 +356,7 @@ Treat **low** as “investigate”, not “confirmed”.
|
|
|
317
356
|
## Dependencies
|
|
318
357
|
|
|
319
358
|
v2 requires **tree-sitter** and per-language grammars (via
|
|
320
|
-
`uv sync`).
|
|
359
|
+
`uv sync`).
|
|
321
360
|
Do not add public PyPI fallbacks.
|
|
322
361
|
|
|
323
362
|
---
|
|
@@ -325,7 +364,6 @@ Do not add public PyPI fallbacks.
|
|
|
325
364
|
## See also
|
|
326
365
|
|
|
327
366
|
- [`SCHEMA.md`](https://github.com/mimecast/src2sink/blob/main/SCHEMA.md) — complete v2 node/edge vocabulary.
|
|
328
|
-
- [`docs/implementation-plan.md`](https://github.com/mimecast/src2sink/blob/main/docs/implementation-plan.md) — **phased plan and progress record**.
|
|
329
367
|
- [`docs/architecture.md`](https://github.com/mimecast/src2sink/blob/main/docs/architecture.md) — components, data flow, trust boundaries.
|
|
330
368
|
- [`docs/threat-model.md`](https://github.com/mimecast/src2sink/blob/main/docs/threat-model.md) — STRIDE risk register.
|
|
331
369
|
- [`docs/security-privacy-gap-analysis.md`](https://github.com/mimecast/src2sink/blob/main/docs/security-privacy-gap-analysis.md) — requirements, abuse cases, SRTM.
|
|
@@ -334,7 +372,7 @@ Do not add public PyPI fallbacks.
|
|
|
334
372
|
- [`docs/api-clients-json.md`](https://github.com/mimecast/src2sink/blob/main/docs/api-clients-json.md) — the internal-service binding file.
|
|
335
373
|
- [`metabase-usage.md`](https://github.com/mimecast/src2sink/blob/main/metabase-usage.md) — using the output as LLM context.
|
|
336
374
|
- [`docs/releasing.md`](https://github.com/mimecast/src2sink/blob/main/docs/releasing.md) — publishing a version to PyPI.
|
|
337
|
-
- [`docs/slsa.md`](https://github.com/mimecast/src2sink/blob/main/docs/slsa.md) — build
|
|
375
|
+
- [`docs/slsa.md`](https://github.com/mimecast/src2sink/blob/main/docs/slsa.md) — build provenance: what is claimed, how to verify it, and what it does not cover.
|
|
338
376
|
- [`CHANGELOG.md`](https://github.com/mimecast/src2sink/blob/main/CHANGELOG.md) — what shipped in each version.
|
|
339
|
-
- [`
|
|
377
|
+
- [`docs/todo.md`](https://github.com/mimecast/src2sink/blob/main/docs/todo.md) — known small gaps.
|
|
340
378
|
- `/ai-sast-scanner` skill — SAST prompt this metabase feeds.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "src2sink"
|
|
3
|
-
version = "
|
|
3
|
+
version = "2.0.0"
|
|
4
4
|
description = "Cross-repository source-to-sink metabase for LLM-assisted SAST"
|
|
5
5
|
authors = [{ name = "Brett Crawley" }]
|
|
6
6
|
readme = "README.md"
|
|
@@ -18,12 +18,11 @@ classifiers = [
|
|
|
18
18
|
"Topic :: Software Development :: Quality Assurance",
|
|
19
19
|
"Typing :: Typed",
|
|
20
20
|
]
|
|
21
|
+
# Kept deliberately small: this package is installed into other people's
|
|
22
|
+
# environments, so every runtime dependency is audited surface (SC-1 / TA-011).
|
|
23
|
+
# Anything listed here must be imported somewhere under src2sink/.
|
|
21
24
|
dependencies = [
|
|
22
25
|
"defusedxml>=0.7.1",
|
|
23
|
-
"ipykernel>=7.2.0",
|
|
24
|
-
"jupyterlab>=4.5.7",
|
|
25
|
-
"openpyxl>=3.1.5",
|
|
26
|
-
"pandas>=3.0.2",
|
|
27
26
|
"tree-sitter>=0.25.2",
|
|
28
27
|
"tree-sitter-go>=0.25.0",
|
|
29
28
|
"tree-sitter-java>=0.23.5",
|
|
@@ -70,8 +69,12 @@ src2sink-baseline = "src2sink.record_fleet_baseline:main"
|
|
|
70
69
|
|
|
71
70
|
[tool.pytest.ini_options]
|
|
72
71
|
testpaths = ["tests"]
|
|
73
|
-
# Coverage is reported and gated on every run
|
|
74
|
-
|
|
72
|
+
# Coverage is reported and gated on every run. The global floor ratchets: it is
|
|
73
|
+
# set just under current coverage so it catches a real regression, and is raised
|
|
74
|
+
# when coverage rises. Per-module floors live in tests/test_zz_security_coverage_gate.py
|
|
75
|
+
# (security modules at 90%, detection modules at 88%), since coverage.py has a
|
|
76
|
+
# single global fail_under and no per-module concept.
|
|
77
|
+
addopts = "--cov=src2sink --cov-report=term-missing --cov-fail-under=85"
|
|
75
78
|
markers = [
|
|
76
79
|
"fleet: fleet-wide regression (needs metabase/repos v2 JSONs)",
|
|
77
80
|
]
|
|
@@ -96,4 +99,28 @@ pretty = true
|
|
|
96
99
|
# E7 statement, E9 syntax). Deliberately NOT enabling the line-length / whitespace
|
|
97
100
|
# style families (E1/E2/E5, W) — turning those on is a large, separate reformat.
|
|
98
101
|
# target-version is inferred from project.requires-python.
|
|
99
|
-
|
|
102
|
+
#
|
|
103
|
+
# D1xx are the docstring *presence* rules: every module, class and public function
|
|
104
|
+
# must be documented. The rest of the D family is deliberately left off — D401
|
|
105
|
+
# ("imperative mood") would require rewriting 23 predicate docstrings from
|
|
106
|
+
# "True when the receiver names a database handle" to "Return True when…", which
|
|
107
|
+
# is noise rather than clarity for a boolean-returning function.
|
|
108
|
+
#
|
|
109
|
+
# PLC0415 keeps imports at module level. 26 had drifted into function bodies and
|
|
110
|
+
# only one was load-bearing; the rest were habit, including a constant that the
|
|
111
|
+
# same package already imported at the top elsewhere. The rule has no autofix, so
|
|
112
|
+
# it reports rather than moves — which is what makes it safe here: a real import
|
|
113
|
+
# cycle must be answered with a `# noqa: PLC0415` naming the cycle, not silently
|
|
114
|
+
# rewritten. Note that isort (`I`) does NOT cover this; it only orders the
|
|
115
|
+
# top-level block and leaves a function-level import exactly where it sits.
|
|
116
|
+
select = ["E4", "E7", "E9", "F", "D1", "PLC0415"]
|
|
117
|
+
|
|
118
|
+
[tool.ruff.lint.per-file-ignores]
|
|
119
|
+
# Tests document themselves through their names and module docstrings; requiring
|
|
120
|
+
# one on every fixture helper adds ceremony without adding information.
|
|
121
|
+
#
|
|
122
|
+
# Tests and gate scripts also import inside functions on purpose — to read a
|
|
123
|
+
# module fresh after mutating config, or to assert on import behaviour itself.
|
|
124
|
+
# PLC0415 protects the package, whose import graph is what a cycle can break.
|
|
125
|
+
"tests/*" = ["D1", "PLC0415"]
|
|
126
|
+
"scripts/*" = ["PLC0415"]
|
|
@@ -30,7 +30,10 @@ from collections import defaultdict
|
|
|
30
30
|
from pathlib import Path
|
|
31
31
|
from typing import Any
|
|
32
32
|
|
|
33
|
-
from ..graph_common import load_v2_repo_records, repo_id
|
|
33
|
+
from ..graph_common import load_v2_repo_records, match_path_in_inbound_index, repo_id
|
|
34
|
+
from ..repo_utils import _build_component_identity_index
|
|
35
|
+
from .library_source_map import _resolve_clone_path
|
|
36
|
+
from .service_call_index import build_inbound_index
|
|
34
37
|
|
|
35
38
|
DISCOVERED_FILE = "api-clients.discovered.json"
|
|
36
39
|
|
|
@@ -144,6 +147,88 @@ def _collect_candidates(
|
|
|
144
147
|
return cands
|
|
145
148
|
|
|
146
149
|
|
|
150
|
+
# A proposed class_pattern is matched as a plain substring in an unguarded,
|
|
151
|
+
# language-agnostic tier, so one appearing across the fleet manufactures phantom
|
|
152
|
+
# edges everywhere rather than merely adding noise.
|
|
153
|
+
MAX_PATTERN_REPOS = 3
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _is_binding_stamped(detail: dict[str, Any]) -> bool:
|
|
157
|
+
"""True when a binding, not an observation, put the target on this node.
|
|
158
|
+
|
|
159
|
+
Demand-side discovery resolves targets against routes and aliases that
|
|
160
|
+
promoted bindings already influence. Re-ingesting an edge a binding created
|
|
161
|
+
as fresh evidence *for that binding* inflates confidence on every run, so the
|
|
162
|
+
provenance already recorded in ``target_repo_evidence`` is used to exclude it.
|
|
163
|
+
"""
|
|
164
|
+
evidence = detail.get("target_repo_evidence")
|
|
165
|
+
return isinstance(evidence, str) and evidence.startswith("api-client class")
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _enclosing_class(file_path: str) -> str:
|
|
169
|
+
"""Best-effort class name for a call site, from its file name.
|
|
170
|
+
|
|
171
|
+
The aggregation phase has the metabase, not the sources, so the enclosing
|
|
172
|
+
class is taken from the file stem. That is exact for Java and Kotlin, where
|
|
173
|
+
the public type must match the file name, and a reasonable proposal
|
|
174
|
+
elsewhere — the reviewer confirms it either way.
|
|
175
|
+
"""
|
|
176
|
+
return Path(file_path).stem if file_path else ""
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _repos_containing_class(records: list[dict[str, Any]], class_name: str) -> set[str]:
|
|
180
|
+
"""Repos with a file of this name, as a proxy for corpus-wide occurrence."""
|
|
181
|
+
hits: set[str] = set()
|
|
182
|
+
for data in records:
|
|
183
|
+
rid = repo_id(data)
|
|
184
|
+
for node in data.get("nodes", []):
|
|
185
|
+
if _enclosing_class(str(node.get("file", ""))) == class_name:
|
|
186
|
+
hits.add(rid)
|
|
187
|
+
break
|
|
188
|
+
return hits
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _demand_side_observations(
|
|
192
|
+
records: list[dict[str, Any]],
|
|
193
|
+
) -> dict[str, dict[str, Any]]:
|
|
194
|
+
"""Map target repo -> observed call sites that resolve to it.
|
|
195
|
+
|
|
196
|
+
Mines the direction supply-side discovery cannot reach: a call site landing
|
|
197
|
+
on a known service in a repo that declares no client library for it. A repo
|
|
198
|
+
that hand-rolls HTTP has no ``*-client`` dependency, so no amount of
|
|
199
|
+
dependency parsing finds it (OI-4).
|
|
200
|
+
"""
|
|
201
|
+
|
|
202
|
+
inbound = build_inbound_index(records)
|
|
203
|
+
memo: dict[str, tuple[list[Any], str]] = {}
|
|
204
|
+
observed: dict[str, dict[str, Any]] = {}
|
|
205
|
+
|
|
206
|
+
for data in records:
|
|
207
|
+
consumer = repo_id(data)
|
|
208
|
+
for node in data.get("nodes", []):
|
|
209
|
+
if node.get("family") != "http-out":
|
|
210
|
+
continue
|
|
211
|
+
detail = node.get("detail") or {}
|
|
212
|
+
if _is_binding_stamped(detail):
|
|
213
|
+
continue
|
|
214
|
+
path = detail.get("path")
|
|
215
|
+
if not isinstance(path, str) or not path:
|
|
216
|
+
continue
|
|
217
|
+
rows, _conf = match_path_in_inbound_index(path, inbound, memo=memo)
|
|
218
|
+
for target, target_path, _method, _ref in rows:
|
|
219
|
+
if target == consumer:
|
|
220
|
+
continue
|
|
221
|
+
entry = observed.setdefault(
|
|
222
|
+
target, {"consumers": set(), "classes": set(), "paths": set()}
|
|
223
|
+
)
|
|
224
|
+
entry["consumers"].add(consumer)
|
|
225
|
+
entry["paths"].add(target_path)
|
|
226
|
+
cls = _enclosing_class(str(node.get("file", "")))
|
|
227
|
+
if cls:
|
|
228
|
+
entry["classes"].add(cls)
|
|
229
|
+
return observed
|
|
230
|
+
|
|
231
|
+
|
|
147
232
|
def _build_entry(
|
|
148
233
|
cand: dict[str, Any],
|
|
149
234
|
scanned: set[str],
|
|
@@ -152,6 +237,7 @@ def _build_entry(
|
|
|
152
237
|
"""Assemble a candidate entry, preserving a reviewer's non-pending decision."""
|
|
153
238
|
consumers = sorted(cand.pop("consumers"))
|
|
154
239
|
coordinate = cand.pop("coordinate")
|
|
240
|
+
warnings = cand.pop("warnings", [])
|
|
155
241
|
has_paths = bool(cand["paths"])
|
|
156
242
|
evidence = {
|
|
157
243
|
"coordinate": coordinate,
|
|
@@ -167,6 +253,13 @@ def _build_entry(
|
|
|
167
253
|
else:
|
|
168
254
|
entry = {k: cand[k] for k in _TUNABLE_FIELDS}
|
|
169
255
|
entry["status"] = STATUS_PENDING
|
|
256
|
+
# How the candidate was found. `both` is materially stronger than either
|
|
257
|
+
# alone: a declared dependency and an observed call site are independent
|
|
258
|
+
# lines of evidence. `call-site` sorts lowest, since it rests on the path
|
|
259
|
+
# matching that OI-1 showed can be wrong.
|
|
260
|
+
entry["discovery_method"] = cand.get("discovery_method", "dependency")
|
|
261
|
+
if warnings:
|
|
262
|
+
entry["warnings"] = warnings
|
|
170
263
|
entry["confidence"] = _confidence(
|
|
171
264
|
str(entry["target_repo"] or ""), scanned, bool(entry["paths"])
|
|
172
265
|
)
|
|
@@ -174,6 +267,87 @@ def _build_entry(
|
|
|
174
267
|
return entry
|
|
175
268
|
|
|
176
269
|
|
|
270
|
+
def _apply_demand_side(
|
|
271
|
+
cands: dict[str, dict[str, Any]],
|
|
272
|
+
records: list[dict[str, Any]],
|
|
273
|
+
) -> None:
|
|
274
|
+
"""Enrich supply-side candidates with observed call sites, or create new ones.
|
|
275
|
+
|
|
276
|
+
Runs *after* the supply-side pass so it can do a keyed lookup rather than a
|
|
277
|
+
merge: the two directions produce different fields for the same candidate
|
|
278
|
+
(``maven_artifact`` and ``import_prefix`` only exist supply-side;
|
|
279
|
+
``class_patterns`` only demand-side), and a demand-side-only candidate has no
|
|
280
|
+
artifact id to key on. Sequencing is for correctness, not speed — both passes
|
|
281
|
+
run in the aggregation phase with the fleet already in memory.
|
|
282
|
+
"""
|
|
283
|
+
observed = _demand_side_observations(records)
|
|
284
|
+
by_target = {c["target_repo"]: (k, c) for k, c in cands.items() if c["target_repo"]}
|
|
285
|
+
|
|
286
|
+
for target, seen in sorted(observed.items()):
|
|
287
|
+
classes = sorted(seen["classes"])
|
|
288
|
+
warnings = []
|
|
289
|
+
for cls in classes:
|
|
290
|
+
repos = _repos_containing_class(records, cls)
|
|
291
|
+
if len(repos) > MAX_PATTERN_REPOS:
|
|
292
|
+
warnings.append(
|
|
293
|
+
f"class_pattern {cls!r} appears in {len(repos)} repos; too "
|
|
294
|
+
"generic to be safe — narrow it before accepting"
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
existing = by_target.get(target)
|
|
298
|
+
if existing is not None:
|
|
299
|
+
_key, cand = existing
|
|
300
|
+
cand["class_patterns"] = sorted({*cand["class_patterns"], *classes})
|
|
301
|
+
cand["paths"] = sorted({*cand["paths"], *seen["paths"]})
|
|
302
|
+
cand["consumers"] |= seen["consumers"]
|
|
303
|
+
cand["discovery_method"] = "both"
|
|
304
|
+
if warnings:
|
|
305
|
+
cand["warnings"] = warnings
|
|
306
|
+
continue
|
|
307
|
+
|
|
308
|
+
# No dependency declares this hop — the hand-rolled case supply-side
|
|
309
|
+
# discovery cannot reach at all.
|
|
310
|
+
cands[_key_demand(target)] = {
|
|
311
|
+
"target_repo": target,
|
|
312
|
+
"maven_artifact": "",
|
|
313
|
+
"import_prefix": "",
|
|
314
|
+
"paths": sorted(seen["paths"]),
|
|
315
|
+
"payload_fields": ["sql"],
|
|
316
|
+
"service_aliases": [target.split("/")[-1]],
|
|
317
|
+
"class_patterns": classes,
|
|
318
|
+
"coordinate": "",
|
|
319
|
+
"consumers": set(seen["consumers"]),
|
|
320
|
+
"discovery_method": "call-site",
|
|
321
|
+
**({"warnings": warnings} if warnings else {}),
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def _key_demand(target_repo: str) -> str:
|
|
326
|
+
"""Candidate key for a call-site-only hop, which has no artifact to key on."""
|
|
327
|
+
return _key(target_repo, "")
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def discover_api_clients_from_records(
|
|
331
|
+
metabase_root: Path,
|
|
332
|
+
records: list[dict[str, Any]],
|
|
333
|
+
resolve: Any,
|
|
334
|
+
) -> int:
|
|
335
|
+
"""Run both discovery passes over in-memory records; returns the candidate count."""
|
|
336
|
+
scanned = {repo_id(d) for d in records}
|
|
337
|
+
cands = _collect_candidates(records, resolve)
|
|
338
|
+
for cand in cands.values():
|
|
339
|
+
cand.setdefault("discovery_method", "dependency")
|
|
340
|
+
_apply_demand_side(cands, records)
|
|
341
|
+
prev = _load_discovered(metabase_root / DISCOVERED_FILE)
|
|
342
|
+
|
|
343
|
+
entries = [
|
|
344
|
+
_build_entry(cand, scanned, prev.get(key))
|
|
345
|
+
for key, cand in sorted(cands.items())
|
|
346
|
+
]
|
|
347
|
+
_write_discovered(metabase_root, entries)
|
|
348
|
+
return len(entries)
|
|
349
|
+
|
|
350
|
+
|
|
177
351
|
def discover_api_clients(
|
|
178
352
|
metabase_root: Path,
|
|
179
353
|
repo_jsons: list[Path],
|
|
@@ -185,23 +359,19 @@ def discover_api_clients(
|
|
|
185
359
|
``status`` and tuned fields of any candidate a reviewer has already accepted
|
|
186
360
|
or rejected. Never touches the authoritative ``api-clients.json``.
|
|
187
361
|
"""
|
|
188
|
-
from ..repo_utils import _build_component_identity_index
|
|
189
|
-
from .library_source_map import _resolve_clone_path
|
|
190
362
|
|
|
191
363
|
records = load_v2_repo_records(metabase_root, json_paths=repo_jsons)
|
|
192
364
|
by_coord, by_name, by_full = _build_component_identity_index(repos_root)
|
|
193
365
|
|
|
194
366
|
def resolve(coord: str) -> str | None:
|
|
367
|
+
"""Resolve a maven coordinate to the repo id that publishes it, or None."""
|
|
195
368
|
return _resolve_clone_path(coord, by_coord, by_name, by_full)
|
|
196
369
|
|
|
197
|
-
|
|
198
|
-
cands = _collect_candidates(records, resolve)
|
|
199
|
-
prev = _load_discovered(metabase_root / DISCOVERED_FILE)
|
|
370
|
+
return discover_api_clients_from_records(metabase_root, records, resolve)
|
|
200
371
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
]
|
|
372
|
+
|
|
373
|
+
def _write_discovered(metabase_root: Path, entries: list[dict[str, Any]]) -> None:
|
|
374
|
+
"""Persist the candidate file."""
|
|
205
375
|
out = {
|
|
206
376
|
"_comment": (
|
|
207
377
|
"CANDIDATE api-client bindings discovered by src2sink-build "
|
|
@@ -215,7 +385,6 @@ def discover_api_clients(
|
|
|
215
385
|
(metabase_root / DISCOVERED_FILE).write_text(
|
|
216
386
|
json.dumps(out, indent=2, sort_keys=False) + "\n", encoding="utf-8"
|
|
217
387
|
)
|
|
218
|
-
return len(entries)
|
|
219
388
|
|
|
220
389
|
|
|
221
390
|
def _load_bindings(path: Path) -> list[dict[str, Any]]:
|
|
@@ -34,6 +34,7 @@ def _row_from_record(data: dict[str, Any]) -> dict[str, Any]:
|
|
|
34
34
|
"http_out": families.get("http-out", 0),
|
|
35
35
|
"sql": families.get("sql", 0),
|
|
36
36
|
"raw_payload": families.get("raw-code-payload", 0),
|
|
37
|
+
"sql_payload_out": families.get("sql-payload-out", 0),
|
|
37
38
|
"pii_field": families.get("pii-field", 0),
|
|
38
39
|
"pii_log": families.get("pii-log", 0),
|
|
39
40
|
"queue": families.get("queue-pub", 0) + families.get("queue-sub", 0),
|