cg-code-graph 0.20.0__tar.gz → 0.21.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.
- {cg_code_graph-0.20.0/cg_code_graph.egg-info → cg_code_graph-0.21.0}/PKG-INFO +2 -1
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/README.md +1 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/__init__.py +1 -1
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/blindspots.py +107 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/cli.py +49 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/config.py +42 -2
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/core/model.py +1 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/core/plugin.py +3 -0
- cg_code_graph-0.21.0/cg_code_graph/core/redact.py +158 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/coverage.py +49 -14
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/external.py +18 -4
- cg_code_graph-0.21.0/cg_code_graph/guard_facts.py +440 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/hooks.py +2 -1
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/indexer.py +33 -2
- cg_code_graph-0.21.0/cg_code_graph/insecure_transport.py +513 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/link.py +96 -2
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/local_sockets.py +4 -2
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/mcp_server.py +30 -1
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/payload.py +5 -3
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/django/plugin.py +16 -1
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/express/plugin.py +13 -1
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/laravel/plugin.py +4 -2
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/laravel/values.py +86 -24
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/php/inline_guards.py +19 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/ts/extractor/extract.mjs +222 -10
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/ts/extractor/fw.mjs +18 -1
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/ts/plugin.py +4 -2
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/presets/express.yaml +6 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/query.py +8 -2
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/routes.py +87 -3
- cg_code_graph-0.21.0/cg_code_graph/sdk_systems.py +2701 -0
- cg_code_graph-0.21.0/cg_code_graph/ssrf_input.py +854 -0
- cg_code_graph-0.21.0/cg_code_graph/surface.py +990 -0
- cg_code_graph-0.21.0/cg_code_graph/webhook_subs.py +557 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/webhooks.py +126 -11
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0/cg_code_graph.egg-info}/PKG-INFO +2 -1
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph.egg-info/SOURCES.txt +17 -0
- cg_code_graph-0.21.0/tests/test_echo_wrapper.py +99 -0
- cg_code_graph-0.21.0/tests/test_fp_batch.py +293 -0
- cg_code_graph-0.21.0/tests/test_redact_salt.py +97 -0
- cg_code_graph-0.21.0/tests/test_sdk_external_infra.py +279 -0
- cg_code_graph-0.21.0/tests/test_sdk_external_mail.py +167 -0
- cg_code_graph-0.21.0/tests/test_ssrf.py +424 -0
- cg_code_graph-0.21.0/tests/test_surface.py +703 -0
- cg_code_graph-0.21.0/tests/test_surface_transport.py +530 -0
- cg_code_graph-0.21.0/tests/test_ts_node_http.py +100 -0
- cg_code_graph-0.21.0/tests/test_unguarded.py +204 -0
- cg_code_graph-0.21.0/tests/test_webhooks_152b.py +194 -0
- cg_code_graph-0.20.0/cg_code_graph/sdk_systems.py +0 -963
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/LICENSE +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/MANIFEST.in +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/__main__.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/affected.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/agent_rules.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/agents.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/aitools.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/android_ipc.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/apple_ipc.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/apps.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/bridges.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/brokers.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/concepts.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/core/__init__.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/core/cache.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/core/detect.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/core/env.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/core/extractors.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/core/fsutil.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/core/generated.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/core/paths.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/core/store.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/core/syntax_errors.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/dart_isolates.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/doctor.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/explore.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/graphql.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/jobs.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/lint_async.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/local_ipc.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/mcp_install.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/parity.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/parity_structure.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plans.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/platform_scan.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/platforms.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/__init__.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/astro/__init__.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/astro/content.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/astro/plugin.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/cfamily/__init__.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/cfamily/plugin.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/cfamily/syntax.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/dart/__init__.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/dart/bridges.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/dart/extractor/bin/extract.dart +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/dart/extractor/pubspec.lock +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/dart/extractor/pubspec.yaml +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/dart/http.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/dart/models.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/dart/plugin.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/dart/program.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/django/__init__.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/django/extras.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/django/models.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/django/schemas.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/django/shapes.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/django/urls.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/express/__init__.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/flutter/__init__.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/flutter/plugin.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/java/__init__.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/java/exact.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/java/plugin.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/jvm/__init__.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/jvm/interop.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/jvm/spring.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/kotlin/__init__.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/kotlin/exact.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/kotlin/plugin.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/kotlin/reparse.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/laravel/__init__.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/laravel/broadcast.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/laravel/tests.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/native/__init__.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/native/gates.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/native/runner.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/native/scipread.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/native/ts.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/nest/__init__.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/nest/plugin.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/nextjs/__init__.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/nextjs/plugin.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/nuxt/__init__.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/nuxt/plugin.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/php/__init__.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/php/external.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/php/extractor/composer.json +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/php/extractor/composer.lock +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/php/extractor/extract.php +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/php/gating.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/php/http_client.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/php/plugin.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/php/strings.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/python/__init__.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/python/aitools.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/python/external.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/python/fields.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/python/plugin.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/python/refs.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/python/roots.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/python/socketio.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/python/subproc.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/python/tests.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/python/values.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/pyweb/__init__.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/pyweb/plugin.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/pyweb/values.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/rust/__init__.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/rust/cargo.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/rust/plugin.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/rust/syntax.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/scip/__init__.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/scip/importer.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/scip/scip.proto +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/scip/scip_pb2.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/stubs/__init__.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/stubs/plugins.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/swift/__init__.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/swift/baseurl.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/swift/exact.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/swift/indexstore.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/swift/packages.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/swift/plugin.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/ts/__init__.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/ts/baseurl.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/ts/extractor/package-lock.json +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/ts/extractor/package.json +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/ts/extractor/rr.mjs +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/ts/nav.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/ts/react_router.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/tsweb/__init__.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/tsweb/common.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/plugins/tsweb/data.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/presets/__init__.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/presets/c_cpp.yaml +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/presets/common.yaml +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/presets/dart.yaml +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/presets/django-ninja.yaml +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/presets/django.yaml +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/presets/djangorestframework.yaml +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/presets/java.yaml +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/presets/kotlin.yaml +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/presets/laravel.yaml +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/presets/nest.yaml +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/presets/nextjs.yaml +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/presets/nuxt.yaml +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/presets/php.yaml +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/presets/python.yaml +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/presets/react-router.yaml +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/presets/rust.yaml +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/presets/spring.yaml +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/presets/swift.yaml +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/presets/typescript.yaml +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/process_runs.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/protocols/__init__.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/protocols/builtin.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/protocols/matchers.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/protocols/view.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/realtime.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/realtime_events.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/realtime_native.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/realtime_ws.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/roundtrip.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/rpc.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/sockets.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/starters.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/tests_index.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/udp_apps.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/viz/__init__.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/viz/graph.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/viz/server.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/viz/static/app.css +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/viz/static/app.js +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/viz/static/index.html +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/viz/static/layered.js +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/viz/static/vendor/VERSIONS.txt +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/viz/static/vendor/cose-base.js +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/viz/static/vendor/cytoscape-fcose.js +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/viz/static/vendor/cytoscape.min.js +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/viz/static/vendor/layout-base.js +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/viz/tools/package-lock.json +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/viz/tools/package.json +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/viz/tools/shoot.mjs +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/watch.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph/xcode.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph.egg-info/dependency_links.txt +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph.egg-info/entry_points.txt +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph.egg-info/requires.txt +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/cg_code_graph.egg-info/top_level.txt +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/pyproject.toml +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/setup.cfg +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_affected.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_agents_install.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_agents_md.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_ai_tools.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_android_ipc.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_apple_ipc.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_astro.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_astro_content.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_astro_server.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_bridges.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_bridges_pigeon_monorepo.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_brokers.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_cache_clean.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_cache_invalidation.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_cache_migration.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_channels_tests.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_cli_output.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_completeness.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_config_extras.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_coverage.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_coverage_ts_hint.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_dart_isolates.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_dispatch_interfaces.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_django_flutter.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_docs_languages.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_docs_links.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_echo_ref.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_elysia.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_enum_constants.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_env.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_explore.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_external.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_gating.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_generated.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_graphql.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_hooks.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_impact_overrides.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_index_isolation.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_inherited_overrides.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_inline_guards.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_java.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_java_exact.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_jobs.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_js_refs.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_js_runtime.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_kotlin.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_kotlin_binding.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_kotlin_exact.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_kotlin_fields.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_kotlin_mixed.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_kotlin_nav_consts.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_kotlin_props.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_laravel_determinism.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_laravel_resources.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_lint_async.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_local_ipc.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_local_sockets.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_mcp.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_mcp_index.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_mcp_install.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_native_cfamily.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_native_concurrency.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_native_rust.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_nuxt_import_dirs.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_nuxt_rootsrc.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_packaging.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_parity.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_parity_structure.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_php_http.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_php_props.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_plain_js.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_plans.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_platform_gaps.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_platform_type_variants.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_platforms.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_platforms_apple.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_presets_config.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_process_ipc.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_process_runs.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_protocols.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_python_app_tests_module.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_python_collection_chains.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_python_compat.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_python_entry_refs.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_python_fields.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_python_infer_cycles.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_python_roots.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_python_subprocess_tests.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_python_tests.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_python_web_hosts.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_python_web_routes.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_python_web_routes_more.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_react_router.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_realtime.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_realtime_native.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_request_keys.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_roundtrip.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_route_tables.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_routes_search_explain.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_rpc.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_scip_health.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_sdk_external.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_snippet.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_sockets.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_spring.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_starters_budget.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_swift.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_swift_binding.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_swift_exact.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_swift_facts.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_swift_fields.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_swift_modules.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_swift_more.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_swift_nav_values.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_swift_parse_errors.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_swift_props.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_swift_sdk.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_swift_testing.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_swift_values.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_tests_scope.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_ts_fields.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_ts_fields_js_vue.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_ts_file_specs.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_ts_frameworks.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_ts_nuxt.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_ts_src_tsconfig.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_values.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_viz.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_vue_nav.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_watch.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_webhooks.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_webhooks_152.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_websockets.py +0 -0
- {cg_code_graph-0.20.0 → cg_code_graph-0.21.0}/tests/test_workspace_link.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cg-code-graph
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.21.0
|
|
4
4
|
Summary: cg: a code graph of routes, calls, data access and platform boundaries for agents and humans (CLI + MCP server)
|
|
5
5
|
Author: cyberchronos00
|
|
6
6
|
License-Expression: MIT
|
|
@@ -90,6 +90,7 @@ Go and others: import a SCIP index with `cg index --scip FILE`. Run `cg doctor`
|
|
|
90
90
|
- `cg affected --base main`: the tests and entry points a change reaches (`--quiet` feeds your test runner).
|
|
91
91
|
- Across repos: `cg link` matches client HTTP calls (frontends and Laravel / Guzzle backends) to server routes, and checks request keys (Laravel FormRequest, Elysia schemas, Nuxt / TypeScript bodies).
|
|
92
92
|
- Route guards: `cg routes --writes --unguarded` lists routes that write with no auth guard. Laravel checks inside the action count as inline guards unless `--strict`.
|
|
93
|
+
- Security views: `cg surface` lists the inbound / outbound attack surface and findings (hardcoded credentials, plaintext protocols, unverified webhooks, unguarded handlers, listeners on all interfaces, disabled TLS / SSH verification, plaintext gRPC, exposed IPC, outbound URLs built from request input) as text, JSON or SARIF, with `--fail-on` for CI.
|
|
93
94
|
- MCP server (`cg-mcp`) so agents query the graph instead of grepping.
|
|
94
95
|
- Visual view (`cg serve`) and plan checks (`cg plan check`) for planned changes.
|
|
95
96
|
|
|
@@ -63,6 +63,7 @@ Go and others: import a SCIP index with `cg index --scip FILE`. Run `cg doctor`
|
|
|
63
63
|
- `cg affected --base main`: the tests and entry points a change reaches (`--quiet` feeds your test runner).
|
|
64
64
|
- Across repos: `cg link` matches client HTTP calls (frontends and Laravel / Guzzle backends) to server routes, and checks request keys (Laravel FormRequest, Elysia schemas, Nuxt / TypeScript bodies).
|
|
65
65
|
- Route guards: `cg routes --writes --unguarded` lists routes that write with no auth guard. Laravel checks inside the action count as inline guards unless `--strict`.
|
|
66
|
+
- Security views: `cg surface` lists the inbound / outbound attack surface and findings (hardcoded credentials, plaintext protocols, unverified webhooks, unguarded handlers, listeners on all interfaces, disabled TLS / SSH verification, plaintext gRPC, exposed IPC, outbound URLs built from request input) as text, JSON or SARIF, with `--fail-on` for CI.
|
|
66
67
|
- MCP server (`cg-mcp`) so agents query the graph instead of grepping.
|
|
67
68
|
- Visual view (`cg serve`) and plan checks (`cg plan check`) for planned changes.
|
|
68
69
|
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""code-graph: deterministic code knowledge graph (beta)."""
|
|
2
|
-
__version__ = "0.
|
|
2
|
+
__version__ = "0.21.0"
|
|
@@ -35,6 +35,8 @@ KINDS = {
|
|
|
35
35
|
"Nuxt imports.dirs entry that is not a literal path or glob"),
|
|
36
36
|
"vue_unresolved_navigation": ("route", "typescript",
|
|
37
37
|
"Nuxt / Vue / Astro navigation target that matches no page or several pages"),
|
|
38
|
+
"ts_unrecognised_http_client": ("endpoint", "typescript",
|
|
39
|
+
"outbound HTTP call through an imported client (http, https, undici, got, node-fetch, superagent, ...) that made no endpoint"),
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
|
|
@@ -415,6 +417,110 @@ def nuxt_unevaluable_import_dirs(root: Path) -> dict | None:
|
|
|
415
417
|
return _finding("nuxt_unevaluable_import_dirs", hits)
|
|
416
418
|
|
|
417
419
|
|
|
420
|
+
# ------------------------------------------------------------------------------------------- TS HTTP clients
|
|
421
|
+
|
|
422
|
+
HTTP_CLIENT_MODS = ("http", "https", "undici", "got", "node-fetch", "superagent", "needle", "phin", "request")
|
|
423
|
+
_HTTP_VERBS = {"request", "get", "post", "put", "patch", "delete", "del", "head", "options", "fetch", "stream"}
|
|
424
|
+
_HTTP_ONLY = {"request", "get"} # node:http / node:https: createServer, Agent, ... are not client calls
|
|
425
|
+
_HTTP_MOD_ALT = "|".join(re.escape(m) for m in HTTP_CLIENT_MODS)
|
|
426
|
+
_IMPORT_DEFAULT = re.compile(r"""import\s+(?:type\s+)?(\w+)\s*(?:,\s*\{([^}]*)\}\s*)?from\s*['"](?:node:)?(%s)['"]""" % _HTTP_MOD_ALT)
|
|
427
|
+
_IMPORT_NS = re.compile(r"""import\s+\*\s+as\s+(\w+)\s+from\s*['"](?:node:)?(%s)['"]""" % _HTTP_MOD_ALT)
|
|
428
|
+
_IMPORT_NAMED = re.compile(r"""import\s+(?:type\s+)?\{([^}]*)\}\s*from\s*['"](?:node:)?(%s)['"]""" % _HTTP_MOD_ALT)
|
|
429
|
+
_REQ_BIND = re.compile(r"""(?:const|let|var)\s+(\w+)\s*=\s*(?:await\s+)?require\(\s*['"](?:node:)?(%s)['"]\s*\)""" % _HTTP_MOD_ALT)
|
|
430
|
+
_REQ_DESTR = re.compile(r"""(?:const|let|var)\s+\{([^}]*)\}\s*=\s*require\(\s*['"](?:node:)?(%s)['"]\s*\)""" % _HTTP_MOD_ALT)
|
|
431
|
+
_TEST_FILE = re.compile(r"(^|/)(tests?|__tests__|e2e|spec|specs|fixtures?)/|\.(test|spec)\.[cm]?[jt]sx?$")
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def _named(spec: str) -> list[tuple[str, str]]:
|
|
435
|
+
out = []
|
|
436
|
+
for part in spec.split(","):
|
|
437
|
+
bits = [b for b in re.split(r"\s+as\s+|\s+", part.replace("type ", "").strip()) if b]
|
|
438
|
+
if bits:
|
|
439
|
+
out.append((bits[0], bits[-1]))
|
|
440
|
+
return out
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def _http_client_calls(src: str) -> int | None:
|
|
444
|
+
"""Offset of the first call through an imported HTTP client module in `src`, else None."""
|
|
445
|
+
binds: dict[str, str] = {} # name -> module (the module object itself)
|
|
446
|
+
named: dict[str, str] = {} # local name -> module (an imported client function)
|
|
447
|
+
for m in _IMPORT_DEFAULT.finditer(src):
|
|
448
|
+
binds[m.group(1)] = m.group(3)
|
|
449
|
+
for imp, loc in _named(m.group(2) or ""):
|
|
450
|
+
named[loc] = f"{m.group(3)}:{imp}"
|
|
451
|
+
for m in _IMPORT_NS.finditer(src):
|
|
452
|
+
binds[m.group(1)] = m.group(2)
|
|
453
|
+
for m in _IMPORT_NAMED.finditer(src):
|
|
454
|
+
for imp, loc in _named(m.group(1)):
|
|
455
|
+
named[loc] = f"{m.group(2)}:{imp}"
|
|
456
|
+
for m in _REQ_BIND.finditer(src):
|
|
457
|
+
binds[m.group(1)] = m.group(2)
|
|
458
|
+
for m in _REQ_DESTR.finditer(src):
|
|
459
|
+
for imp, loc in _named(m.group(1).replace(":", " as ")):
|
|
460
|
+
named[loc] = f"{m.group(2)}:{imp}"
|
|
461
|
+
best = None
|
|
462
|
+
|
|
463
|
+
def hit(pos):
|
|
464
|
+
nonlocal best
|
|
465
|
+
if best is None or pos < best:
|
|
466
|
+
best = pos
|
|
467
|
+
|
|
468
|
+
for name, mod in binds.items():
|
|
469
|
+
verbs = _HTTP_ONLY if mod in ("http", "https") else _HTTP_VERBS
|
|
470
|
+
if mod not in ("http", "https"):
|
|
471
|
+
m = re.search(r"(?<![\w.$])%s\s*\(" % re.escape(name), src) # got(url), request(opts), needle(...)
|
|
472
|
+
if m:
|
|
473
|
+
hit(m.start())
|
|
474
|
+
m = re.search(r"(?<![\w.$])%s\s*\.\s*(%s)\s*\(" % (re.escape(name), "|".join(sorted(verbs))), src)
|
|
475
|
+
if m:
|
|
476
|
+
hit(m.start())
|
|
477
|
+
for name, spec in named.items():
|
|
478
|
+
mod, _, imp = spec.partition(":")
|
|
479
|
+
ok = imp in (_HTTP_ONLY if mod in ("http", "https") else _HTTP_VERBS | {"Client", "Pool"})
|
|
480
|
+
if not ok:
|
|
481
|
+
continue
|
|
482
|
+
m = re.search(r"(?<![\w.$])(?:new\s+)?%s\s*\(" % re.escape(name), src)
|
|
483
|
+
if m:
|
|
484
|
+
hit(m.start())
|
|
485
|
+
return best
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
def _opaque_origin(origin: str | None) -> bool:
|
|
489
|
+
"""An origin no host can be read from: a bare placeholder that is not a configured / env value, or a templated host."""
|
|
490
|
+
o = origin or ""
|
|
491
|
+
if not o:
|
|
492
|
+
return False
|
|
493
|
+
if o.startswith("{"):
|
|
494
|
+
return not o.startswith(("{env.", "{runtimeConfig."))
|
|
495
|
+
return "://" in o and "{" in o.split("://", 1)[1].split("/", 1)[0]
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def ts_unrecognised_http_client(root: Path, files: list[str], builder) -> dict | None:
|
|
499
|
+
"""Files that import an HTTP client module (http, https, undici, got, node-fetch, superagent, needle, phin, request)
|
|
500
|
+
and call it, with no client endpoint made there (a call cg could not follow, or only an origin it could not read)."""
|
|
501
|
+
if builder is None:
|
|
502
|
+
return None
|
|
503
|
+
made: dict[str, bool] = {}
|
|
504
|
+
for e in builder.edges.values():
|
|
505
|
+
if e.kind != "HTTP_CALLS" or not e.file:
|
|
506
|
+
continue
|
|
507
|
+
n = builder.nodes.get(e.dst)
|
|
508
|
+
if n is not None and not _opaque_origin((n.attrs or {}).get("origin")):
|
|
509
|
+
made[e.file] = True
|
|
510
|
+
hits = []
|
|
511
|
+
for f in files:
|
|
512
|
+
if made.get(f) or _TEST_FILE.search(f):
|
|
513
|
+
continue
|
|
514
|
+
src = _read(root, f)
|
|
515
|
+
if not src or not re.search(r"(?:from|require\s*\()\s*['\"](?:node:)?(?:%s)['\"]" % _HTTP_MOD_ALT, src):
|
|
516
|
+
continue
|
|
517
|
+
src = _strip_comments(src)
|
|
518
|
+
pos = _http_client_calls(src)
|
|
519
|
+
if pos is not None:
|
|
520
|
+
hits.append((f, _line(src, pos)))
|
|
521
|
+
return _finding("ts_unrecognised_http_client", hits)
|
|
522
|
+
|
|
523
|
+
|
|
418
524
|
# ------------------------------------------------------------------------------------------- driver
|
|
419
525
|
|
|
420
526
|
def detect(root: str | Path, files_by_ext: dict[str, list[str]], builder=None, programs: dict | None = None) -> list[dict]:
|
|
@@ -442,6 +548,7 @@ def detect(root: str | Path, files_by_ext: dict[str, list[str]], builder=None, p
|
|
|
442
548
|
run(express_loop_routes, root, js)
|
|
443
549
|
run(nuxt_unevaluable_import_dirs, root)
|
|
444
550
|
run(vue_unresolved_navigation, builder)
|
|
551
|
+
run(ts_unrecognised_http_client, root, js, builder)
|
|
445
552
|
run(laravel_loop_routes, root, php)
|
|
446
553
|
py = programs.get("python")
|
|
447
554
|
if py is not None:
|
|
@@ -207,6 +207,23 @@ def main(argv=None):
|
|
|
207
207
|
p.add_argument("--source", help="address or credential source: literal, env, env-example, compose, config")
|
|
208
208
|
p.add_argument("--tls-off", action="store_true", help="only systems known to be reached without TLS")
|
|
209
209
|
p.add_argument("--max-items", type=int, default=60)
|
|
210
|
+
p = sub.add_parser("surface", help="security views over recorded facts: inbound / outbound attack surface and findings "
|
|
211
|
+
"(hardcoded credentials, plaintext protocols, disabled TLS / SSH verification, unverified "
|
|
212
|
+
"webhooks, unguarded handlers, listeners on all interfaces, exposed IPC); text, JSON or "
|
|
213
|
+
"SARIF; --fail-on for CI gates")
|
|
214
|
+
p.add_argument("--db", required=True)
|
|
215
|
+
p.add_argument("--inbound", action="store_true", help="list the inbound surface (routes, webhooks, sockets, consumers, IPC)")
|
|
216
|
+
p.add_argument("--outbound", action="store_true", help="list the outbound surface (every external system)")
|
|
217
|
+
p.add_argument("--protocol", help="only this protocol (http, ws, grpc, tcp, redis, postgres, saas, ...)")
|
|
218
|
+
from .surface import finding_names
|
|
219
|
+
p.add_argument("--finding", help="only this finding: " + ", ".join(finding_names()))
|
|
220
|
+
p.add_argument("--min-confidence", choices=["heuristic", "resolved", "exact"], default="heuristic",
|
|
221
|
+
help="drop findings and items below this confidence (default: heuristic, keep all)")
|
|
222
|
+
p.add_argument("--format", choices=["text", "json", "sarif"], default="text")
|
|
223
|
+
p.add_argument("--fail-on", help="comma-separated finding types; exit 1 when any remains after ignores")
|
|
224
|
+
p.add_argument("--max-items", type=int, default=200)
|
|
225
|
+
p.add_argument("--strict", action="store_true", help="unguarded: count route guards only (an inline check does not protect)")
|
|
226
|
+
p.add_argument("--show-ignored", action="store_true", help="also list findings accepted by .cg.yaml surface.ignore")
|
|
210
227
|
p = sub.add_parser("tools", help="LLM tools and MCP tools / resources / prompts: handler, tables it reaches, agents "
|
|
211
228
|
"offering it, callers, checks; agents, dynamic dispatch, model calls")
|
|
212
229
|
p.add_argument("pattern", nargs="?", help="tool name, substring or glob")
|
|
@@ -569,7 +586,14 @@ def main(argv=None):
|
|
|
569
586
|
res = external(st, a.pattern, protocol=a.protocol, source=a.source, tls_off=a.tls_off,
|
|
570
587
|
max_items=max(a.max_items, 200) if a.json else a.max_items)
|
|
571
588
|
print(json.dumps(res, indent=1, default=str) if a.json else render_external(res, max_items=a.max_items))
|
|
589
|
+
if not a.json:
|
|
590
|
+
from .coverage import endpoint_note
|
|
591
|
+
n = endpoint_note(st)
|
|
592
|
+
if n:
|
|
593
|
+
print(n)
|
|
572
594
|
return
|
|
595
|
+
if a.cmd == "surface":
|
|
596
|
+
return surface_cmd(a, st)
|
|
573
597
|
if a.cmd == "tools":
|
|
574
598
|
from .aitools import render_tools, tools
|
|
575
599
|
res = tools(st, a.pattern, framework=a.framework, unmatched=a.unmatched, agent=a.agent,
|
|
@@ -721,6 +745,11 @@ def main(argv=None):
|
|
|
721
745
|
elif a.cmd == "api-calls":
|
|
722
746
|
rows = Q.api_calls(st, a.spec)
|
|
723
747
|
print(json.dumps(rows, indent=1, default=str) if a.json else Q.render_api_calls(rows))
|
|
748
|
+
if not a.json:
|
|
749
|
+
from .coverage import endpoint_note
|
|
750
|
+
n = endpoint_note(st)
|
|
751
|
+
if n:
|
|
752
|
+
print(n)
|
|
724
753
|
elif a.cmd == "stats":
|
|
725
754
|
doc = stats_doc(st)
|
|
726
755
|
print(json.dumps(doc, indent=1, default=str) if a.json else render_stats(st, doc))
|
|
@@ -824,6 +853,26 @@ def _note(comp: dict, as_json: bool) -> None:
|
|
|
824
853
|
print(n)
|
|
825
854
|
|
|
826
855
|
|
|
856
|
+
def surface_cmd(a, st) -> int:
|
|
857
|
+
"""`cg surface`: exit 0, or 1 when a --fail-on finding remains after ignores; 2 for a bad finding name."""
|
|
858
|
+
from . import surface as S
|
|
859
|
+
fail_on = [x.strip() for x in a.fail_on.split(",") if x.strip()] if a.fail_on else []
|
|
860
|
+
try:
|
|
861
|
+
res = S.surface(st, inbound=a.inbound, outbound=a.outbound, protocol=a.protocol, finding=a.finding,
|
|
862
|
+
min_confidence=a.min_confidence, fail_on=fail_on, max_items=a.max_items, strict=a.strict,
|
|
863
|
+
show_ignored=a.show_ignored)
|
|
864
|
+
except ValueError as ex:
|
|
865
|
+
print(f"cg surface: {ex}", file=sys.stderr)
|
|
866
|
+
return 2
|
|
867
|
+
if a.format == "json":
|
|
868
|
+
print(S.to_json(res))
|
|
869
|
+
elif a.format == "sarif":
|
|
870
|
+
print(json.dumps(S.to_sarif(res), indent=1))
|
|
871
|
+
else:
|
|
872
|
+
print(S.render_surface(res, max_items=a.max_items))
|
|
873
|
+
return 1 if res["failed"] else 0
|
|
874
|
+
|
|
875
|
+
|
|
827
876
|
def config_cmd(a) -> int:
|
|
828
877
|
from .config import ConfigError, effective, find, load_file, render_effective, unknown_key_warnings
|
|
829
878
|
if a.action == "validate":
|
|
@@ -26,6 +26,11 @@ Records project-specific knowledge once, for the CLI, the MCP server and the vis
|
|
|
26
26
|
protocols:
|
|
27
27
|
external: ["kafka:audit.*", "http:GET /status"] # <protocol>:<name glob> handled outside the analysed repos
|
|
28
28
|
# (not reported as no_receiver / no_sender by cg protocols)
|
|
29
|
+
surface:
|
|
30
|
+
ignore: # accepted risks of `cg surface` (reason is required)
|
|
31
|
+
- {finding: unguarded, path: "app/Http/Controllers/HealthController.php", reason: public health probe}
|
|
32
|
+
- {finding: plaintext, id: "external:redis:cache:6379", reason: compose-internal network}
|
|
33
|
+
- {finding: hardcoded, fingerprint: 3f9a1c0b7d2e4a55, reason: rotated test key}
|
|
29
34
|
gates: config/gates.json # gate scenarios file (cg index --gates)
|
|
30
35
|
plans:
|
|
31
36
|
dir: docs/plans # plans directory (--plans-dir)
|
|
@@ -53,7 +58,7 @@ SCHEMA: dict[str, set | None] = { # top-level key -> allowed sub-keys (None:
|
|
|
53
58
|
"frameworks": {"add", "remove"}, "auth": {"extra_patterns"}, "secret": {"extra_patterns"}, "gates": None,
|
|
54
59
|
"plans": {"dir", "text_mention_dirs"}, "viz": {"presets"}, "generated": {"paths", "vendored", "keep", "include"},
|
|
55
60
|
"platforms": {"targets", "paths", "file_suffixes", "path_conventions"}, "include": None, "apps": None,
|
|
56
|
-
"rust": {"targets"}, "protocols": {"external"}, "lossy": None,
|
|
61
|
+
"rust": {"targets"}, "protocols": {"external"}, "lossy": None, "surface": {"ignore"},
|
|
57
62
|
}
|
|
58
63
|
APP_KEYS = ("name", "root", "role", "links")
|
|
59
64
|
APP_ROLES = ("backend", "frontend")
|
|
@@ -298,6 +303,9 @@ def parse(data: Any, fname: str = ".cg.yaml") -> dict:
|
|
|
298
303
|
if bad:
|
|
299
304
|
raise ConfigError(f"{fname}: protocols.external: {bad[0]!r} is not <protocol>:<name glob> (e.g. kafka:audit.*)")
|
|
300
305
|
out["protocols"] = {"external": ext}
|
|
306
|
+
sf = _section(data, "surface", fname)
|
|
307
|
+
if sf is not None and sf.get("ignore") is not None:
|
|
308
|
+
out["surface"] = {"ignore": _surface_ignores(sf["ignore"], f"{fname}: surface.ignore")}
|
|
301
309
|
if data.get("gates") is not None:
|
|
302
310
|
out["gates"] = norm_root(data["gates"], f"{fname}: gates")
|
|
303
311
|
pl = _section(data, "plans", fname)
|
|
@@ -319,6 +327,38 @@ def parse(data: Any, fname: str = ".cg.yaml") -> dict:
|
|
|
319
327
|
return out
|
|
320
328
|
|
|
321
329
|
|
|
330
|
+
SURFACE_IGNORE_KEYS = ("finding", "path", "id", "fingerprint", "reason")
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def _surface_ignores(v: Any, where: str) -> list[dict]:
|
|
334
|
+
"""`surface.ignore`: [{finding, path | id | fingerprint, reason}]; the reason is required."""
|
|
335
|
+
from .surface import FINDINGS
|
|
336
|
+
if not isinstance(v, list):
|
|
337
|
+
raise ConfigError(f"{where}: expected a list of {{finding, path | id | fingerprint, reason}} entries")
|
|
338
|
+
out = []
|
|
339
|
+
for i, e in enumerate(v):
|
|
340
|
+
w = f"{where}[{i}]"
|
|
341
|
+
if not isinstance(e, dict):
|
|
342
|
+
raise ConfigError(f"{w}: expected a mapping with finding, path | id | fingerprint and reason")
|
|
343
|
+
bad = sorted(str(k) for k in set(e) - set(SURFACE_IGNORE_KEYS))
|
|
344
|
+
if bad:
|
|
345
|
+
raise ConfigError(f"{w}: unknown key {bad[0]!r} (allowed: {', '.join(SURFACE_IGNORE_KEYS)})")
|
|
346
|
+
f = e.get("finding")
|
|
347
|
+
if f not in FINDINGS:
|
|
348
|
+
raise ConfigError(f"{w}: finding {f!r} is not one of {', '.join(FINDINGS)}")
|
|
349
|
+
reason = e.get("reason")
|
|
350
|
+
if not isinstance(reason, str) or not reason.strip():
|
|
351
|
+
raise ConfigError(f"{w}: reason is required (say why the risk is accepted)")
|
|
352
|
+
sel = {k: e[k] for k in ("path", "id", "fingerprint") if e.get(k) is not None}
|
|
353
|
+
if not sel:
|
|
354
|
+
raise ConfigError(f"{w}: give at least one of path, id or fingerprint")
|
|
355
|
+
for k, x in sel.items():
|
|
356
|
+
if not isinstance(x, str) or not x.strip():
|
|
357
|
+
raise ConfigError(f"{w}: {k} must be a non-empty string")
|
|
358
|
+
out.append({"finding": f, **{k: x.strip() for k, x in sel.items()}, "reason": reason.strip()})
|
|
359
|
+
return out
|
|
360
|
+
|
|
361
|
+
|
|
322
362
|
def _apps(v: Any, fname: str) -> list[dict]:
|
|
323
363
|
"""apps as a list of {name, root, role, links} (or a mapping name -> {root, role, links}), validated: unique names,
|
|
324
364
|
role backend / frontend, links naming backend apps. `root` may be relative, `../other-repo` or absolute.
|
|
@@ -516,7 +556,7 @@ def effective(root: str | Path, python_roots: list[str] | None = None, gates: st
|
|
|
516
556
|
row(f"platforms.{k}", pc.get(k, True), f"{fname} platforms.{k}" if k in pc else f"built-in ({what})")
|
|
517
557
|
row("auth.token_pattern", PR.values("common", "auth", "token_pattern"), "preset common")
|
|
518
558
|
for section in ("auth", "secret"):
|
|
519
|
-
for key in ("guards", "not_auth") if section == "auth" else ("guards",):
|
|
559
|
+
for key in ("guards", "not_auth", "session_only") if section == "auth" else ("guards",):
|
|
520
560
|
for p in plan["presets"]:
|
|
521
561
|
g = PR.values(p, section, key, default=None)
|
|
522
562
|
if g:
|
|
@@ -72,6 +72,7 @@ EDGE_KINDS: dict[str, tuple[bool, str]] = {
|
|
|
72
72
|
"READS_PROP": (True, "code reads a stored property (field node: Swift `var x`, `@State`, `@Published`)"),
|
|
73
73
|
"WRITES_PROP": (True, "code writes a stored property (assignment, compound assignment)"),
|
|
74
74
|
"USES_VALUE": (True, "code refers to an enum case, a constant, a static/global variable or an object-like macro"),
|
|
75
|
+
"REGISTERS_CALLBACK": (False, "function builds a callback URL for one of the app's own routes and registers it with an external party (attrs.body_key, endpoint)"),
|
|
75
76
|
"REFERENCES_FN": (True, "code/data takes a function as a value (callback, dispatch table, handler registration)"),
|
|
76
77
|
"USES_UNSAFE": (True, "code contains an unsafe block or is an unsafe fn (sink node unsafe:<crate>)"),
|
|
77
78
|
"GATED_BY": (True, "code compiled only under a condition: Cargo feature, cfg predicate or preprocessor macro"),
|
|
@@ -59,6 +59,9 @@ class GraphBuilder:
|
|
|
59
59
|
# Pigeon API definitions found by the Dart plugin ({Api: {kind: host | flutter, methods, file}}): bridges.py
|
|
60
60
|
# links their native implementations / native callers
|
|
61
61
|
self.pigeon_apis: dict = {}
|
|
62
|
+
# keyed-hash salt for the markers that stand in for secret values (cg_code_graph/core/redact.py); the indexer
|
|
63
|
+
# sets it from the graph DB before any plugin runs
|
|
64
|
+
self.redact_salt: str | None = None
|
|
62
65
|
|
|
63
66
|
def add_node(self, kind: str, key: str, name: str | None = None, **kw) -> str:
|
|
64
67
|
nid = node_id(kind, key)
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""Secret values never reach the graph database (#47).
|
|
2
|
+
|
|
3
|
+
A literal that belongs to a credential-looking key (password, secret, token, key, api_key, auth, private, credential,
|
|
4
|
+
passphrase, dsn, ...) is stored as `redacted:hmac:<first 8 hex of HMAC-SHA-256(graph salt, value)>`: enough to see that
|
|
5
|
+
a literal exists and to tell two values apart within one graph, not enough to recover one, and not recognisable by
|
|
6
|
+
hashing a common password. The salt is `secrets.token_hex(16)`, created on the first index of a graph DB, kept in its
|
|
7
|
+
`meta` table (`redact_salt`) and carried over by a re-index or refresh of that DB; a new DB gets a new salt, so markers
|
|
8
|
+
of different graphs never compare. Graphs written before part 2d hold `redacted:sha256:<8 hex>` markers, which
|
|
9
|
+
`is_redacted` still accepts. A password inside a URL (`scheme://user:pw@host`) gets the same marker in place of the
|
|
10
|
+
password so the host stays readable. Placeholders (`${X}`, `%(X)s`, `<x>`, `null`) and
|
|
11
|
+
values that are already markers are left alone. Applied at index time by the plugins that keep literal config values
|
|
12
|
+
(Laravel config/*.php `value` and `env()` defaults) and as a last pass over every node / edge attr before the write.
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import hashlib
|
|
17
|
+
import hmac
|
|
18
|
+
import re
|
|
19
|
+
import secrets
|
|
20
|
+
|
|
21
|
+
MARK = "redacted:hmac:"
|
|
22
|
+
LEGACY_MARK = "redacted:sha256:"
|
|
23
|
+
SECRET_WORDS = ("PASSWORD", "PASS", "PASSWD", "SECRET", "TOKEN", "SECRET_KEY", "SECRET_ACCESS_KEY", "KEY", "AUTH", "PWD")
|
|
24
|
+
_EXTRA_WORDS = {"PASSWORD", "PASSWD", "PWD", "PASSPHRASE", "SECRET", "SECRETS", "TOKEN", "TOKENS", "AUTH", "AUTHORIZATION",
|
|
25
|
+
"CREDENTIAL", "CREDENTIALS", "PRIVATE", "APIKEY", "DSN", "BEARER", "SALT", "SIGNATURE"}
|
|
26
|
+
_NOT_SECRET_TAIL = ("_KEY_ID", "_PUBLIC_KEY", "_KEY_PATH")
|
|
27
|
+
_URL_PW = re.compile(r"(?P<head>\b[A-Za-z][\w+.-]*://[^/\s:@'\"]*:)(?P<pw>[^@/\s'\"]+)(?P<tail>@)")
|
|
28
|
+
_PLACEHOLDER = ("${", "{{", "%(", "<", "$")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def secret_key(key: str) -> bool:
|
|
32
|
+
"""The `cg surface` rule for a config key / env name that holds a credential."""
|
|
33
|
+
k = re.sub(r"[^A-Za-z0-9]+", "_", key).upper().strip("_")
|
|
34
|
+
return any(k.endswith("_" + s) or k == s for s in SECRET_WORDS) and not k.endswith(_NOT_SECRET_TAIL)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def looks_secret(key: str) -> bool:
|
|
38
|
+
"""Wider than `secret_key`: any word of the name says credential (private_key_pem, db_dsn, apiKey, auth_header)."""
|
|
39
|
+
if secret_key(key):
|
|
40
|
+
return True
|
|
41
|
+
spaced = re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", str(key))
|
|
42
|
+
words = [w for w in re.split(r"[^A-Za-z0-9]+", spaced.upper()) if w]
|
|
43
|
+
if any(w in _EXTRA_WORDS for w in words):
|
|
44
|
+
return True
|
|
45
|
+
return "KEY" in words and not set(words) & {"PUBLIC", "ID", "PATH", "FILE", "NAME", "PREFIX", "TYPE", "SIZE", "LENGTH"}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def is_redacted(v) -> bool:
|
|
49
|
+
return isinstance(v, str) and v.startswith((MARK, LEGACY_MARK))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def new_salt() -> str:
|
|
53
|
+
return secrets.token_hex(16)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def marker(value: str, salt: str) -> str:
|
|
57
|
+
"""The stored stand-in for `value` in the graph that owns `salt`. There is no default salt on purpose: an unsalted
|
|
58
|
+
marker would be a plain hash of the value."""
|
|
59
|
+
if not salt:
|
|
60
|
+
raise ValueError("marker() needs the graph's redact salt")
|
|
61
|
+
return MARK + hmac.new(salt.encode(), value.encode("utf-8", "replace"), hashlib.sha256).hexdigest()[:8]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def placeholder(v) -> bool:
|
|
65
|
+
if not isinstance(v, str):
|
|
66
|
+
return True
|
|
67
|
+
t = v.strip()
|
|
68
|
+
return not t or t.startswith(_PLACEHOLDER) or t.lower() in ("null", "none", "false", "true", "changeme?") or is_redacted(t)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def redact_url(v: str, salt: str) -> str:
|
|
72
|
+
if "://" not in v or "@" not in v:
|
|
73
|
+
return v
|
|
74
|
+
return _URL_PW.sub(lambda m: m["head"] + (m["pw"] if placeholder(m["pw"]) else marker(m["pw"], salt)) + m["tail"], v)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def redact(value, key: str | None, salt: str):
|
|
78
|
+
"""`value` as it may be stored: a marker for a literal under a credential-looking `key`, a URL password replaced
|
|
79
|
+
by one, anything else unchanged. Non-strings and placeholders pass through."""
|
|
80
|
+
if not isinstance(value, str):
|
|
81
|
+
return value
|
|
82
|
+
if key is not None and looks_secret(key) and not placeholder(value):
|
|
83
|
+
return marker(value, salt)
|
|
84
|
+
return redact_url(value, salt)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
_VALUE_KEYS = {"value", "literal", "default", "env_default", "default_value"}
|
|
88
|
+
_NAME_KEYS = ("key", "name", "env", "env_key", "setting")
|
|
89
|
+
# Names that only ever carry a literal credential. `token`, `secret`, `credential(s)` are left out on purpose: plugins
|
|
90
|
+
# use them for identifiers (the Nest dependency-injection token on INJECTS edges, a secret's name, a credential source).
|
|
91
|
+
_DIRECT = {"password", "passwd", "pwd", "passphrase", "api_key", "apikey", "access_key", "secret_key", "client_secret",
|
|
92
|
+
"private_key", "auth_token"}
|
|
93
|
+
_REFERENCE = re.compile(r"^(?:env|config|file|const|literal)[: ]|^\$\{|^[\w./-]+:\d+$")
|
|
94
|
+
STRUCTURAL_EDGES = {"INJECTS", "BOUND_TO", "CREDENTIAL_FROM"}
|
|
95
|
+
# Attr maps keyed by source identifiers: an exported class called `ApiKey` is a name, not a literal under a credential key.
|
|
96
|
+
_IDENTIFIER_MAPS = {"reexports"}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _holds_secret(d: dict, k: str) -> bool:
|
|
100
|
+
if k.lower() in _DIRECT:
|
|
101
|
+
return not _REFERENCE.match(str(d[k]))
|
|
102
|
+
if k in _VALUE_KEYS:
|
|
103
|
+
return any(isinstance(d.get(nk), str) and looks_secret(d[nk]) for nk in _NAME_KEYS)
|
|
104
|
+
return False
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def sweep(builder, salt: str | None = None) -> int:
|
|
108
|
+
"""Last pass over node / edge attrs; returns the count of replaced values. Replaces the password of a
|
|
109
|
+
`scheme://user:password@host` string, a literal under a credential-named attr (`password`, `token`, ...), and the
|
|
110
|
+
`value` / `default` of a `{key|name: <credential-looking>, value: ...}` record (TypeScript / Python config readers,
|
|
111
|
+
Laravel config). Booleans such as `credential_literal`, hosts and every other value stay as they are. The salt is
|
|
112
|
+
`salt`, else `builder.redact_salt` (set by the indexer from the graph DB)."""
|
|
113
|
+
salt = salt or getattr(builder, "redact_salt", None)
|
|
114
|
+
if not salt:
|
|
115
|
+
raise ValueError("sweep() needs the graph's redact salt")
|
|
116
|
+
n = 0
|
|
117
|
+
|
|
118
|
+
def walk(x, name=None):
|
|
119
|
+
nonlocal n
|
|
120
|
+
if name in _IDENTIFIER_MAPS:
|
|
121
|
+
return x
|
|
122
|
+
if isinstance(x, str):
|
|
123
|
+
if "@" in x and "://" in x:
|
|
124
|
+
y = redact_url(x, salt)
|
|
125
|
+
if y != x:
|
|
126
|
+
n += 1
|
|
127
|
+
return y
|
|
128
|
+
return x
|
|
129
|
+
if isinstance(x, dict):
|
|
130
|
+
for k, v in x.items():
|
|
131
|
+
if isinstance(v, str) and _holds_secret(x, k) and not placeholder(v):
|
|
132
|
+
x[k] = marker(v, salt)
|
|
133
|
+
n += 1
|
|
134
|
+
continue
|
|
135
|
+
nv = walk(v, k)
|
|
136
|
+
if nv is not v:
|
|
137
|
+
x[k] = nv
|
|
138
|
+
return x
|
|
139
|
+
if isinstance(x, list):
|
|
140
|
+
for i, v in enumerate(x):
|
|
141
|
+
nv = walk(v)
|
|
142
|
+
if nv is not v:
|
|
143
|
+
x[i] = nv
|
|
144
|
+
return x
|
|
145
|
+
return x
|
|
146
|
+
for node in builder.nodes.values():
|
|
147
|
+
doc = getattr(node, "doc", None)
|
|
148
|
+
if isinstance(doc, str) and "@" in doc and "://" in doc:
|
|
149
|
+
y = redact_url(doc, salt)
|
|
150
|
+
if y != doc:
|
|
151
|
+
node.doc = y
|
|
152
|
+
n += 1
|
|
153
|
+
if node.attrs:
|
|
154
|
+
walk(node.attrs)
|
|
155
|
+
for e in builder.edges.values():
|
|
156
|
+
if e.attrs and e.kind not in STRUCTURAL_EDGES:
|
|
157
|
+
walk(e.attrs)
|
|
158
|
+
return n
|
|
@@ -343,6 +343,11 @@ def compute(root: str | Path, plugins: dict, scip_imported: bool = False, report
|
|
|
343
343
|
at = st.get("realtime_subscriptions_unresolved_receiver_at") or []
|
|
344
344
|
if at:
|
|
345
345
|
e["realtime_subscriptions_unresolved_receiver_at"] = at[:8]
|
|
346
|
+
if lang == "typescript" and st and st.get("realtime_subscriptions_unresolved_wrapper"):
|
|
347
|
+
e["realtime_subscriptions_unresolved_wrapper"] = st["realtime_subscriptions_unresolved_wrapper"]
|
|
348
|
+
at = st.get("realtime_subscriptions_unresolved_wrapper_at") or []
|
|
349
|
+
if at:
|
|
350
|
+
e["realtime_subscriptions_unresolved_wrapper_at"] = at[:8]
|
|
346
351
|
if lang == "python" and st:
|
|
347
352
|
for k in ("roots_mode", "source_roots", "roots_warnings", "roots_ambiguous", "module_name_collisions"):
|
|
348
353
|
if st.get(k):
|
|
@@ -503,13 +508,22 @@ def _entry_text(e: dict) -> str:
|
|
|
503
508
|
|
|
504
509
|
|
|
505
510
|
def realtime_unresolved_lines(e: dict, indent: str = " ") -> list[str]:
|
|
506
|
-
"""'.private(...)' on a receiver that is not an Echo client, with a channel literal
|
|
507
|
-
|
|
508
|
-
if
|
|
511
|
+
"""'.private(...)' on a receiver that is not an Echo client, with a channel literal, and subscription wrapper calls whose
|
|
512
|
+
channel does not resolve: where to look."""
|
|
513
|
+
if e.get("language") != "typescript":
|
|
509
514
|
return []
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
515
|
+
out = []
|
|
516
|
+
n = e.get("realtime_subscriptions_unresolved_receiver") or 0
|
|
517
|
+
if n:
|
|
518
|
+
at = e.get("realtime_subscriptions_unresolved_receiver_at") or []
|
|
519
|
+
where = f" ({', '.join(at[:5])})" if at else ""
|
|
520
|
+
out.append(f"{indent}typescript: {n} .private(...) call{'s' if n != 1 else ''} on an unknown receiver{where}")
|
|
521
|
+
w = e.get("realtime_subscriptions_unresolved_wrapper") or 0
|
|
522
|
+
if w:
|
|
523
|
+
at = e.get("realtime_subscriptions_unresolved_wrapper_at") or []
|
|
524
|
+
where = f" ({', '.join(at[:5])})" if at else ""
|
|
525
|
+
out.append(f"{indent}typescript: {w} subscription wrapper call{'s' if w != 1 else ''} whose channel does not resolve{where}")
|
|
526
|
+
return out
|
|
513
527
|
|
|
514
528
|
|
|
515
529
|
def syntax_error_lines(e: dict, all_files: bool = False, indent: str = " ") -> list[str]:
|
|
@@ -553,9 +567,11 @@ def summary_line(cov: dict | None, repo: str | None = None) -> str:
|
|
|
553
567
|
|
|
554
568
|
def _bs_count(bs: list[dict]) -> str:
|
|
555
569
|
r = sum(b["count"] for b in bs if b["category"] == "route")
|
|
556
|
-
h = sum(b["count"] for b in bs if b["category"]
|
|
570
|
+
h = sum(b["count"] for b in bs if b["category"] == "handler")
|
|
571
|
+
x = sum(b["count"] for b in bs if b["category"] == "endpoint")
|
|
557
572
|
parts = ([f"{r} unmodelled route registration{'s' if r != 1 else ''}"] if r else []) + \
|
|
558
|
-
([f"{h} handler{'s' if h != 1 else ''} registered dynamically"] if h else [])
|
|
573
|
+
([f"{h} handler{'s' if h != 1 else ''} registered dynamically"] if h else []) + \
|
|
574
|
+
([f"{x} file{'s' if x != 1 else ''} with an outbound HTTP call that made no endpoint"] if x else [])
|
|
559
575
|
return ", ".join(parts)
|
|
560
576
|
|
|
561
577
|
|
|
@@ -911,6 +927,21 @@ def completeness(covs: dict[str, dict | None], languages=None, dirs=None, repos=
|
|
|
911
927
|
return out
|
|
912
928
|
|
|
913
929
|
|
|
930
|
+
def endpoint_note(store) -> str:
|
|
931
|
+
"""`coverage note:` for outbound HTTP calls cg could not turn into endpoints (the `endpoint` blind spots), shown after
|
|
932
|
+
`cg api-calls` / `cg external`; '' when there are none."""
|
|
933
|
+
try:
|
|
934
|
+
covs = for_graph(store)
|
|
935
|
+
except Exception: # noqa: BLE001
|
|
936
|
+
return ""
|
|
937
|
+
found = [b for cov in covs.values() for b in blind_spots(cov) if b["category"] == "endpoint"]
|
|
938
|
+
if not found:
|
|
939
|
+
return ""
|
|
940
|
+
parts = [f"{b['count']} file{'s' if b['count'] != 1 else ''} call an HTTP client cg made no endpoint for ({', '.join(b['samples'][:3])})"
|
|
941
|
+
for b in found]
|
|
942
|
+
return "coverage note: " + "; ".join(parts) + ". Outbound calls from there may be missing; use your normal search and file reading."
|
|
943
|
+
|
|
944
|
+
|
|
914
945
|
def completeness_for(store, node_ids=None, categories=("route", "handler"), whole: bool = False, **kw) -> dict:
|
|
915
946
|
try:
|
|
916
947
|
covs = for_graph(store)
|
|
@@ -931,23 +962,27 @@ def possibly_more(comp: dict) -> str:
|
|
|
931
962
|
return ""
|
|
932
963
|
parts = []
|
|
933
964
|
r = sum(b["count"] for b in comp.get("blind_spots", []) if b["category"] == "route")
|
|
934
|
-
h = sum(b["count"] for b in comp.get("blind_spots", []) if b["category"]
|
|
965
|
+
h = sum(b["count"] for b in comp.get("blind_spots", []) if b["category"] == "handler")
|
|
966
|
+
x = sum(b["count"] for b in comp.get("blind_spots", []) if b["category"] == "endpoint")
|
|
935
967
|
if r:
|
|
936
968
|
parts.append(f"{r} unmodelled route registration{'s' if r != 1 else ''}")
|
|
937
969
|
if h:
|
|
938
970
|
parts.append(f"{h} dynamically registered handler{'s' if h != 1 else ''}")
|
|
971
|
+
if x:
|
|
972
|
+
parts.append(f"{x} file{'s' if x != 1 else ''} with an outbound HTTP call that made no endpoint")
|
|
939
973
|
for k, v in comp.get("languages", {}).items():
|
|
940
974
|
if v["complete"]:
|
|
941
975
|
continue
|
|
942
|
-
lang = k.
|
|
976
|
+
repo, _, lang = k.rpartition("/")
|
|
943
977
|
label = LANG_LABEL.get(lang, lang)
|
|
978
|
+
of = f" ({repo})" if repo else "" # combined graph: keys are `<repo>/<language>`; two repos must not print identical fragments
|
|
944
979
|
miss = sum(v.get(b, 0) for b in ("parse_failed", "skipped_oversize", "unmapped"))
|
|
945
980
|
if v["mode"] in ("exact", "scip") and miss:
|
|
946
|
-
parts.append(f"{miss} {label} file{'s' if miss != 1 else ''} not indexed")
|
|
981
|
+
parts.append(f"{miss} {label} file{'s' if miss != 1 else ''} not indexed{of}")
|
|
947
982
|
elif v["mode"] == "heuristic":
|
|
948
|
-
parts.append(f"{label} heuristic only")
|
|
983
|
+
parts.append(f"{label} heuristic only{of}")
|
|
949
984
|
else:
|
|
950
|
-
parts.append(f"{label} {v['mode'].replace('_', ' ')}")
|
|
985
|
+
parts.append(f"{label} {v['mode'].replace('_', ' ')}{of}")
|
|
951
986
|
se = comp.get("syntax_errors") or []
|
|
952
987
|
if se:
|
|
953
988
|
x = se[0]
|
|
@@ -968,7 +1003,7 @@ def answer_note(comp: dict) -> str:
|
|
|
968
1003
|
return ""
|
|
969
1004
|
parts = []
|
|
970
1005
|
for b in comp.get("blind_spots", []):
|
|
971
|
-
noun = "route registration"
|
|
1006
|
+
noun = {"route": "route registration", "endpoint": "outbound HTTP call"}.get(b["category"], "handler registration")
|
|
972
1007
|
parts.append(f"{b['count']} {noun}{'s' if b['count'] != 1 else ''} cg does not model ({b['what']}: {b['sample']})")
|
|
973
1008
|
rest = possibly_more({**comp, "blind_spots": []})
|
|
974
1009
|
if rest:
|