src2sink 3.1.0__tar.gz → 3.2.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-3.1.0/src2sink.egg-info → src2sink-3.2.0}/PKG-INFO +1 -1
- {src2sink-3.1.0 → src2sink-3.2.0}/pyproject.toml +1 -1
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink/aggregators/graphs.py +31 -21
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink/build_metabase_v2.py +188 -30
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink/constants.py +11 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink/dependencies.py +104 -29
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink/derive.py +1 -1
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink/extractors/ast_walk.py +280 -13
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink/extractors/file_context.py +4 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink/extractors/ts_extractors.py +42 -10
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink/extractors/unified.py +12 -1
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink/resolve.py +21 -4
- src2sink-3.2.0/src2sink/run_timing.py +166 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink/schema.py +31 -1
- {src2sink-3.1.0 → src2sink-3.2.0/src2sink.egg-info}/PKG-INFO +1 -1
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink.egg-info/SOURCES.txt +4 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/tests/test_cross_repo_caller_coverage.py +1 -1
- {src2sink-3.1.0 → src2sink-3.2.0}/tests/test_detection_input_coverage.py +20 -2
- src2sink-3.2.0/tests/test_language_support_matrix.py +481 -0
- src2sink-3.2.0/tests/test_oi36_parse_failures.py +241 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/tests/test_run_manifest.py +15 -3
- src2sink-3.2.0/tests/test_run_timing.py +221 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/tests/test_silent_failure_gate.py +8 -8
- {src2sink-3.1.0 → src2sink-3.2.0}/tests/test_type_declarations.py +206 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/LICENSE +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/README.md +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/setup.cfg +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink/__init__.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink/aggregators/__init__.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink/aggregators/api_client_discovery.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink/aggregators/auth_cards.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink/aggregators/crypto_cards.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink/aggregators/data_stores.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink/aggregators/fleet_pass.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink/aggregators/index_v2.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink/aggregators/library_source_map.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink/aggregators/openapi_discovery.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink/aggregators/openapi_edges.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink/aggregators/openapi_match.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink/aggregators/openapi_models.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink/aggregators/openapi_report.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink/aggregators/payload_producers.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink/aggregators/phase3.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink/aggregators/pii_cross_repo.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink/aggregators/pii_flow_v2.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink/aggregators/pii_lifecycle.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink/aggregators/pii_lifecycle_report.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink/aggregators/pii_touchpoint_collect.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink/aggregators/pii_touchpoint_utils.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink/aggregators/queues.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink/aggregators/ropa.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink/aggregators/service_call_collect.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink/aggregators/service_call_index.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink/aggregators/service_call_models.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink/aggregators/service_call_report.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink/aggregators/service_calls.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink/aggregators/taint_buckets.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink/aggregators/taint_catalogs.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink/aggregators/taint_writers.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink/aggregators/traces_index.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink/checkout_scan.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink/curate_internal_libraries.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink/extractors/__init__.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink/extractors/base.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink/extractors/config.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink/extractors/go.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink/extractors/http_out.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink/extractors/java.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink/extractors/javascript.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink/extractors/kotlin.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink/extractors/node_factory.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink/extractors/patterns.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink/extractors/python_extractor.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink/extractors/regex_extractors.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink/extractors/symbols.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink/graph_common.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink/index_store.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink/internal_groups.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink/known_api_clients.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink/library_taint_java.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink/limits.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink/maven.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink/models/__init__.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink/models/auth_model.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink/models/crypto_agility.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink/models/pii_lifecycle.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink/models/ropa.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink/paths.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink/prescreen.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink/py.typed +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink/record_fleet_baseline.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink/renderers/__init__.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink/renderers/markdown.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink/renderers/mermaid.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink/repo_utils.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink/safe_paths.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink/sanitize.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink/trace.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink/trace_batch.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink/vocabulary.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink.egg-info/dependency_links.txt +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink.egg-info/entry_points.txt +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink.egg-info/requires.txt +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/src2sink.egg-info/top_level.txt +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/tests/test_aggregate_output_golden.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/tests/test_aggregators_extra.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/tests/test_api_client_discovery.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/tests/test_ast_walk.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/tests/test_build_internals.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/tests/test_build_main.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/tests/test_call_observations.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/tests/test_call_resolution.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/tests/test_characterization.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/tests/test_checkout_scan.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/tests/test_config_hygiene.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/tests/test_curate.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/tests/test_demand_side_discovery.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/tests/test_dependency_pinning.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/tests/test_derive_pass.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/tests/test_detection_fingerprint_gate.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/tests/test_detection_version.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/tests/test_discovery_scan_cost.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/tests/test_entry_points.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/tests/test_field_precision.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/tests/test_fleet_index.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/tests/test_fleet_pass.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/tests/test_gradle_version_catalogs.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/tests/test_graph_common.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/tests/test_http_guard_evidence.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/tests/test_import_graph.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/tests/test_import_style.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/tests/test_internal_groups.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/tests/test_limits.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/tests/test_maven_resolution.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/tests/test_method_structure.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/tests/test_module_imports.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/tests/test_oi13_kotlin_parity.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/tests/test_oi26_receiver_scope.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/tests/test_oi28_index_fast_path.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/tests/test_oi33_canonical_repo_id.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/tests/test_oi37_express_anchor.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/tests/test_oi38_trace_index_freshness.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/tests/test_oi39_test_path_classification.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/tests/test_oi40_client_repo_target.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/tests/test_oi42_promote_validation.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/tests/test_openapi.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/tests/test_output_neutralisation.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/tests/test_path_match_significance.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/tests/test_phase0_smoke.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/tests/test_phase1_config.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/tests/test_phase1_extractors.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/tests/test_phase2_graphs.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/tests/test_phase3_pii.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/tests/test_phase4_aggregators.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/tests/test_phase4_extractor_snapshots.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/tests/test_phase4_fleet_regression.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/tests/test_phase4_regression.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/tests/test_phase4_taint_caps.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/tests/test_polyglot_dependencies.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/tests/test_post_phase4_backlog.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/tests/test_prescreen.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/tests/test_producer_scan_single_pass.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/tests/test_redos_bounds.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/tests/test_renderers.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/tests/test_repo_utils_helpers.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/tests/test_sanitize.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/tests/test_security_input_handling.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/tests/test_source_map_fix.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/tests/test_sql_classifier.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/tests/test_sql_payload_out.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/tests/test_sql_sink_evidence.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/tests/test_sql_source_construction.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/tests/test_tainted_paths.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/tests/test_trace_batch.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/tests/test_trace_fleet_scaling.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/tests/test_trace_render.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/tests/test_zz_security_coverage_gate.py +0 -0
- {src2sink-3.1.0 → src2sink-3.2.0}/tests/test_zz_untrusted_interpolation_guard.py +0 -0
|
@@ -11,6 +11,7 @@ from ..graph_common import (
|
|
|
11
11
|
v2_record_paths,
|
|
12
12
|
)
|
|
13
13
|
from ..index_store import build_index
|
|
14
|
+
from .. import run_timing
|
|
14
15
|
from .data_stores import StoreCollector, write_data_store_graph
|
|
15
16
|
from .index_v2 import _row_from_record, write_index_v2
|
|
16
17
|
from .openapi_edges import write_openapi_artifacts
|
|
@@ -77,12 +78,15 @@ def aggregate_graphs_v2(
|
|
|
77
78
|
"""Run all v2 cross-repo graph writers (service, queue, data-store, PII, phase 3)."""
|
|
78
79
|
# One load serves the report and the index, and the edges it computes are
|
|
79
80
|
# handed on rather than recomputed (`OI-41`).
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
81
|
+
with run_timing.phase("shared-load"):
|
|
82
|
+
shared = load_v2_repo_records(metabase_root, json_paths=repo_jsons)
|
|
83
|
+
with run_timing.phase("service-call-graph"):
|
|
84
|
+
call_edges, _unmatched = write_service_call_graph(
|
|
85
|
+
metabase_root, repo_jsons, repos_root=repos_root, records=shared,
|
|
86
|
+
)
|
|
84
87
|
if repos_root:
|
|
85
|
-
|
|
88
|
+
with run_timing.phase("openapi"):
|
|
89
|
+
write_openapi_artifacts(metabase_root, repos_root, repo_jsons)
|
|
86
90
|
# One streamed pass drives every converted aggregator, instead of each
|
|
87
91
|
# parsing the whole metabase for itself (`OI-41`). Collectors retain their
|
|
88
92
|
# reductions, never the records — so this buys the time without the resident
|
|
@@ -90,22 +94,28 @@ def aggregate_graphs_v2(
|
|
|
90
94
|
queues, stores = QueueCollector(), StoreCollector()
|
|
91
95
|
index_rows = MapCollector(_row_from_record)
|
|
92
96
|
pii_flow = PiiFlowCollector()
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
metabase_root, repo_jsons,
|
|
100
|
-
)
|
|
97
|
+
with run_timing.phase("streamed-pass"):
|
|
98
|
+
run_fleet_pass(
|
|
99
|
+
metabase_root, (queues, stores, index_rows, pii_flow), json_paths=repo_jsons,
|
|
100
|
+
)
|
|
101
|
+
with run_timing.phase("queue-and-store-graphs"):
|
|
102
|
+
write_queue_graph(metabase_root, repo_jsons, graph=queues.result())
|
|
103
|
+
write_data_store_graph(metabase_root, repo_jsons, collected=stores.result())
|
|
104
|
+
with run_timing.phase("payload-producers"):
|
|
105
|
+
producer_indices = write_payload_producer_index(
|
|
106
|
+
metabase_root, repo_jsons, repos_root=repos_root, records=shared,
|
|
107
|
+
)
|
|
101
108
|
del shared # released before the streamed pass below
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
109
|
+
with run_timing.phase("index-and-pii"):
|
|
110
|
+
write_index_v2(metabase_root, repo_jsons, rows=index_rows.result())
|
|
111
|
+
write_pii_flow_v2(metabase_root, repo_jsons, counts=pii_flow.result())
|
|
112
|
+
write_traces_index(metabase_root)
|
|
105
113
|
if phase3:
|
|
106
|
-
|
|
114
|
+
with run_timing.phase("phase3"):
|
|
115
|
+
aggregate_phase3_v2(metabase_root, repo_jsons, edges=call_edges)
|
|
107
116
|
if fleet_index:
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
117
|
+
with run_timing.phase("fleet-index"):
|
|
118
|
+
write_fleet_index(
|
|
119
|
+
metabase_root, repo_jsons, repos_root=repos_root,
|
|
120
|
+
producer_indices=producer_indices, call_edges=call_edges,
|
|
121
|
+
)
|
|
@@ -11,13 +11,14 @@ import multiprocessing as mp
|
|
|
11
11
|
import os
|
|
12
12
|
import re
|
|
13
13
|
import sys
|
|
14
|
+
import time
|
|
14
15
|
from collections import Counter
|
|
15
16
|
from collections.abc import Iterator
|
|
16
17
|
from importlib.metadata import PackageNotFoundError, version
|
|
17
18
|
from pathlib import Path
|
|
18
19
|
from typing import Any
|
|
19
20
|
|
|
20
|
-
from . import checkout_scan, prescreen, repo_utils
|
|
21
|
+
from . import checkout_scan, prescreen, repo_utils, run_timing
|
|
21
22
|
from .aggregators.api_client_discovery import (
|
|
22
23
|
DISCOVERED_FILE,
|
|
23
24
|
discover_api_clients,
|
|
@@ -32,7 +33,14 @@ from .aggregators.library_source_map import (
|
|
|
32
33
|
from .aggregators.phase3 import aggregate_phase3_v2
|
|
33
34
|
from .aggregators.pii_flow_v2 import write_pii_flow_v2
|
|
34
35
|
from .aggregators.taint_catalogs import aggregate_taint_catalogs_v2
|
|
35
|
-
from .constants import
|
|
36
|
+
from .constants import (
|
|
37
|
+
MAX_FILE_BYTES,
|
|
38
|
+
NOTE_LANGUAGE_COVERAGE,
|
|
39
|
+
NOTE_PARSE_FAILED,
|
|
40
|
+
NOTE_UNPARSED_MANIFEST,
|
|
41
|
+
SKIP_DIRS,
|
|
42
|
+
SOURCE_EXTENSIONS,
|
|
43
|
+
)
|
|
36
44
|
from .maven import resolve_pom_dependencies
|
|
37
45
|
from .dependencies import (
|
|
38
46
|
parse_go_mod,
|
|
@@ -41,6 +49,7 @@ from .dependencies import (
|
|
|
41
49
|
unparsed_ecosystem_notes,
|
|
42
50
|
)
|
|
43
51
|
from .extractors.config import extract_from_config, is_config_path
|
|
52
|
+
from .extractors.ast_walk import coverage_gaps
|
|
44
53
|
from .extractors.unified import extract_from_file
|
|
45
54
|
from .internal_groups import (
|
|
46
55
|
add_internal_groups_arguments,
|
|
@@ -293,22 +302,36 @@ def _collect_dependencies(repo_root: Path) -> tuple[list[dict[str, str]], list[s
|
|
|
293
302
|
"in build.gradle* matched no catalog entry; dependencies may be "
|
|
294
303
|
"incomplete (looked for *.versions.toml and settings.gradle*)"
|
|
295
304
|
)
|
|
296
|
-
|
|
305
|
+
non_jvm_deps, non_jvm_notes = _collect_non_jvm_dependencies(repo_root)
|
|
306
|
+
deps.extend(non_jvm_deps)
|
|
307
|
+
notes.extend(non_jvm_notes)
|
|
297
308
|
notes.extend(unparsed_ecosystem_notes(repo_root))
|
|
298
309
|
return deps, notes
|
|
299
310
|
|
|
300
311
|
|
|
301
|
-
def _collect_non_jvm_dependencies(
|
|
302
|
-
|
|
312
|
+
def _collect_non_jvm_dependencies(
|
|
313
|
+
repo_root: Path,
|
|
314
|
+
) -> tuple[list[dict[str, str]], list[str]]:
|
|
315
|
+
"""Gather npm, Go and Python dependencies, preferring lockfiles (OI-19).
|
|
316
|
+
|
|
317
|
+
Returns notes alongside: a manifest that will not parse yields no
|
|
318
|
+
dependencies, which is the same value as a repo that declares none
|
|
319
|
+
(`OI-36`).
|
|
320
|
+
"""
|
|
303
321
|
deps: list[dict[str, str]] = []
|
|
322
|
+
notes: list[str] = []
|
|
304
323
|
for pkg in repo_root.rglob("package.json"):
|
|
305
324
|
if not is_skipped_path(pkg, repo_root):
|
|
306
|
-
|
|
325
|
+
pkg_deps, pkg_notes = parse_npm_dependencies(repo_root, pkg)
|
|
326
|
+
deps.extend(pkg_deps)
|
|
327
|
+
notes.extend(pkg_notes)
|
|
307
328
|
for gomod in repo_root.rglob("go.mod"):
|
|
308
329
|
if not is_skipped_path(gomod, repo_root):
|
|
309
330
|
deps.extend(parse_go_mod(gomod))
|
|
310
|
-
|
|
311
|
-
|
|
331
|
+
py_deps, py_notes = parse_python_dependencies(repo_root)
|
|
332
|
+
deps.extend(py_deps)
|
|
333
|
+
notes.extend(py_notes)
|
|
334
|
+
return deps, notes
|
|
312
335
|
|
|
313
336
|
|
|
314
337
|
def _record_dependencies(summary: RepoSummaryV2, deps: list[dict[str, str]]) -> None:
|
|
@@ -325,6 +348,28 @@ def _record_dependencies(summary: RepoSummaryV2, deps: list[dict[str, str]]) ->
|
|
|
325
348
|
summary.dependencies_external_count += 1
|
|
326
349
|
|
|
327
350
|
|
|
351
|
+
def _note_language_coverage(summary: RepoSummaryV2, lang_counts: Counter[str]) -> None:
|
|
352
|
+
"""Say, once per language, what this repo's scan could not extract (`OI-43`).
|
|
353
|
+
|
|
354
|
+
Per repo per language, never per file: Scala alone would otherwise put a note
|
|
355
|
+
on every Scala file in the estate, and a signal that loud stops being read —
|
|
356
|
+
the noise question that kept this out of `OI-36` phase 1.
|
|
357
|
+
|
|
358
|
+
The gaps are computed from the grammar tables, so a note cannot claim a
|
|
359
|
+
limitation that has since been fixed, and cannot stay silent about one that
|
|
360
|
+
has not. A language with full coverage produces nothing.
|
|
361
|
+
"""
|
|
362
|
+
for language in sorted(lang_counts):
|
|
363
|
+
gaps = coverage_gaps(language)
|
|
364
|
+
if not gaps:
|
|
365
|
+
continue
|
|
366
|
+
summary.notes.append(
|
|
367
|
+
f"{language}: {NOTE_LANGUAGE_COVERAGE} across {lang_counts[language]} "
|
|
368
|
+
f"file(s) — {'; '.join(gaps)}. Findings for this language are "
|
|
369
|
+
"incomplete rather than absent; see OI-43."
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
|
|
328
373
|
def _scan_repo_files(
|
|
329
374
|
repo_root: Path, repo_id: str, summary: RepoSummaryV2
|
|
330
375
|
) -> tuple[Counter[str], list[FlowNode], list[FlowEdge]]:
|
|
@@ -375,7 +420,8 @@ def _scan_repo_files(
|
|
|
375
420
|
continue
|
|
376
421
|
lang_counts[lang] += 1
|
|
377
422
|
nodes, edges = extract_from_file(
|
|
378
|
-
repo_id=repo_id, rel_path=rel, language=lang, source=text
|
|
423
|
+
repo_id=repo_id, rel_path=rel, language=lang, source=text,
|
|
424
|
+
notes=summary.notes
|
|
379
425
|
)
|
|
380
426
|
all_nodes.extend(nodes)
|
|
381
427
|
all_edges.extend(edges)
|
|
@@ -444,6 +490,7 @@ def analyse_repo_v2(repo_root: Path, group: str, name: str, path_rel: str) -> Re
|
|
|
444
490
|
lang_counts, all_nodes, all_edges = _scan_repo_files(repo_root, repo_id, summary)
|
|
445
491
|
all_nodes = prune_unresolvable_calls(all_nodes)
|
|
446
492
|
summary.language_breakdown = dict(lang_counts)
|
|
493
|
+
_note_language_coverage(summary, lang_counts)
|
|
447
494
|
if lang_counts:
|
|
448
495
|
summary.primary_language = lang_counts.most_common(1)[0][0]
|
|
449
496
|
summary.file_counts = dict(lang_counts)
|
|
@@ -686,6 +733,62 @@ def _prewalk_checkout(repos_root: Path) -> None:
|
|
|
686
733
|
)
|
|
687
734
|
|
|
688
735
|
|
|
736
|
+
def _fold_record(
|
|
737
|
+
data: dict[str, Any], unparsed: dict[str, int], gaps: Counter[str]
|
|
738
|
+
) -> None:
|
|
739
|
+
"""Fold one repo record's notes and languages into the running counts."""
|
|
740
|
+
before = unparsed["source_files"] + unparsed["manifests"]
|
|
741
|
+
for note in data.get("notes", []):
|
|
742
|
+
if NOTE_PARSE_FAILED in note:
|
|
743
|
+
unparsed["source_files"] += 1
|
|
744
|
+
elif NOTE_UNPARSED_MANIFEST in note:
|
|
745
|
+
unparsed["manifests"] += 1
|
|
746
|
+
if unparsed["source_files"] + unparsed["manifests"] > before:
|
|
747
|
+
unparsed["repos_affected"] += 1
|
|
748
|
+
for language in data.get("language_breakdown") or {}:
|
|
749
|
+
if coverage_gaps(language):
|
|
750
|
+
gaps[language] += 1
|
|
751
|
+
|
|
752
|
+
|
|
753
|
+
def _record_counts(json_paths: list[Path]) -> dict[str, Any]:
|
|
754
|
+
"""What the fleet could not read, and where resolution is limited.
|
|
755
|
+
|
|
756
|
+
Both numbers in **one pass**. The notes that feed them (`OI-36` phase 1,
|
|
757
|
+
`OI-43` step 4) are only half of each fix: a note on one repo among 746 is
|
|
758
|
+
not something anyone reads, and the count is what makes it visible on a run
|
|
759
|
+
that otherwise reports success. Reading the fleet twice to produce two
|
|
760
|
+
numbers would be `OI-41`'s defect reintroduced for the sake of tidiness.
|
|
761
|
+
|
|
762
|
+
`unparsed` counts from the notes, whose markers are shared constants so the
|
|
763
|
+
wording cannot drift out from under the counter. `resolution_gaps` is
|
|
764
|
+
recomputed from each record's `language_breakdown` instead, because deriving
|
|
765
|
+
it from the live grammar tables cannot go stale the way parsing prose can.
|
|
766
|
+
"""
|
|
767
|
+
unparsed = {
|
|
768
|
+
"source_files": 0, "manifests": 0, "repos_affected": 0, "records_unreadable": 0,
|
|
769
|
+
}
|
|
770
|
+
gaps: Counter[str] = Counter()
|
|
771
|
+
unreadable: list[str] = []
|
|
772
|
+
for jp in json_paths:
|
|
773
|
+
try:
|
|
774
|
+
data = json.loads(jp.read_text(encoding="utf-8"))
|
|
775
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
776
|
+
# A record this cannot read is a repo whose failures go uncounted, so
|
|
777
|
+
# the number would understate the problem while looking authoritative
|
|
778
|
+
# — the defect this function exists to report, in the reporter.
|
|
779
|
+
unreadable.append(f"{jp.name}: {type(exc).__name__}")
|
|
780
|
+
continue
|
|
781
|
+
_fold_record(data, unparsed, gaps)
|
|
782
|
+
unparsed["records_unreadable"] = len(unreadable)
|
|
783
|
+
if unreadable:
|
|
784
|
+
print(
|
|
785
|
+
f" ⚠ {len(unreadable)} repo record(s) could not be read while "
|
|
786
|
+
"counting parse failures; the unparsed counts below are a lower bound",
|
|
787
|
+
file=sys.stderr,
|
|
788
|
+
)
|
|
789
|
+
return {"unparsed": unparsed, "resolution_gaps": dict(sorted(gaps.items()))}
|
|
790
|
+
|
|
791
|
+
|
|
689
792
|
def _write_run_manifest(
|
|
690
793
|
metabase_root: Path,
|
|
691
794
|
args: argparse.Namespace,
|
|
@@ -695,6 +798,8 @@ def _write_run_manifest(
|
|
|
695
798
|
timed_out: int,
|
|
696
799
|
started_at: str,
|
|
697
800
|
finished_at: str,
|
|
801
|
+
total_seconds: float,
|
|
802
|
+
json_paths: list[Path] | None = None,
|
|
698
803
|
) -> None:
|
|
699
804
|
"""Record run provenance for reproducibility / GDPR Art. 30 (finding R-1).
|
|
700
805
|
|
|
@@ -710,6 +815,15 @@ def _write_run_manifest(
|
|
|
710
815
|
"detection_version": DETECTION_VERSION,
|
|
711
816
|
"started_at": started_at,
|
|
712
817
|
"finished_at": finished_at,
|
|
818
|
+
# `OI-32`, 4.0 phase 0. Wall clock alone answered only "how long", and
|
|
819
|
+
# every 3.0.0 performance number had to be gathered by instrumenting a
|
|
820
|
+
# build by hand — including that aggregation was 78% of the run. Shares
|
|
821
|
+
# are of the whole run at every depth; time no phase claimed is reported
|
|
822
|
+
# as `unattributed` rather than dropped. See `src2sink/run_timing.py`.
|
|
823
|
+
"timing": {
|
|
824
|
+
"total_seconds": round(total_seconds, 2),
|
|
825
|
+
"phases": run_timing.timings(total_seconds),
|
|
826
|
+
},
|
|
713
827
|
"invocation": {
|
|
714
828
|
"repos_root": Path(args.repos_root).name,
|
|
715
829
|
"metabase_root": Path(args.metabase_root).name,
|
|
@@ -733,6 +847,13 @@ def _write_run_manifest(
|
|
|
733
847
|
"updated": len(rows),
|
|
734
848
|
"skipped": skipped,
|
|
735
849
|
"timed_out": timed_out,
|
|
850
|
+
# `OI-36` phase 1 and `OI-43` step 4. A detection path that failed
|
|
851
|
+
# to empty now records why, and a language whose resolution is
|
|
852
|
+
# limited says so — but a note buried in one repo record among 746
|
|
853
|
+
# is not something anyone reads. These are the fleet-level numbers:
|
|
854
|
+
# a run that reports success while a tenth of the estate would not
|
|
855
|
+
# parse should say so in the same breath.
|
|
856
|
+
**_record_counts(json_paths or []),
|
|
736
857
|
},
|
|
737
858
|
"updated_repos": sorted(
|
|
738
859
|
(
|
|
@@ -923,6 +1044,35 @@ def _generate_source_map(
|
|
|
923
1044
|
print(f"{prefix}resolved {fixed} flagged source-map entries from pom.xml")
|
|
924
1045
|
|
|
925
1046
|
|
|
1047
|
+
def _aggregate_all(
|
|
1048
|
+
metabase_root: Path,
|
|
1049
|
+
jsons: list[Path],
|
|
1050
|
+
repos_root: Path,
|
|
1051
|
+
args: argparse.Namespace,
|
|
1052
|
+
*,
|
|
1053
|
+
taint: bool = True,
|
|
1054
|
+
phase3: bool = True,
|
|
1055
|
+
prefix: str = "",
|
|
1056
|
+
) -> None:
|
|
1057
|
+
"""Run every aggregation step over the written records, timing each.
|
|
1058
|
+
|
|
1059
|
+
Shared by the full scan and `--aggregate-only` so both report the same
|
|
1060
|
+
breakdown — the aggregate-only path is how you would measure aggregation
|
|
1061
|
+
deliberately, and it would be perverse for that one to be the untimed one.
|
|
1062
|
+
"""
|
|
1063
|
+
# Before any phase searches the tree, so they all share one traversal.
|
|
1064
|
+
with run_timing.phase("prewalk-checkout"):
|
|
1065
|
+
_prewalk_checkout(repos_root)
|
|
1066
|
+
if taint:
|
|
1067
|
+
with run_timing.phase("taint-catalogs"):
|
|
1068
|
+
aggregate_taint_catalogs_v2(metabase_root, jsons)
|
|
1069
|
+
aggregate_graphs_v2(metabase_root, jsons, repos_root=repos_root, phase3=phase3)
|
|
1070
|
+
with run_timing.phase("source-map"):
|
|
1071
|
+
_generate_source_map(metabase_root, jsons, repos_root, prefix=prefix)
|
|
1072
|
+
with run_timing.phase("api-client-discovery"):
|
|
1073
|
+
_maybe_discover_api_clients(metabase_root, jsons, repos_root, args)
|
|
1074
|
+
|
|
1075
|
+
|
|
926
1076
|
def _run_aggregate_only(
|
|
927
1077
|
metabase_root: Path, repos_root: Path, args: argparse.Namespace
|
|
928
1078
|
) -> int:
|
|
@@ -936,20 +1086,23 @@ def _run_aggregate_only(
|
|
|
936
1086
|
)
|
|
937
1087
|
return 2
|
|
938
1088
|
print(f"Aggregate-only: {len(jsons)} v2 JSONs")
|
|
1089
|
+
run_timing.reset()
|
|
1090
|
+
started = time.perf_counter()
|
|
939
1091
|
if args.phase3_only:
|
|
940
|
-
|
|
941
|
-
|
|
1092
|
+
with run_timing.phase("pii-flow"):
|
|
1093
|
+
write_pii_flow_v2(metabase_root, jsons)
|
|
1094
|
+
with run_timing.phase("phase3"):
|
|
1095
|
+
aggregate_phase3_v2(metabase_root, jsons)
|
|
942
1096
|
else:
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
aggregate_taint_catalogs_v2(metabase_root, jsons)
|
|
947
|
-
aggregate_graphs_v2(
|
|
948
|
-
metabase_root, jsons, repos_root=repos_root, phase3=not args.no_phase3
|
|
1097
|
+
_aggregate_all(
|
|
1098
|
+
metabase_root, jsons, repos_root, args,
|
|
1099
|
+
taint=not args.graphs_only, phase3=not args.no_phase3, prefix=" ",
|
|
949
1100
|
)
|
|
950
|
-
_generate_source_map(metabase_root, jsons, repos_root, prefix=" ")
|
|
951
|
-
_maybe_discover_api_clients(metabase_root, jsons, repos_root, args)
|
|
952
1101
|
print("Done.")
|
|
1102
|
+
# No manifest here: this mode re-renders artefacts without scanning, so
|
|
1103
|
+
# writing `run-manifest.json` would replace a full run's provenance with a
|
|
1104
|
+
# partial one that looks like a scan which updated nothing.
|
|
1105
|
+
print("\n".join(run_timing.render_lines(time.perf_counter() - started)))
|
|
953
1106
|
return 0
|
|
954
1107
|
|
|
955
1108
|
|
|
@@ -1084,8 +1237,11 @@ def main() -> int:
|
|
|
1084
1237
|
return _run_aggregate_only(metabase_root, repos_root, args)
|
|
1085
1238
|
|
|
1086
1239
|
started_at = dt.datetime.now(dt.timezone.utc).isoformat()
|
|
1240
|
+
run_timing.reset()
|
|
1241
|
+
run_started = time.perf_counter()
|
|
1087
1242
|
force = args.force
|
|
1088
|
-
|
|
1243
|
+
with run_timing.phase("discovery"):
|
|
1244
|
+
discovered = _discover_repos(repos_root, metabase_root, args, force)
|
|
1089
1245
|
|
|
1090
1246
|
# Load pre-screen indicators once in the parent; pass the list to workers.
|
|
1091
1247
|
prescreen_indicators: tuple[str, ...] = ()
|
|
@@ -1100,18 +1256,17 @@ def main() -> int:
|
|
|
1100
1256
|
f"v2: {len(discovered)} repos, workers={args.workers}, "
|
|
1101
1257
|
f"repo-timeout={args.repo_timeout}s{' (force)' if force else ''}"
|
|
1102
1258
|
)
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1259
|
+
with run_timing.phase("extraction"):
|
|
1260
|
+
rows, skipped, timed_out = _dispatch_and_collect(
|
|
1261
|
+
discovered, args, pattern_strings, prescreen_indicators
|
|
1262
|
+
)
|
|
1106
1263
|
|
|
1107
|
-
|
|
1264
|
+
with run_timing.phase("load-records"):
|
|
1265
|
+
jsons = _load_v2_jsons(metabase_root)
|
|
1108
1266
|
if jsons:
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
aggregate_graphs_v2(metabase_root, jsons, repos_root=repos_root)
|
|
1113
|
-
_generate_source_map(metabase_root, jsons, repos_root, prefix="")
|
|
1114
|
-
_maybe_discover_api_clients(metabase_root, jsons, repos_root, args)
|
|
1267
|
+
with run_timing.phase("aggregation"):
|
|
1268
|
+
_aggregate_all(metabase_root, jsons, repos_root, args)
|
|
1269
|
+
total_seconds = time.perf_counter() - run_started
|
|
1115
1270
|
_write_run_manifest(
|
|
1116
1271
|
metabase_root,
|
|
1117
1272
|
args,
|
|
@@ -1120,12 +1275,15 @@ def main() -> int:
|
|
|
1120
1275
|
timed_out=timed_out,
|
|
1121
1276
|
started_at=started_at,
|
|
1122
1277
|
finished_at=dt.datetime.now(dt.timezone.utc).isoformat(),
|
|
1278
|
+
total_seconds=total_seconds,
|
|
1279
|
+
json_paths=jsons,
|
|
1123
1280
|
)
|
|
1124
1281
|
print(
|
|
1125
1282
|
f"Finished {len(rows)} updated, {skipped} skipped, {timed_out} timed out, "
|
|
1126
1283
|
f"{len(jsons)} v2 JSONs for aggregation."
|
|
1127
1284
|
f"{_limits_hit_summary(jsons, timed_out)}"
|
|
1128
1285
|
)
|
|
1286
|
+
print("\n".join(run_timing.render_lines(total_seconds)))
|
|
1129
1287
|
return 0
|
|
1130
1288
|
|
|
1131
1289
|
|
|
@@ -25,6 +25,17 @@ CONFIG_FILE_NAMES = {
|
|
|
25
25
|
"bootstrap.yml", "bootstrap.yaml", "bootstrap.properties",
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
# Stable markers in a repo's `notes`, so the run manifest can count what could
|
|
29
|
+
# not be parsed without re-deriving anyone's wording (`OI-36`, 4.0 phase 1). A
|
|
30
|
+
# note whose text drifts silently stops being counted, and the run then reports
|
|
31
|
+
# fewer failures than happened — which is this issue arriving through its own
|
|
32
|
+
# fix. Producers and `_unparsed_counts` share these two strings.
|
|
33
|
+
NOTE_PARSE_FAILED = "parse failed"
|
|
34
|
+
NOTE_UNPARSED_MANIFEST = "could not be parsed"
|
|
35
|
+
# `OI-43` step 4. Marks a per-repo statement about what a language could
|
|
36
|
+
# contribute, as distinct from a file that broke.
|
|
37
|
+
NOTE_LANGUAGE_COVERAGE = "limited extraction"
|
|
38
|
+
|
|
28
39
|
MAX_FILE_BYTES = 1_500_000
|
|
29
40
|
|
|
30
41
|
# Paths whose contents are not the deployed service. Matched against whole path
|