sourcecode 3.1.1__py3-none-any.whl → 3.2.0__py3-none-any.whl
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.
Potentially problematic release.
This version of sourcecode might be problematic. Click here for more details.
- sourcecode/__init__.py +1 -1
- sourcecode/cli.py +54 -9
- sourcecode/detectors/java.py +10 -5
- sourcecode/integration_coordinates.py +175 -0
- sourcecode/mcp/registry.py +3 -1
- sourcecode/mcp/server.py +5 -2
- sourcecode/repository_ir.py +25 -17
- sourcecode/retrieval/steps_graph.py +10 -8
- sourcecode/retrieval/steps_intf.py +23 -1
- sourcecode/security_posture.py +165 -19
- sourcecode/semantic_integration_engine.py +54 -2
- sourcecode/spring_impact.py +124 -7
- sourcecode/spring_semantic.py +56 -4
- {sourcecode-3.1.1.dist-info → sourcecode-3.2.0.dist-info}/METADATA +1 -1
- {sourcecode-3.1.1.dist-info → sourcecode-3.2.0.dist-info}/RECORD +18 -17
- {sourcecode-3.1.1.dist-info → sourcecode-3.2.0.dist-info}/WHEEL +0 -0
- {sourcecode-3.1.1.dist-info → sourcecode-3.2.0.dist-info}/entry_points.txt +0 -0
- {sourcecode-3.1.1.dist-info → sourcecode-3.2.0.dist-info}/licenses/LICENSE +0 -0
sourcecode/__init__.py
CHANGED
sourcecode/cli.py
CHANGED
|
@@ -4084,7 +4084,11 @@ def impact_cmd(
|
|
|
4084
4084
|
- direct_callers — classes that directly call or depend on the target
|
|
4085
4085
|
- indirect_callers — transitive callers (BFS, bounded by --depth)
|
|
4086
4086
|
- endpoints_affected — HTTP endpoints that transitively depend on the target
|
|
4087
|
-
- transactional_boundaries_touched — @Transactional
|
|
4087
|
+
- transactional_boundaries_touched — @Transactional declarations on the target
|
|
4088
|
+
and on everything that calls it
|
|
4089
|
+
- transactional_boundaries_reaching — the callers-only subset: what would run
|
|
4090
|
+
this change inside a transaction (the figure
|
|
4091
|
+
`ask retrieve transactions-reaching` reports)
|
|
4088
4092
|
- risk_score / risk_level — quantified change risk
|
|
4089
4093
|
|
|
4090
4094
|
\b
|
|
@@ -4714,7 +4718,10 @@ def export_cmd(
|
|
|
4714
4718
|
),
|
|
4715
4719
|
integrations: bool = typer.Option(
|
|
4716
4720
|
False, "--integrations",
|
|
4717
|
-
help=
|
|
4721
|
+
help=(
|
|
4722
|
+
"Detect outbound integrations (HTTP/LDAP/JMS clients) with file:line "
|
|
4723
|
+
"evidence, plus the client libraries the build declares."
|
|
4724
|
+
),
|
|
4718
4725
|
),
|
|
4719
4726
|
c4: bool = typer.Option(
|
|
4720
4727
|
False, "--c4",
|
|
@@ -4742,6 +4749,11 @@ def export_cmd(
|
|
|
4742
4749
|
rolled up from class-level relation edges.
|
|
4743
4750
|
--integrations Outbound integrations (RestTemplate/WebClient/Feign/LDAP/JMS)
|
|
4744
4751
|
with file:line evidence — external-system dependency arrows.
|
|
4752
|
+
Also lists the client libraries the BUILD declares
|
|
4753
|
+
(declared_clients) and which of those no source construct
|
|
4754
|
+
uses — a config-wired client has no construct to find, so a
|
|
4755
|
+
count of 0 is never left standing alone against a client
|
|
4756
|
+
library named in the stack block.
|
|
4745
4757
|
--c4 Unified architecture document mapped onto the open C4 model
|
|
4746
4758
|
(context/containers/components/code) + an API surface and a
|
|
4747
4759
|
per-directory content-hash manifest for incremental consumers.
|
|
@@ -4792,9 +4804,12 @@ def export_cmd(
|
|
|
4792
4804
|
graph = ir.get("graph", {})
|
|
4793
4805
|
_ep_data = extract_java_endpoints(root)
|
|
4794
4806
|
endpoints = _ep_data.get("endpoints", [])
|
|
4807
|
+
from sourcecode.integration_coordinates import declared_integration_coordinates
|
|
4795
4808
|
_integrations = SemanticIntegrationEngine(
|
|
4796
4809
|
ContextGraph.build(file_list, root)
|
|
4797
|
-
).as_report(
|
|
4810
|
+
).as_report(
|
|
4811
|
+
len(file_list), declared=declared_integration_coordinates(root),
|
|
4812
|
+
)
|
|
4798
4813
|
data = _build_c4_export(
|
|
4799
4814
|
root,
|
|
4800
4815
|
file_list,
|
|
@@ -4831,9 +4846,12 @@ def export_cmd(
|
|
|
4831
4846
|
if integrations:
|
|
4832
4847
|
from sourcecode.context_graph import ContextGraph
|
|
4833
4848
|
from sourcecode.semantic_integration_engine import SemanticIntegrationEngine
|
|
4849
|
+
from sourcecode.integration_coordinates import declared_integration_coordinates
|
|
4834
4850
|
data["integrations"] = SemanticIntegrationEngine(
|
|
4835
4851
|
ContextGraph.build(file_list, root)
|
|
4836
|
-
).as_report(
|
|
4852
|
+
).as_report(
|
|
4853
|
+
len(file_list), declared=declared_integration_coordinates(root),
|
|
4854
|
+
)
|
|
4837
4855
|
|
|
4838
4856
|
_prog.finish()
|
|
4839
4857
|
output = _serialize_dict(data, format)
|
|
@@ -5684,19 +5702,35 @@ def _render_gate_coverage_section(result: "SpringAuditResult") -> list[str]: #
|
|
|
5684
5702
|
gc = (result.security_posture or {}).get("gate_coverage")
|
|
5685
5703
|
if not gc:
|
|
5686
5704
|
return []
|
|
5687
|
-
not_covered = gc.get("not_carrying_gate", 0)
|
|
5705
|
+
not_covered = gc.get("endpoints_not_carrying_gate", gc.get("not_carrying_gate", 0))
|
|
5688
5706
|
gates = ", ".join(f"`{g}`" for g in gc.get("gate_annotations", [])) or "the detected gate"
|
|
5689
|
-
|
|
5707
|
+
# One unit, named: these are endpoints, not handler methods. The two counts differ
|
|
5708
|
+
# (one method can serve several mappings) and must never be summed together.
|
|
5709
|
+
total = gc.get("endpoints_total", gc.get("total_controller_handlers", 0))
|
|
5690
5710
|
lines: list[str] = ["", "---", ""]
|
|
5691
5711
|
if not_covered == 0:
|
|
5692
|
-
|
|
5712
|
+
_gated = gc.get("endpoints_carrying_gate", total)
|
|
5713
|
+
_std = gc.get("endpoints_standard_guarded", 0)
|
|
5714
|
+
if _std:
|
|
5715
|
+
lines.append(
|
|
5716
|
+
f"✅ **Gate coverage** — all {total} endpoints are guarded: {_gated} carry "
|
|
5717
|
+
f"{gates}, {_std} rely on a standard guard."
|
|
5718
|
+
)
|
|
5719
|
+
else:
|
|
5720
|
+
lines.append(f"✅ **Gate coverage** — all {total} endpoints carry {gates}.")
|
|
5693
5721
|
return lines
|
|
5694
5722
|
|
|
5695
5723
|
covered = gc.get("possibly_filter_covered", 0)
|
|
5724
|
+
_standard = gc.get("endpoints_standard_guarded", 0)
|
|
5696
5725
|
lines.append(
|
|
5697
|
-
f"🔓 **Gate coverage** — {not_covered} of {total}
|
|
5726
|
+
f"🔓 **Gate coverage** — {not_covered} of {total} endpoints do not carry "
|
|
5698
5727
|
f"{gates}."
|
|
5699
5728
|
)
|
|
5729
|
+
if _standard:
|
|
5730
|
+
lines.append(
|
|
5731
|
+
f"_{gc.get('endpoints_carrying_gate', 0)} carry the gate, {_standard} rely on a "
|
|
5732
|
+
f"standard guard, {not_covered} on neither — the three sum to {total}._"
|
|
5733
|
+
)
|
|
5700
5734
|
if gc.get("reconstructed_filter_patterns"):
|
|
5701
5735
|
lines.append(
|
|
5702
5736
|
f"_{covered} of those match a reconstructed servlet filter pattern "
|
|
@@ -6369,6 +6403,16 @@ def migrate_check_cmd(
|
|
|
6369
6403
|
ask migrate-check . --output migration.json
|
|
6370
6404
|
ask migrate-check . --snapshot --ref sprint-12 persist a readiness point
|
|
6371
6405
|
ask migrate-check . --trend days-remaining over time
|
|
6406
|
+
ask migrate-check . --blast-radius endpoints behind each blocker
|
|
6407
|
+
|
|
6408
|
+
\b
|
|
6409
|
+
Blast radius (--blast-radius):
|
|
6410
|
+
Ranks the affected product files by the HTTP endpoints whose call path runs
|
|
6411
|
+
through them — the re-test plan, ordered by regression scope. Reach is never
|
|
6412
|
+
build breakage: a javax→jakarta finding fails compilation whether or not an
|
|
6413
|
+
endpoint reaches it. A file whose types the IR could not resolve is reported
|
|
6414
|
+
unresolved (reach unknown, never zero), and a repository with no endpoints in
|
|
6415
|
+
the IR gets a null share with the reason stated instead of a 0.0.
|
|
6372
6416
|
|
|
6373
6417
|
\b
|
|
6374
6418
|
Readiness time series:
|
|
@@ -6587,7 +6631,8 @@ def impact_chain_cmd(
|
|
|
6587
6631
|
- indirect_callers — transitive callers (BFS up to --depth hops)
|
|
6588
6632
|
- endpoints_affected — HTTP endpoints reachable through the call chain
|
|
6589
6633
|
- transaction_boundary — @Transactional semantics on the target (if any)
|
|
6590
|
-
- security_surfaces — per-endpoint security
|
|
6634
|
+
- security_surfaces — per-endpoint security verdict + confidence
|
|
6635
|
+
(security_posture authority) + declared policy + SEC findings
|
|
6591
6636
|
- impact_findings — TX/SEC audit findings touching the call chain
|
|
6592
6637
|
- risk_level — critical | high | medium | low
|
|
6593
6638
|
|
sourcecode/detectors/java.py
CHANGED
|
@@ -15,6 +15,7 @@ from sourcecode.detectors.parsers import (
|
|
|
15
15
|
substitute_maven_properties as _resolve_maven_property,
|
|
16
16
|
unique_strings,
|
|
17
17
|
)
|
|
18
|
+
from sourcecode.integration_coordinates import coordinates_for_kind
|
|
18
19
|
from sourcecode.schema import FrameworkDetection
|
|
19
20
|
from sourcecode.tree_utils import flatten_file_tree
|
|
20
21
|
|
|
@@ -488,11 +489,15 @@ class JavaDetector(AbstractDetector):
|
|
|
488
489
|
if "spring-boot-starter-data-jpa" in text or "spring-data-jpa" in text:
|
|
489
490
|
frameworks.append(FrameworkDetection(name="Spring Data JPA", source=source))
|
|
490
491
|
# The Boot starter names neither of the module coordinates, so a Boot app
|
|
491
|
-
# wiring LDAP the standard way declared no LDAP framework at all.
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
492
|
+
# wiring LDAP the standard way declared no LDAP framework at all. The coordinate
|
|
493
|
+
# list lives in `integration_coordinates` — the same set `export --integrations`
|
|
494
|
+
# reads, so this stack block and that report can never recognize different
|
|
495
|
+
# libraries for the same external system (ADR-0008 R12).
|
|
496
|
+
# Only the Spring-family coordinates may carry the "Spring LDAP" label — the
|
|
497
|
+
# authority also knows non-Spring directory clients, and naming the wrong library
|
|
498
|
+
# would trade one incoherence for another.
|
|
499
|
+
if any(
|
|
500
|
+
c in text for c in coordinates_for_kind("ldap") if c.startswith("spring-")
|
|
496
501
|
):
|
|
497
502
|
frameworks.append(FrameworkDetection(name="Spring LDAP", source=source))
|
|
498
503
|
if "spring-aspects" in text or "spring-aop" in text:
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""integration_coordinates.py — the declared-client signal for outbound integrations.
|
|
2
|
+
|
|
3
|
+
THE authority for one question: *which outbound-integration client libraries does this
|
|
4
|
+
repository's build declare?* (ADR-0008 R1). It is a different question from "which client
|
|
5
|
+
constructs does the source contain" — that one belongs to
|
|
6
|
+
:mod:`sourcecode.semantic_integration_engine` — and the two answers only conflict when a
|
|
7
|
+
document reports one and stays silent about the other.
|
|
8
|
+
|
|
9
|
+
That is exactly what the field reported, twice, two majors apart: `export --integrations`
|
|
10
|
+
answered `count: 0` on a repository whose stack block listed **Spring LDAP**. Reproduced on
|
|
11
|
+
a Boot app that declares `spring-boot-starter-data-ldap` and configures
|
|
12
|
+
`spring.ldap.urls` in YAML: the directory is wired entirely by properties, so no LDAP
|
|
13
|
+
client type is ever imported and the construct scan is *right* to find nothing — but the
|
|
14
|
+
document as a whole then said the system has no outbound integrations while its own stack
|
|
15
|
+
block said it talks to a directory.
|
|
16
|
+
|
|
17
|
+
A declared coordinate is **evidence of a declared client, never an observed integration**
|
|
18
|
+
(the same discipline that stopped a `org.mybatis` coordinate from being reported as MyBatis
|
|
19
|
+
usage in `detectors/java.py`). This module answers what the build declares; the engine
|
|
20
|
+
answers what the code does; the payload states both and says which is which.
|
|
21
|
+
|
|
22
|
+
VAI (see `security_posture` §0.5): the table below lists **published open-source artifact
|
|
23
|
+
coordinates** — the identity of a library, matched literally, the same basis as the engine's
|
|
24
|
+
published client-type table. No proprietary, client or convention name appears here, and no
|
|
25
|
+
predicate branches on a repository's own naming.
|
|
26
|
+
"""
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import re
|
|
30
|
+
from dataclasses import dataclass
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
from typing import Iterable, Optional
|
|
33
|
+
|
|
34
|
+
# Build-manifest artifact coordinate → the outbound-integration kind it declares.
|
|
35
|
+
# Kinds are exactly the engine's kinds, so declared and observed answers are comparable.
|
|
36
|
+
# Only coordinates whose sole purpose is to talk to an external system are listed: a
|
|
37
|
+
# driver-or-anything coordinate that a repository might carry for unrelated reasons has no
|
|
38
|
+
# place here, because a declaration nobody can act on is noise, not evidence.
|
|
39
|
+
_COORDINATE_KIND: "dict[str, str]" = {
|
|
40
|
+
# Directory (LDAP)
|
|
41
|
+
"spring-ldap-core": "ldap",
|
|
42
|
+
"spring-security-ldap": "ldap",
|
|
43
|
+
"spring-boot-starter-data-ldap": "ldap",
|
|
44
|
+
"spring-boot-starter-ldap": "ldap",
|
|
45
|
+
"unboundid-ldapsdk": "ldap",
|
|
46
|
+
# Kafka
|
|
47
|
+
"spring-kafka": "kafka",
|
|
48
|
+
"kafka-clients": "kafka",
|
|
49
|
+
"spring-cloud-stream-binder-kafka": "kafka",
|
|
50
|
+
# RabbitMQ / AMQP
|
|
51
|
+
"spring-boot-starter-amqp": "rabbitmq",
|
|
52
|
+
"spring-rabbit": "rabbitmq",
|
|
53
|
+
"amqp-client": "rabbitmq",
|
|
54
|
+
# JMS brokers
|
|
55
|
+
"spring-boot-starter-activemq": "jms",
|
|
56
|
+
"spring-boot-starter-artemis": "jms",
|
|
57
|
+
"activemq-client": "jms",
|
|
58
|
+
"activemq-broker": "jms",
|
|
59
|
+
"artemis-jms-client": "jms",
|
|
60
|
+
"spring-jms": "jms",
|
|
61
|
+
# Redis
|
|
62
|
+
"spring-boot-starter-data-redis": "redis",
|
|
63
|
+
"spring-data-redis": "redis",
|
|
64
|
+
"lettuce-core": "redis",
|
|
65
|
+
"jedis": "redis",
|
|
66
|
+
# Elasticsearch
|
|
67
|
+
"spring-boot-starter-data-elasticsearch": "elasticsearch",
|
|
68
|
+
"elasticsearch-rest-high-level-client": "elasticsearch",
|
|
69
|
+
"elasticsearch-java": "elasticsearch",
|
|
70
|
+
# Mail (SMTP)
|
|
71
|
+
"spring-boot-starter-mail": "smtp",
|
|
72
|
+
# Declarative HTTP clients
|
|
73
|
+
"spring-cloud-starter-openfeign": "http",
|
|
74
|
+
"feign-core": "http",
|
|
75
|
+
"okhttp": "http",
|
|
76
|
+
# SOAP
|
|
77
|
+
"spring-ws-core": "soap",
|
|
78
|
+
"spring-boot-starter-web-services": "soap",
|
|
79
|
+
"cxf-rt-frontend-jaxws": "soap",
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
# Build manifests scanned. Text-level, like the framework-coordinate scan in
|
|
83
|
+
# `detectors/java.py` — a build file is configuration, not source.
|
|
84
|
+
_MANIFEST_NAMES: frozenset[str] = frozenset({
|
|
85
|
+
"pom.xml", "build.gradle", "build.gradle.kts",
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
# Directories that never hold a production build manifest.
|
|
89
|
+
_SKIP_DIRS: frozenset[str] = frozenset({
|
|
90
|
+
".git", ".idea", "target", "build", "node_modules", ".sourcecode-cache",
|
|
91
|
+
".gradle", "out", "dist",
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
# A repository with thousands of modules is real (OFBiz, Broadleaf); an unbounded walk is
|
|
95
|
+
# not. The cap is stated in the payload when it bites, never applied silently.
|
|
96
|
+
_MAX_MANIFESTS = 400
|
|
97
|
+
|
|
98
|
+
_TEST_SCOPE_WINDOW = 4 # lines after a Maven coordinate that may carry its <scope>
|
|
99
|
+
_TEST_SCOPE = re.compile(r"<scope>\s*(test|provided)\s*</scope>", re.IGNORECASE)
|
|
100
|
+
_GRADLE_TEST_CONF = re.compile(r"^\s*(test|androidTest)[A-Za-z]*\s*[(\s'\"]", re.IGNORECASE)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@dataclass(frozen=True)
|
|
104
|
+
class DeclaredClient:
|
|
105
|
+
"""A client library the build declares, with the line that declares it."""
|
|
106
|
+
|
|
107
|
+
kind: str
|
|
108
|
+
coordinate: str
|
|
109
|
+
evidence: str # "relpath:line" — same shape as an Integration's evidence
|
|
110
|
+
|
|
111
|
+
def to_dict(self) -> dict:
|
|
112
|
+
return {"kind": self.kind, "coordinate": self.coordinate, "evidence": self.evidence}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def coordinates_for_kind(kind: str) -> tuple[str, ...]:
|
|
116
|
+
"""Published coordinates that declare ``kind``. One home for the list, so a consumer
|
|
117
|
+
that recognizes a library recognizes the same set this authority does."""
|
|
118
|
+
return tuple(sorted(c for c, k in _COORDINATE_KIND.items() if k == kind))
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _manifest_paths(root: Path) -> list[Path]:
|
|
122
|
+
found: list[Path] = []
|
|
123
|
+
for name in sorted(_MANIFEST_NAMES):
|
|
124
|
+
for path in sorted(root.rglob(name)):
|
|
125
|
+
if len(found) >= _MAX_MANIFESTS:
|
|
126
|
+
return found
|
|
127
|
+
if any(part in _SKIP_DIRS for part in path.parts):
|
|
128
|
+
continue
|
|
129
|
+
if path.is_file():
|
|
130
|
+
found.append(path)
|
|
131
|
+
return found
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _is_test_scoped(lines: list[str], idx: int, manifest: str) -> bool:
|
|
135
|
+
"""True when the declaration is a test/provided dependency — a build-time coordinate
|
|
136
|
+
is not a statement that the running system talks to that external party."""
|
|
137
|
+
if manifest == "pom.xml":
|
|
138
|
+
for line in lines[idx: idx + _TEST_SCOPE_WINDOW + 1]:
|
|
139
|
+
if _TEST_SCOPE.search(line):
|
|
140
|
+
return True
|
|
141
|
+
return False
|
|
142
|
+
return bool(_GRADLE_TEST_CONF.match(lines[idx]))
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def declared_integration_coordinates(
|
|
146
|
+
root: Optional[Path], *, manifests: Optional[Iterable[Path]] = None,
|
|
147
|
+
) -> list[DeclaredClient]:
|
|
148
|
+
"""Client libraries the build declares, deterministically ordered, deduplicated by
|
|
149
|
+
(kind, coordinate). Never raises: an unreadable manifest contributes nothing.
|
|
150
|
+
"""
|
|
151
|
+
if root is None:
|
|
152
|
+
return []
|
|
153
|
+
root = Path(root)
|
|
154
|
+
paths = list(manifests) if manifests is not None else _manifest_paths(root)
|
|
155
|
+
|
|
156
|
+
seen: dict[tuple[str, str], DeclaredClient] = {}
|
|
157
|
+
for path in paths:
|
|
158
|
+
try:
|
|
159
|
+
lines = path.read_text(encoding="utf-8", errors="ignore").splitlines()
|
|
160
|
+
except OSError:
|
|
161
|
+
continue
|
|
162
|
+
try:
|
|
163
|
+
rel = str(path.relative_to(root)).replace("\\", "/")
|
|
164
|
+
except ValueError:
|
|
165
|
+
rel = path.name
|
|
166
|
+
for idx, line in enumerate(lines):
|
|
167
|
+
for coordinate, kind in _COORDINATE_KIND.items():
|
|
168
|
+
if coordinate not in line:
|
|
169
|
+
continue
|
|
170
|
+
if _is_test_scoped(lines, idx, path.name):
|
|
171
|
+
continue
|
|
172
|
+
key = (kind, coordinate)
|
|
173
|
+
if key not in seen:
|
|
174
|
+
seen[key] = DeclaredClient(kind, coordinate, f"{rel}:{idx + 1}")
|
|
175
|
+
return [seen[k] for k in sorted(seen)]
|
sourcecode/mcp/registry.py
CHANGED
|
@@ -781,7 +781,9 @@ Blast-radius analysis: who calls a class and what breaks if it changes? Java onl
|
|
|
781
781
|
|
|
782
782
|
Maps to: ask impact <target> <repo_path> [--depth <depth>]
|
|
783
783
|
Returns: direct_callers, indirect_callers, endpoints_affected,
|
|
784
|
-
transactional_boundaries_touched
|
|
784
|
+
transactional_boundaries_touched (target + callers),
|
|
785
|
+
transactional_boundaries_reaching (callers only), risk_score,
|
|
786
|
+
risk_level, stats.
|
|
785
787
|
|
|
786
788
|
Use this when:
|
|
787
789
|
- Planning a refactor: understand the full call chain before changing a class
|
sourcecode/mcp/server.py
CHANGED
|
@@ -890,7 +890,8 @@ def get_impact_chain(repo_path: str = ".", symbol: str = "", depth: int = 4) ->
|
|
|
890
890
|
Returns: ImpactChainResult with schema_version, symbol, resolution,
|
|
891
891
|
direct_callers, indirect_callers, endpoints_affected,
|
|
892
892
|
transaction_boundary (propagation/isolation/read_only),
|
|
893
|
-
security_surfaces (per-endpoint
|
|
893
|
+
security_surfaces (per-endpoint verdict/confidence from the
|
|
894
|
+
security_posture authority + declared policy + finding IDs),
|
|
894
895
|
impact_findings (TX-001..005 + SEC-001..003 findings in call chain),
|
|
895
896
|
analysis_warnings, risk_level, confidence, metadata.
|
|
896
897
|
|
|
@@ -1320,7 +1321,9 @@ def get_impact_context(repo_path: str = ".", target: str = "", depth: int = 4) -
|
|
|
1320
1321
|
|
|
1321
1322
|
Maps to: ask impact <target> <repo_path> [--depth <depth>]
|
|
1322
1323
|
Returns: direct_callers, indirect_callers, endpoints_affected,
|
|
1323
|
-
transactional_boundaries_touched
|
|
1324
|
+
transactional_boundaries_touched (target + callers),
|
|
1325
|
+
transactional_boundaries_reaching (callers only), risk_score,
|
|
1326
|
+
risk_level, stats.
|
|
1324
1327
|
|
|
1325
1328
|
Use this when:
|
|
1326
1329
|
- Planning a refactor: understand the full call chain before changing a class
|
sourcecode/repository_ir.py
CHANGED
|
@@ -26,6 +26,10 @@ from typing import Any, Iterable, Optional
|
|
|
26
26
|
from sourcecode.caller_metrics import CALLER_METRIC_RECONCILIATION
|
|
27
27
|
from sourcecode.endpoint_metrics import ENDPOINT_SURFACE_RECONCILIATION
|
|
28
28
|
from sourcecode.fqn_utils import normalize_owner_fqn as _normalize_owner_fqn
|
|
29
|
+
from sourcecode.spring_semantic import (
|
|
30
|
+
boundaries_declared_within as _boundaries_declared_within,
|
|
31
|
+
build_tx_index_from_ir as _build_tx_index_from_ir,
|
|
32
|
+
)
|
|
29
33
|
from sourcecode.path_filters import (
|
|
30
34
|
is_test_path as _is_test_path,
|
|
31
35
|
is_test_fixture_module_path as _is_test_fixture_module_path,
|
|
@@ -6803,6 +6807,7 @@ def compute_blast_radius(
|
|
|
6803
6807
|
"security_surface_affected": [],
|
|
6804
6808
|
"cross_module_impact": [],
|
|
6805
6809
|
"transactional_boundaries_touched": [],
|
|
6810
|
+
"transactional_boundaries_reaching": [],
|
|
6806
6811
|
"risk_score": 0.0,
|
|
6807
6812
|
"risk_level": "unknown",
|
|
6808
6813
|
"confidence_score": 0.0,
|
|
@@ -6828,6 +6833,7 @@ def compute_blast_radius(
|
|
|
6828
6833
|
"security_surface_affected": [],
|
|
6829
6834
|
"cross_module_impact": [],
|
|
6830
6835
|
"transactional_boundaries_touched": [],
|
|
6836
|
+
"transactional_boundaries_reaching": [],
|
|
6831
6837
|
"risk_score": 0.0,
|
|
6832
6838
|
"risk_level": "unknown",
|
|
6833
6839
|
"confidence_score": 0.0,
|
|
@@ -7134,23 +7140,19 @@ def compute_blast_radius(
|
|
|
7134
7140
|
})
|
|
7135
7141
|
cross_module_impact = cross_module_impact[:10]
|
|
7136
7142
|
|
|
7137
|
-
# ── 7. Transactional boundaries
|
|
7138
|
-
|
|
7139
|
-
|
|
7140
|
-
|
|
7141
|
-
|
|
7142
|
-
|
|
7143
|
-
|
|
7144
|
-
|
|
7145
|
-
|
|
7146
|
-
|
|
7147
|
-
|
|
7148
|
-
|
|
7149
|
-
|
|
7150
|
-
txn_nodes.append(fqn)
|
|
7151
|
-
break
|
|
7152
|
-
|
|
7153
|
-
txn_nodes = sorted(set(txn_nodes))
|
|
7143
|
+
# ── 7. Transactional boundaries ───────────────────────────────────────────
|
|
7144
|
+
# Both figures come from the one transaction-boundary authority (ADR-0008 R1);
|
|
7145
|
+
# they differ only in which symbols are asked about:
|
|
7146
|
+
# touched — the target and everything that calls it (what this change sits in)
|
|
7147
|
+
# reaching — the callers alone (what would run this change inside a transaction)
|
|
7148
|
+
# This used to be derived here from a graph node `role == "transaction_boundary"`
|
|
7149
|
+
# that no producer ever assigns, so `impact` reported 0 boundaries on repos where
|
|
7150
|
+
# `retrieve transactions-reaching` reported dozens, and `n_txn` never contributed
|
|
7151
|
+
# to the risk score or its escalation rules.
|
|
7152
|
+
_tx_index = _build_tx_index_from_ir(ir)
|
|
7153
|
+
_caller_symbols = set(direct_callers) | set(indirect_callers)
|
|
7154
|
+
txn_nodes = sorted({b.symbol for b in _boundaries_declared_within(_tx_index, affected_classes)})
|
|
7155
|
+
txn_reaching = sorted({b.symbol for b in _boundaries_declared_within(_tx_index, _caller_symbols)})
|
|
7154
7156
|
|
|
7155
7157
|
# ── 8. Risk score ─────────────────────────────────────────────────────────
|
|
7156
7158
|
n_direct = len(direct_callers)
|
|
@@ -7288,6 +7290,11 @@ def compute_blast_radius(
|
|
|
7288
7290
|
"security_surface_affected": security_surface_affected,
|
|
7289
7291
|
"cross_module_impact": cross_module_impact,
|
|
7290
7292
|
"transactional_boundaries_touched": txn_nodes,
|
|
7293
|
+
# The callers-only subset: the boundaries that would run this change inside a
|
|
7294
|
+
# transaction. Same authority, narrower question — and the figure
|
|
7295
|
+
# `retrieve transactions-reaching` answers, so the two commands agree by
|
|
7296
|
+
# construction rather than by coincidence.
|
|
7297
|
+
"transactional_boundaries_reaching": txn_reaching,
|
|
7291
7298
|
"depth_reached": _effective_depth, # actual BFS depth used, not the requested max
|
|
7292
7299
|
"bfs_truncated": _bfs_truncated,
|
|
7293
7300
|
"stats": {
|
|
@@ -7297,6 +7304,7 @@ def compute_blast_radius(
|
|
|
7297
7304
|
"indirect_callers_sampled": _indirect_sampled,
|
|
7298
7305
|
"endpoints_affected_count": n_ep,
|
|
7299
7306
|
"transactional_boundaries_count": n_txn,
|
|
7307
|
+
"transactional_boundaries_reaching_count": len(txn_reaching),
|
|
7300
7308
|
"mappers_affected_count": n_mappers,
|
|
7301
7309
|
"modules_affected_count": n_modules,
|
|
7302
7310
|
"security_surface_count": n_sec,
|
|
@@ -93,16 +93,18 @@ def resolve_execution_paths(step: Step, ctx: "StepContext") -> None:
|
|
|
93
93
|
# ── transactions reaching (impact chain callers ∩ tx_index) ──────────────────────
|
|
94
94
|
def resolve_transactions_reaching(step: Step, ctx: "StepContext") -> None:
|
|
95
95
|
"""The @Transactional boundaries whose enclosing class can reach the target through the
|
|
96
|
-
call chain. Reuses run_impact_chain (reachability) + the tx boundary index.
|
|
96
|
+
call chain. Reuses run_impact_chain (reachability) + the tx boundary index.
|
|
97
|
+
|
|
98
|
+
Membership is decided by the one transaction-boundary authority (ADR-0008 R1), the
|
|
99
|
+
same call `impact` makes for `transactional_boundaries_reaching` — so the two
|
|
100
|
+
commands answer this question with one number instead of two."""
|
|
97
101
|
assert isinstance(step, ResolveTransactionsReaching)
|
|
102
|
+
from sourcecode.spring_semantic import boundaries_declared_within
|
|
103
|
+
|
|
98
104
|
result = _impact_chain(ctx)
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
boundaries = [
|
|
103
|
-
b for b in ctx.knowledge.spring_model.tx_index.all_declared
|
|
104
|
-
if _enclosing_class(b.symbol) in caller_classes
|
|
105
|
-
]
|
|
105
|
+
callers = set(result.direct_callers) | set(result.indirect_callers)
|
|
106
|
+
caller_classes = {_enclosing_class(c) for c in callers}
|
|
107
|
+
boundaries = boundaries_declared_within(ctx.knowledge.spring_model.tx_index, callers)
|
|
106
108
|
ctx.put(
|
|
107
109
|
SLOT_ENTITIES,
|
|
108
110
|
[
|
|
@@ -123,7 +123,29 @@ def resolve_integration_inventory(step: Step, ctx: "StepContext") -> None:
|
|
|
123
123
|
],
|
|
124
124
|
)
|
|
125
125
|
ctx.put(SLOT_EVIDENCE, [_integration_evidence(i) for i in integrations])
|
|
126
|
-
|
|
126
|
+
observations = _kind_histogram(integrations, key=lambda i: i.kind, label="integration")
|
|
127
|
+
# A client library the build declares but whose construct nothing imports is not an
|
|
128
|
+
# entry in this inventory — it is not an observed integration — but leaving it unsaid
|
|
129
|
+
# is how a `count: 0` came to sit in the same document as a directory client library
|
|
130
|
+
# in the stack block (ADR-0008 R12). Same coordinate authority `export --integrations`
|
|
131
|
+
# reads, so both surfaces recognize the same libraries.
|
|
132
|
+
from sourcecode.integration_coordinates import declared_integration_coordinates
|
|
133
|
+
|
|
134
|
+
_declared = declared_integration_coordinates(getattr(ctx.knowledge, "root", None))
|
|
135
|
+
_observed_kinds = {i.kind for i in integrations}
|
|
136
|
+
for d in _declared:
|
|
137
|
+
if d.kind in _observed_kinds:
|
|
138
|
+
continue
|
|
139
|
+
observations.append(Observation(
|
|
140
|
+
name=f"declared_not_observed:{d.kind}",
|
|
141
|
+
value=d.coordinate,
|
|
142
|
+
detail=(
|
|
143
|
+
f"the build declares this client at {d.evidence}; no client construct "
|
|
144
|
+
"found in source (a properties/YAML/XML-wired client has none) — "
|
|
145
|
+
"declared, not observed"
|
|
146
|
+
),
|
|
147
|
+
))
|
|
148
|
+
ctx.put(SLOT_OBSERVATIONS, observations)
|
|
127
149
|
ctx.put(SLOT_CONFIDENCE, "high" if integrations else "medium")
|
|
128
150
|
_add_service(ctx, "SemanticIntegrationEngine")
|
|
129
151
|
|
sourcecode/security_posture.py
CHANGED
|
@@ -228,14 +228,18 @@ def _collect_usage(
|
|
|
228
228
|
cir: "CanonicalRepositoryIR",
|
|
229
229
|
) -> tuple[dict[str, _AnnotationUsage], int, int, set[str]]:
|
|
230
230
|
"""Scan existing per-symbol annotations. Returns (usage_by_simple,
|
|
231
|
-
|
|
231
|
+
handler_declarations, non_handler_declarations, handler_symbols).
|
|
232
232
|
|
|
233
233
|
Controller-handler methods = the handler symbols of `cir.endpoints`; this is
|
|
234
234
|
the authoritative endpoint surface (already structurally derived by the IR).
|
|
235
|
+
|
|
236
|
+
The two integers count **method declarations**, not distinct symbols: overloaded
|
|
237
|
+
methods share one FQN and are separate annotation sites, so on a real monolith 75
|
|
238
|
+
distinct handler symbols produced 90 declarations. Coverage below is measured
|
|
239
|
+
against declarations because that is what was scanned — the payload says so rather
|
|
240
|
+
than letting a reader assume it was endpoints (3574) or distinct methods (75).
|
|
235
241
|
"""
|
|
236
|
-
handler_symbols: set[str] =
|
|
237
|
-
ep.handler_symbol for ep in cir.endpoints if getattr(ep, "handler_symbol", "")
|
|
238
|
-
}
|
|
242
|
+
handler_symbols: set[str] = set(_handler_symbols(cir))
|
|
239
243
|
controller_classes: set[str] = {
|
|
240
244
|
ep.controller_class for ep in cir.endpoints if getattr(ep, "controller_class", "")
|
|
241
245
|
}
|
|
@@ -389,6 +393,9 @@ def _detect_custom_gates(
|
|
|
389
393
|
usage, total_handler, total_non_handler, _ = _collect_usage(cir)
|
|
390
394
|
if total_handler == 0:
|
|
391
395
|
return []
|
|
396
|
+
_population = endpoint_population(cir)
|
|
397
|
+
population_handler_methods = _population["handler_methods"]
|
|
398
|
+
population_endpoints = _population["endpoints"]
|
|
392
399
|
|
|
393
400
|
verdicts: list[Verdict] = []
|
|
394
401
|
for simple, u in sorted(usage.items()):
|
|
@@ -415,9 +422,22 @@ def _detect_custom_gates(
|
|
|
415
422
|
# concrete name lives ONLY here, as evidence (VAI §0.5)
|
|
416
423
|
"annotation": u.token,
|
|
417
424
|
"coverage": round(coverage, 4),
|
|
418
|
-
"coverage_pct": f"{round(coverage * 100)}% of controller
|
|
425
|
+
"coverage_pct": f"{round(coverage * 100)}% of controller handler method declarations",
|
|
426
|
+
# The denominator is stated in its own unit: handler *methods*, not
|
|
427
|
+
# endpoints. A repo with 936 handler methods can expose several thousand
|
|
428
|
+
# endpoints, so this percentage must never be read against an endpoint
|
|
429
|
+
# count (ADR-0008 — the payload states the unit, the reader does not guess).
|
|
430
|
+
"denominator": "controller handler method declarations",
|
|
419
431
|
"handler_hits": u.handler_hits,
|
|
420
|
-
|
|
432
|
+
# What the percentage was actually measured against: annotation sites on
|
|
433
|
+
# handler methods. Overloads share an FQN, so this is ≥ the distinct-method
|
|
434
|
+
# count and unrelated to the endpoint count.
|
|
435
|
+
"handler_declarations_total": total_handler,
|
|
436
|
+
# The other two units, stated beside it so no reader has to guess which one
|
|
437
|
+
# a percentage used — the exact confusion this payload caused in the field.
|
|
438
|
+
"handler_methods_total": population_handler_methods,
|
|
439
|
+
"endpoints_total": population_endpoints,
|
|
440
|
+
"handler_total": total_handler, # deprecated alias of handler_declarations_total
|
|
421
441
|
"non_handler_hits": u.non_handler_hits,
|
|
422
442
|
"specificity_ratio": (
|
|
423
443
|
round(handler_rate / non_handler_rate, 2)
|
|
@@ -489,6 +509,106 @@ def gate_bearing_handlers(
|
|
|
489
509
|
_GATE_COVERAGE_LIST_CAP = 200
|
|
490
510
|
|
|
491
511
|
|
|
512
|
+
def _handler_symbols(cir: "CanonicalRepositoryIR") -> set[str]:
|
|
513
|
+
"""The distinct handler-method symbols behind the endpoint surface — the one place
|
|
514
|
+
this set is derived, so a count of it means the same thing everywhere."""
|
|
515
|
+
return {ep.handler_symbol for ep in cir.endpoints if getattr(ep, "handler_symbol", "")}
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def endpoint_population(cir: "CanonicalRepositoryIR") -> dict:
|
|
519
|
+
"""THE population every posture partition is measured against (ADR-0008 R1).
|
|
520
|
+
|
|
521
|
+
Two units live in this projection and they are not interchangeable: a repository
|
|
522
|
+
has more *endpoints* than *handler methods*, because one method can answer several
|
|
523
|
+
mappings. Reporting a count of one next to a count of the other under names that
|
|
524
|
+
both read as "handlers" is how a payload came to show 698 + 2635 against a total of
|
|
525
|
+
3574 — three figures, no stated unit, and no way for a reader to tell which
|
|
526
|
+
denominator any percentage used.
|
|
527
|
+
|
|
528
|
+
Every count derived from either population MUST come from here and MUST say which
|
|
529
|
+
one it is.
|
|
530
|
+
"""
|
|
531
|
+
return {
|
|
532
|
+
"endpoints": len(cir.endpoints),
|
|
533
|
+
"handler_methods": len(_handler_symbols(cir)),
|
|
534
|
+
"note": (
|
|
535
|
+
"Two units. 'endpoints' counts mapped routes (the denominator for every "
|
|
536
|
+
"endpoint partition below); 'handler_methods' counts the distinct methods "
|
|
537
|
+
"serving them. One method can serve several endpoints — measured on a real "
|
|
538
|
+
"monolith, 220 endpoints came from 75 methods — so the two never have to "
|
|
539
|
+
"match, and a figure is only comparable to others in its own unit."
|
|
540
|
+
),
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def unknown_endpoint_surface(reason: str, declared_policy: str = "") -> dict:
|
|
545
|
+
"""The honest answer for an endpoint this authority cannot speak about.
|
|
546
|
+
|
|
547
|
+
Absence of a posture entry is absence of evidence: it is `coverage_unknown`
|
|
548
|
+
with the reason, never a coverage claim (ADR-0008 R9). The declared per-method
|
|
549
|
+
policy, if known, travels along as evidence — it is not the answer.
|
|
550
|
+
"""
|
|
551
|
+
return {
|
|
552
|
+
"verdict": "coverage_unknown",
|
|
553
|
+
"confidence": Confidence.UNKNOWN.value,
|
|
554
|
+
"declared_policy": declared_policy or "unknown",
|
|
555
|
+
"reason": reason,
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
def endpoint_security_surface(
|
|
560
|
+
cir: "CanonicalRepositoryIR",
|
|
561
|
+
*,
|
|
562
|
+
root: Optional[Path] = None,
|
|
563
|
+
posture: Optional[Any] = None,
|
|
564
|
+
) -> dict[str, dict]:
|
|
565
|
+
"""THE per-endpoint security answer, keyed by ``endpoint_id`` (ADR-0008 R1).
|
|
566
|
+
|
|
567
|
+
One question — "what is known about the security of this endpoint?" — had two
|
|
568
|
+
producers: this module's four-way verdict, and `impact-chain`, which printed the
|
|
569
|
+
raw per-method policy under the name `security_policy`. Measured on Keycloak
|
|
570
|
+
(2026-07-28): the posture projection called 406 endpoints ``protected_custom``,
|
|
571
|
+
and **324 of those carry the declared policy ``none_detected``** because their
|
|
572
|
+
gate is a bespoke annotation with no standard vocabulary. `impact-chain` reported
|
|
573
|
+
exactly that string for the same `endpoint_id` — so one run of one tool answered
|
|
574
|
+
"guarded by a custom mechanism (Likely)" and "no security detected" about
|
|
575
|
+
``DELETE /admin/realms/{realm}``. The declared policy is a fact about an
|
|
576
|
+
annotation; it was never an answer about coverage.
|
|
577
|
+
|
|
578
|
+
Consumers bind here instead of re-deriving. ``posture`` accepts an already-computed
|
|
579
|
+
:class:`SecurityPostureResult` or its ``to_dict()`` form, so a caller that has run
|
|
580
|
+
the security audit reuses that derivation rather than paying for a second one.
|
|
581
|
+
|
|
582
|
+
Never raises: any structural gap yields :func:`unknown_endpoint_surface`.
|
|
583
|
+
"""
|
|
584
|
+
if posture is None:
|
|
585
|
+
try:
|
|
586
|
+
posture = infer_security_posture(cir, root=root)
|
|
587
|
+
except Exception:
|
|
588
|
+
return {}
|
|
589
|
+
|
|
590
|
+
verdicts: Any
|
|
591
|
+
if isinstance(posture, dict):
|
|
592
|
+
verdicts = posture.get("endpoints") or []
|
|
593
|
+
else:
|
|
594
|
+
verdicts = getattr(posture, "endpoint_verdicts", None) or []
|
|
595
|
+
|
|
596
|
+
out: dict[str, dict] = {}
|
|
597
|
+
for v in verdicts:
|
|
598
|
+
if not isinstance(v, dict):
|
|
599
|
+
continue
|
|
600
|
+
ep_id = v.get("endpoint_id") or ""
|
|
601
|
+
if not ep_id:
|
|
602
|
+
continue
|
|
603
|
+
out[ep_id] = {
|
|
604
|
+
"verdict": v.get("verdict") or "coverage_unknown",
|
|
605
|
+
"confidence": v.get("confidence") or Confidence.UNKNOWN.value,
|
|
606
|
+
# Evidence, not answer: the guard vocabulary declared on the handler.
|
|
607
|
+
"declared_policy": v.get("policy") or "unknown",
|
|
608
|
+
}
|
|
609
|
+
return out
|
|
610
|
+
|
|
611
|
+
|
|
492
612
|
@dataclass
|
|
493
613
|
class SecurityPostureResult:
|
|
494
614
|
"""Repo-level security-posture projection (P1-A)."""
|
|
@@ -499,6 +619,9 @@ class SecurityPostureResult:
|
|
|
499
619
|
endpoint_verdicts: list[dict] = field(default_factory=list)
|
|
500
620
|
rollup: dict = field(default_factory=dict)
|
|
501
621
|
limitations: list[str] = field(default_factory=list)
|
|
622
|
+
# The population every partition in this projection is measured against, stated
|
|
623
|
+
# once so a reader never has to infer a denominator from a percentage.
|
|
624
|
+
population: dict = field(default_factory=dict)
|
|
502
625
|
# Gate-coverage projection (P1-A follow-up): when a custom gate IS the detected
|
|
503
626
|
# mechanism, which controller handlers do NOT carry it and are not standard-guarded
|
|
504
627
|
# — the residue that relies solely on a centralized filter, if one exists. None when
|
|
@@ -522,6 +645,7 @@ class SecurityPostureResult:
|
|
|
522
645
|
self.interception.to_dict() if self.interception else None
|
|
523
646
|
),
|
|
524
647
|
"endpoints": self.endpoint_verdicts,
|
|
648
|
+
"population": dict(self.population),
|
|
525
649
|
"rollup": self.rollup,
|
|
526
650
|
**({"gate_coverage": self.gate_coverage} if self.gate_coverage else {}),
|
|
527
651
|
"limitations": list(self.limitations),
|
|
@@ -552,6 +676,7 @@ def infer_security_posture(
|
|
|
552
676
|
Never raises; never emits a categorical "unsecured" verdict.
|
|
553
677
|
"""
|
|
554
678
|
security_model = str(cir.metadata.get("security_model", "unknown"))
|
|
679
|
+
population = endpoint_population(cir)
|
|
555
680
|
|
|
556
681
|
# (0) config: names never drive detection — only upgrade confidence.
|
|
557
682
|
configured: dict[str, Any] = {}
|
|
@@ -588,6 +713,11 @@ def infer_security_posture(
|
|
|
588
713
|
# escape set: handlers that do NOT carry the detected custom gate and have no
|
|
589
714
|
# standard guard of their own — the residue that relies solely on a filter.
|
|
590
715
|
escaping: list[dict] = []
|
|
716
|
+
# Three-way partition of the SAME population (endpoints), counted in the one loop
|
|
717
|
+
# that visits every endpoint exactly once, so the parts sum to the whole by
|
|
718
|
+
# construction rather than by three producers agreeing after the fact.
|
|
719
|
+
n_gated = 0 # carries the detected custom gate
|
|
720
|
+
n_standard_only = 0 # no custom gate, but a standard guard of its own
|
|
591
721
|
|
|
592
722
|
for ep in cir.endpoints:
|
|
593
723
|
policy = ep.security.policy if ep.security is not None else "none_detected"
|
|
@@ -640,12 +770,12 @@ def infer_security_posture(
|
|
|
640
770
|
|
|
641
771
|
# escape set (only meaningful when a custom gate is the mechanism): a handler
|
|
642
772
|
# that neither carries the gate nor a standard guard nor a config-custom policy.
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
):
|
|
773
|
+
_gated = carries_gate or policy == "custom"
|
|
774
|
+
if _gated:
|
|
775
|
+
n_gated += 1
|
|
776
|
+
elif _is_standard_guard_policy(policy):
|
|
777
|
+
n_standard_only += 1
|
|
778
|
+
if has_custom_gate and not _gated and not _is_standard_guard_policy(policy):
|
|
649
779
|
escaping.append({
|
|
650
780
|
"endpoint_id": ep.id,
|
|
651
781
|
"method": ep.method,
|
|
@@ -692,13 +822,26 @@ def infer_security_posture(
|
|
|
692
822
|
else:
|
|
693
823
|
no_pattern += 1
|
|
694
824
|
|
|
695
|
-
total_handlers =
|
|
825
|
+
total_handlers = population["endpoints"]
|
|
696
826
|
not_covered = len(escaping)
|
|
697
827
|
_patterns = (
|
|
698
828
|
filter_surface.patterns if filter_surface and not filter_surface.is_empty() else []
|
|
699
829
|
)
|
|
700
830
|
gate_coverage = {
|
|
701
831
|
"gate_annotations": sorted(gate_names),
|
|
832
|
+
# ── the endpoint partition: three parts, one population, they sum ──────
|
|
833
|
+
"population": "endpoints",
|
|
834
|
+
"endpoints_total": population["endpoints"],
|
|
835
|
+
"endpoints_carrying_gate": n_gated,
|
|
836
|
+
"endpoints_standard_guarded": n_standard_only,
|
|
837
|
+
"endpoints_not_carrying_gate": not_covered,
|
|
838
|
+
# ── a different unit, named as one ────────────────────────────────────
|
|
839
|
+
"handler_methods_total": population["handler_methods"],
|
|
840
|
+
"handler_methods_carrying_gate": len(gate_bearers),
|
|
841
|
+
# ── deprecated aliases (two-minor window) ─────────────────────────────
|
|
842
|
+
# `total_controller_handlers` always counted endpoints, and `gate_bearing`
|
|
843
|
+
# counted handler methods, so the two never belonged in one sum. Kept so no
|
|
844
|
+
# consumer breaks; read the explicit keys above.
|
|
702
845
|
"total_controller_handlers": total_handlers,
|
|
703
846
|
"gate_bearing": len(gate_bearers),
|
|
704
847
|
"not_carrying_gate": not_covered,
|
|
@@ -713,12 +856,14 @@ def infer_security_posture(
|
|
|
713
856
|
escaping, key=lambda d: (d["path"] or "", d["method"] or "")
|
|
714
857
|
)[:_GATE_COVERAGE_LIST_CAP],
|
|
715
858
|
"note": (
|
|
716
|
-
"
|
|
717
|
-
"guard. 'filter_pattern_match' marks those a declarative servlet
|
|
718
|
-
"(@WebFilter / web.xml) pattern covers — still review, pattern
|
|
719
|
-
"not proof of enforcement. Spring FilterRegistrationBean /
|
|
720
|
-
"/ AspectJ pointcuts are NOT reconstructed, so a missing
|
|
721
|
-
"of exposure."
|
|
859
|
+
"Endpoints whose handler does not carry the detected custom gate and has no "
|
|
860
|
+
"standard guard. 'filter_pattern_match' marks those a declarative servlet "
|
|
861
|
+
"filter (@WebFilter / web.xml) pattern covers — still review, pattern "
|
|
862
|
+
"presence is not proof of enforcement. Spring FilterRegistrationBean / "
|
|
863
|
+
"HttpSecurity DSL / AspectJ pointcuts are NOT reconstructed, so a missing "
|
|
864
|
+
"match is not proof of exposure. Counts marked endpoints_* partition the "
|
|
865
|
+
"endpoint population and sum to endpoints_total; handler_methods_* count a "
|
|
866
|
+
"different unit and are not comparable to them."
|
|
722
867
|
),
|
|
723
868
|
"truncated": not_covered > _GATE_COVERAGE_LIST_CAP,
|
|
724
869
|
}
|
|
@@ -730,6 +875,7 @@ def infer_security_posture(
|
|
|
730
875
|
endpoint_verdicts=verdicts,
|
|
731
876
|
rollup=rollup,
|
|
732
877
|
limitations=limitations,
|
|
878
|
+
population=population,
|
|
733
879
|
gate_coverage=gate_coverage,
|
|
734
880
|
)
|
|
735
881
|
# keep reference to suppress-set so callers (SEC-001) can dedupe false alarms
|
|
@@ -288,9 +288,23 @@ class SemanticIntegrationEngine:
|
|
|
288
288
|
"runtime/DI-wired clients remain out of static scope."
|
|
289
289
|
)
|
|
290
290
|
|
|
291
|
-
def as_report(
|
|
291
|
+
def as_report(
|
|
292
|
+
self,
|
|
293
|
+
n_files: Optional[int] = None,
|
|
294
|
+
*,
|
|
295
|
+
declared: "Optional[list]" = None,
|
|
296
|
+
) -> dict:
|
|
292
297
|
"""Same output contract as integration_detector.detect_integrations, so the
|
|
293
|
-
`export --integrations` / C4 external_systems consumer is unchanged.
|
|
298
|
+
`export --integrations` / C4 external_systems consumer is unchanged.
|
|
299
|
+
|
|
300
|
+
``declared`` — the build's declared client libraries from the coordinate
|
|
301
|
+
authority (:mod:`sourcecode.integration_coordinates`). They are reported in
|
|
302
|
+
their own block and never counted as integrations: a coordinate says the build
|
|
303
|
+
declares a client, not that the code uses one. Passing them is what stops this
|
|
304
|
+
report from answering `count: 0` in a document whose stack block lists a client
|
|
305
|
+
library for the same external system (ADR-0008 R12/R14) — a contradiction that
|
|
306
|
+
survived two releases on the LDAP surface.
|
|
307
|
+
"""
|
|
294
308
|
recs = self.detect()
|
|
295
309
|
by_kind: dict[str, int] = {}
|
|
296
310
|
for r in recs:
|
|
@@ -306,11 +320,49 @@ class SemanticIntegrationEngine:
|
|
|
306
320
|
if r.confidence is not None:
|
|
307
321
|
rec["confidence"] = r.confidence
|
|
308
322
|
integrations.append(rec)
|
|
323
|
+
|
|
324
|
+
declared_list = list(declared or [])
|
|
325
|
+
observed_kinds = set(by_kind)
|
|
326
|
+
declared_block = [
|
|
327
|
+
{**d.to_dict(), "observed_in_source": d.kind in observed_kinds}
|
|
328
|
+
for d in declared_list
|
|
329
|
+
]
|
|
330
|
+
declared_not_observed = sorted(
|
|
331
|
+
{d.kind for d in declared_list} - observed_kinds
|
|
332
|
+
)
|
|
333
|
+
if declared_not_observed:
|
|
334
|
+
_cites = "; ".join(
|
|
335
|
+
f"{d.kind} ({d.coordinate} at {d.evidence})"
|
|
336
|
+
for d in declared_list if d.kind in set(declared_not_observed)
|
|
337
|
+
)
|
|
338
|
+
coverage_reason = (
|
|
339
|
+
f"{coverage_reason} The build DECLARES a client library for "
|
|
340
|
+
f"{len(declared_not_observed)} kind(s) with no client construct in "
|
|
341
|
+
f"source — {_cites}. A configuration-wired client (properties, YAML, "
|
|
342
|
+
"XML, or a starter's auto-configuration) is not statically visible as a "
|
|
343
|
+
"construct, so read these as declared-not-observed, neither as an "
|
|
344
|
+
"integration record nor as their absence."
|
|
345
|
+
)
|
|
346
|
+
|
|
309
347
|
return {
|
|
310
348
|
"integrations": integrations,
|
|
311
349
|
"by_kind": {k: by_kind[k] for k in sorted(by_kind)},
|
|
312
350
|
"count": len(recs),
|
|
313
351
|
"confidence": "observed" if recs else "not_analyzed",
|
|
352
|
+
# Declared ≠ observed. Two facts about the same external system, each under
|
|
353
|
+
# its own name and its own evidence, so no reader has to reconcile a count of
|
|
354
|
+
# 0 here against a client library named in the stack block.
|
|
355
|
+
"declared_clients": declared_block,
|
|
356
|
+
"declared_clients_count": len(declared_block),
|
|
357
|
+
"kinds_declared_not_observed": declared_not_observed,
|
|
358
|
+
"declared_clients_note": (
|
|
359
|
+
"Client libraries the BUILD declares (Maven/Gradle coordinates, "
|
|
360
|
+
"test/provided scope excluded). Evidence of a declared client, never of "
|
|
361
|
+
"an observed integration: a coordinate does not prove the code uses it, "
|
|
362
|
+
"and its absence from `integrations` does not prove the system does not "
|
|
363
|
+
"talk to that party — a properties/YAML/XML-wired client has no construct "
|
|
364
|
+
"to find."
|
|
365
|
+
),
|
|
314
366
|
"coverage_confidence": coverage_confidence,
|
|
315
367
|
"coverage_confidence_reason": coverage_reason,
|
|
316
368
|
"coverage_note": (
|
sourcecode/spring_impact.py
CHANGED
|
@@ -96,7 +96,13 @@ class AffectedEndpoint:
|
|
|
96
96
|
controller_class: str
|
|
97
97
|
handler_symbol: str
|
|
98
98
|
source_file: str
|
|
99
|
-
|
|
99
|
+
# Guard vocabulary DECLARED on the handler ("spring_pre_authorize",
|
|
100
|
+
# "none_detected", "unknown"). Evidence about an annotation — never a statement
|
|
101
|
+
# about whether the route is covered. The coverage answer is `security_verdict`,
|
|
102
|
+
# bound from the one authority (security_posture.endpoint_security_surface).
|
|
103
|
+
security_policy: str
|
|
104
|
+
security_verdict: str = "coverage_unknown"
|
|
105
|
+
security_confidence: str = "Unknown"
|
|
100
106
|
|
|
101
107
|
def to_dict(self) -> dict:
|
|
102
108
|
return {
|
|
@@ -106,6 +112,9 @@ class AffectedEndpoint:
|
|
|
106
112
|
"controller_class": self.controller_class,
|
|
107
113
|
"handler_symbol": self.handler_symbol,
|
|
108
114
|
"source_file": self.source_file,
|
|
115
|
+
"security_verdict": self.security_verdict,
|
|
116
|
+
"security_confidence": self.security_confidence,
|
|
117
|
+
# kept for consumers reading the declared annotation; see the field note
|
|
109
118
|
"security_policy": self.security_policy,
|
|
110
119
|
}
|
|
111
120
|
|
|
@@ -863,11 +872,43 @@ def _build_chain_explanation(
|
|
|
863
872
|
# Security surface aggregation
|
|
864
873
|
# ---------------------------------------------------------------------------
|
|
865
874
|
|
|
875
|
+
def _bind_security_surface(
|
|
876
|
+
endpoints_affected: list[AffectedEndpoint],
|
|
877
|
+
surface: dict[str, dict],
|
|
878
|
+
*,
|
|
879
|
+
unavailable_reason: str = "",
|
|
880
|
+
) -> None:
|
|
881
|
+
"""Bind each affected endpoint to the one security authority, in place.
|
|
882
|
+
|
|
883
|
+
An endpoint the authority does not speak about (or an authority that could not
|
|
884
|
+
be built) is `coverage_unknown` with its reason — never the declared policy
|
|
885
|
+
reused as a coverage claim (ADR-0008 R9).
|
|
886
|
+
"""
|
|
887
|
+
from sourcecode.security_posture import unknown_endpoint_surface
|
|
888
|
+
|
|
889
|
+
for ep in endpoints_affected:
|
|
890
|
+
answer = surface.get(ep.endpoint_id)
|
|
891
|
+
if answer is None:
|
|
892
|
+
answer = unknown_endpoint_surface(
|
|
893
|
+
unavailable_reason
|
|
894
|
+
or "endpoint is not present in the security-posture projection",
|
|
895
|
+
declared_policy=ep.security_policy,
|
|
896
|
+
)
|
|
897
|
+
ep.security_verdict = answer.get("verdict", "coverage_unknown")
|
|
898
|
+
ep.security_confidence = answer.get("confidence", "Unknown")
|
|
899
|
+
|
|
900
|
+
|
|
866
901
|
def _build_security_surfaces(
|
|
867
902
|
endpoints_affected: list[AffectedEndpoint],
|
|
868
903
|
impact_findings: list[SpringFinding],
|
|
869
904
|
) -> list[dict]:
|
|
870
|
-
"""Per-endpoint security surface with associated finding IDs.
|
|
905
|
+
"""Per-endpoint security surface with associated finding IDs.
|
|
906
|
+
|
|
907
|
+
The surface reports the authority's verdict (`security_posture`); the declared
|
|
908
|
+
annotation policy travels with it as evidence. Emitting the declared policy
|
|
909
|
+
alone is what made this command answer "none_detected" for 324 Keycloak
|
|
910
|
+
endpoints the same run classified `protected_custom`.
|
|
911
|
+
"""
|
|
871
912
|
finding_by_ep: dict[str, list[str]] = {}
|
|
872
913
|
for f in impact_findings:
|
|
873
914
|
if f.category == "security":
|
|
@@ -881,6 +922,11 @@ def _build_security_surfaces(
|
|
|
881
922
|
"endpoint_id": ep.endpoint_id,
|
|
882
923
|
"method": ep.method,
|
|
883
924
|
"path": ep.path,
|
|
925
|
+
"verdict": ep.security_verdict,
|
|
926
|
+
"confidence": ep.security_confidence,
|
|
927
|
+
"declared_policy": ep.security_policy,
|
|
928
|
+
# deprecated alias of `declared_policy` (two-minor window): it always
|
|
929
|
+
# carried the declared annotation, never a coverage verdict.
|
|
884
930
|
"security_policy": ep.security_policy,
|
|
885
931
|
"security_findings": finding_by_ep.get(ep.endpoint_id, []),
|
|
886
932
|
})
|
|
@@ -1102,10 +1148,13 @@ class ImpactOrchestrator:
|
|
|
1102
1148
|
pass
|
|
1103
1149
|
|
|
1104
1150
|
# ── 5. TX + SEC audit findings, filtered to call chain ────────────
|
|
1151
|
+
# The security audit already derives the posture projection; take it from
|
|
1152
|
+
# there rather than deriving the same answer a second time (ADR-0008 R1).
|
|
1153
|
+
posture_dict: dict = {}
|
|
1105
1154
|
if prebuilt_findings is not None:
|
|
1106
1155
|
all_findings = prebuilt_findings
|
|
1107
1156
|
else:
|
|
1108
|
-
all_findings = _run_audit_for_chain(cir, model, root)
|
|
1157
|
+
all_findings, posture_dict = _run_audit_for_chain(cir, model, root)
|
|
1109
1158
|
|
|
1110
1159
|
impact_findings_raw = _filter_findings(
|
|
1111
1160
|
all_findings, seed_fqns, direct_callers_raw, indirect_callers_raw,
|
|
@@ -1118,6 +1167,18 @@ class ImpactOrchestrator:
|
|
|
1118
1167
|
impact_findings = [f.to_dict() for f in impact_findings_raw]
|
|
1119
1168
|
|
|
1120
1169
|
# ── 6. Security surfaces ──────────────────────────────────────────
|
|
1170
|
+
security_surface_index, surface_gap = _endpoint_security_authority(
|
|
1171
|
+
cir, root, posture_dict,
|
|
1172
|
+
)
|
|
1173
|
+
# The gap is only worth a warning when it actually withheld an answer:
|
|
1174
|
+
# a repo with no affected endpoints is not missing a security surface.
|
|
1175
|
+
surface_gap_warned = False
|
|
1176
|
+
if surface_gap and endpoints_affected:
|
|
1177
|
+
warnings.append(surface_gap)
|
|
1178
|
+
surface_gap_warned = True
|
|
1179
|
+
_bind_security_surface(
|
|
1180
|
+
endpoints_affected, security_surface_index, unavailable_reason=surface_gap,
|
|
1181
|
+
)
|
|
1121
1182
|
security_surfaces = _build_security_surfaces(endpoints_affected, impact_findings_raw)
|
|
1122
1183
|
|
|
1123
1184
|
# ── 7. Risk ───────────────────────────────────────────────────────
|
|
@@ -1176,6 +1237,13 @@ class ImpactOrchestrator:
|
|
|
1176
1237
|
key=lambda f: (SEVERITY_ORDER.get(f.severity, 9), f.symbol)
|
|
1177
1238
|
)
|
|
1178
1239
|
impact_findings = [f.to_dict() for f in impact_findings_raw]
|
|
1240
|
+
_bind_security_surface(
|
|
1241
|
+
endpoints_affected, security_surface_index,
|
|
1242
|
+
unavailable_reason=surface_gap,
|
|
1243
|
+
)
|
|
1244
|
+
if surface_gap and endpoints_affected and not surface_gap_warned:
|
|
1245
|
+
warnings.append(surface_gap)
|
|
1246
|
+
surface_gap_warned = True
|
|
1179
1247
|
security_surfaces = _build_security_surfaces(endpoints_affected, impact_findings_raw)
|
|
1180
1248
|
risk_level, risk_score, risk_score_raw = _compute_risk(
|
|
1181
1249
|
len(direct_callers), len(indirect_callers),
|
|
@@ -1456,16 +1524,21 @@ def _run_audit_for_chain(
|
|
|
1456
1524
|
cir: CanonicalRepositoryIR,
|
|
1457
1525
|
model: SpringSemanticModel,
|
|
1458
1526
|
root: Optional[Path],
|
|
1459
|
-
) -> list[SpringFinding]:
|
|
1460
|
-
"""Run TX + SEC audit
|
|
1527
|
+
) -> tuple[list[SpringFinding], dict]:
|
|
1528
|
+
"""Run TX + SEC audit; return (combined findings, security-posture projection).
|
|
1529
|
+
|
|
1530
|
+
The posture travels out with the findings because the security audit already
|
|
1531
|
+
derives it — the impact chain binds to that derivation instead of computing a
|
|
1532
|
+
second answer to the same question (ADR-0008 R1).
|
|
1461
1533
|
|
|
1462
1534
|
Uses the pre-built model — no duplicate CIR traversal.
|
|
1463
|
-
Never raises; returns [] on any error.
|
|
1535
|
+
Never raises; returns ([], {}) on any error.
|
|
1464
1536
|
"""
|
|
1465
1537
|
from sourcecode.spring_security_audit import run_security_audit
|
|
1466
1538
|
from sourcecode.spring_tx_analyzer import run_tx_audit
|
|
1467
1539
|
|
|
1468
1540
|
findings: list[SpringFinding] = []
|
|
1541
|
+
posture: dict = {}
|
|
1469
1542
|
try:
|
|
1470
1543
|
tx_result = run_tx_audit(cir, root=root, model=model)
|
|
1471
1544
|
findings.extend(tx_result.findings)
|
|
@@ -1474,9 +1547,53 @@ def _run_audit_for_chain(
|
|
|
1474
1547
|
try:
|
|
1475
1548
|
sec_result = run_security_audit(cir, root=root, model=model)
|
|
1476
1549
|
findings.extend(sec_result.findings)
|
|
1550
|
+
posture = getattr(sec_result, "security_posture", None) or {}
|
|
1551
|
+
except Exception:
|
|
1552
|
+
pass
|
|
1553
|
+
return findings, posture
|
|
1554
|
+
|
|
1555
|
+
|
|
1556
|
+
# Per-CIR memo of the endpoint security authority's answer (one derivation per repo).
|
|
1557
|
+
_SURFACE_CACHE_ATTR = "_impact_endpoint_security_surface"
|
|
1558
|
+
|
|
1559
|
+
|
|
1560
|
+
def _endpoint_security_authority(
|
|
1561
|
+
cir: CanonicalRepositoryIR,
|
|
1562
|
+
root: Optional[Path],
|
|
1563
|
+
posture: dict,
|
|
1564
|
+
) -> tuple[dict[str, dict], str]:
|
|
1565
|
+
"""(endpoint_id → security answer, reason it is unavailable).
|
|
1566
|
+
|
|
1567
|
+
Reuses ``posture`` when the audit already produced it; otherwise asks the
|
|
1568
|
+
authority directly (the `prebuilt_findings` path, where no audit ran here).
|
|
1569
|
+
Memoized on the CIR because `pr-impact` issues one query per changed class
|
|
1570
|
+
against the same repository — reuse is what keeps "one authority" from meaning
|
|
1571
|
+
"one full projection per query".
|
|
1572
|
+
"""
|
|
1573
|
+
from sourcecode.security_posture import endpoint_security_surface
|
|
1574
|
+
|
|
1575
|
+
cached = getattr(cir, _SURFACE_CACHE_ATTR, None)
|
|
1576
|
+
if cached is not None:
|
|
1577
|
+
return cached
|
|
1578
|
+
|
|
1579
|
+
try:
|
|
1580
|
+
surface = endpoint_security_surface(
|
|
1581
|
+
cir, root=root, posture=posture or None,
|
|
1582
|
+
)
|
|
1583
|
+
except Exception:
|
|
1584
|
+
surface = {}
|
|
1585
|
+
if not surface:
|
|
1586
|
+
answer = ({}, (
|
|
1587
|
+
"security-posture projection unavailable for this repository; every "
|
|
1588
|
+
"endpoint security answer is coverage_unknown (absence of evidence)"
|
|
1589
|
+
))
|
|
1590
|
+
else:
|
|
1591
|
+
answer = (surface, "")
|
|
1592
|
+
try:
|
|
1593
|
+
setattr(cir, _SURFACE_CACHE_ATTR, answer)
|
|
1477
1594
|
except Exception:
|
|
1478
1595
|
pass
|
|
1479
|
-
return
|
|
1596
|
+
return answer
|
|
1480
1597
|
|
|
1481
1598
|
|
|
1482
1599
|
# ---------------------------------------------------------------------------
|
sourcecode/spring_semantic.py
CHANGED
|
@@ -16,7 +16,7 @@ from __future__ import annotations
|
|
|
16
16
|
import re
|
|
17
17
|
import time
|
|
18
18
|
from dataclasses import dataclass, field
|
|
19
|
-
from typing import TYPE_CHECKING, Optional
|
|
19
|
+
from typing import TYPE_CHECKING, Iterable, Optional
|
|
20
20
|
|
|
21
21
|
if TYPE_CHECKING:
|
|
22
22
|
from sourcecode.canonical_ir import CanonicalRepositoryIR
|
|
@@ -295,12 +295,24 @@ def build_tx_index(cir: "CanonicalRepositoryIR") -> TransactionBoundaryIndex:
|
|
|
295
295
|
3. Method-level boundaries override class-level for the same method.
|
|
296
296
|
4. Class-level boundaries are inherited by methods that lack their own.
|
|
297
297
|
"""
|
|
298
|
+
return build_tx_index_from_ir(
|
|
299
|
+
getattr(cir, "_raw_ir", {}) or {}, repo_id=getattr(cir, "cir_hash", "")[:16]
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def build_tx_index_from_ir(raw_ir: dict, repo_id: str = "") -> TransactionBoundaryIndex:
|
|
304
|
+
"""Same index, built straight from a repo IR dict.
|
|
305
|
+
|
|
306
|
+
THE authority for "where are the @Transactional boundaries" (ADR-0008 R1).
|
|
307
|
+
`build_tx_index` is the CanonicalRepositoryIR-shaped entry point; consumers that
|
|
308
|
+
hold the raw IR dict (blast radius) call this one instead of re-deriving
|
|
309
|
+
boundaries from graph node roles. Both produce the same index from the same nodes.
|
|
310
|
+
"""
|
|
298
311
|
t0 = time.monotonic()
|
|
299
|
-
index = TransactionBoundaryIndex(repo_id=
|
|
312
|
+
index = TransactionBoundaryIndex(repo_id=repo_id)
|
|
300
313
|
|
|
301
314
|
try:
|
|
302
|
-
|
|
303
|
-
graph = raw_ir.get("graph") or {}
|
|
315
|
+
graph = (raw_ir or {}).get("graph") or {}
|
|
304
316
|
nodes = graph.get("nodes") or []
|
|
305
317
|
|
|
306
318
|
# Pass 1: build meta-@Transactional map from annotation-type nodes.
|
|
@@ -378,3 +390,43 @@ def build_tx_index(cir: "CanonicalRepositoryIR") -> TransactionBoundaryIndex:
|
|
|
378
390
|
|
|
379
391
|
index.build_time_ms = round((time.monotonic() - t0) * 1000, 2)
|
|
380
392
|
return index
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def boundaries_declared_within(
|
|
396
|
+
index: TransactionBoundaryIndex, symbols: "Iterable[str]"
|
|
397
|
+
) -> list[TransactionBoundary]:
|
|
398
|
+
"""The @Transactional declaration sites carried by *symbols*, ordered by symbol.
|
|
399
|
+
|
|
400
|
+
THE authority for "which transaction boundaries belong to this set of symbols"
|
|
401
|
+
(ADR-0008 R1). Every consumer that reports transaction boundaries over a set of
|
|
402
|
+
symbols — a blast radius, a caller closure, a module — calls this. A consumer that
|
|
403
|
+
re-derives the answer from graph node roles, annotation strings or naming is in
|
|
404
|
+
violation of the ADR, and is how `impact` came to report 0 boundaries where
|
|
405
|
+
`retrieve transactions-reaching` reported 47 on the same symbol.
|
|
406
|
+
|
|
407
|
+
Membership follows Spring's own inheritance, in both directions and no further:
|
|
408
|
+
|
|
409
|
+
- the declaration site itself is in the set;
|
|
410
|
+
- a **class**-level boundary when a method of that class is in the set (the method
|
|
411
|
+
runs inside it);
|
|
412
|
+
- a **method**-level boundary when its class is in the set (asking about a class asks
|
|
413
|
+
about the methods it declares).
|
|
414
|
+
|
|
415
|
+
Asking about one method does *not* pull in a sibling method's own boundary — that
|
|
416
|
+
sibling is a different code path, and reporting it would be the kind of quietly
|
|
417
|
+
inflated count this authority exists to prevent.
|
|
418
|
+
"""
|
|
419
|
+
wanted = {s for s in symbols if s}
|
|
420
|
+
if not wanted:
|
|
421
|
+
return []
|
|
422
|
+
declaring_classes = {s.split("#", 1)[0] for s in wanted if "#" in s}
|
|
423
|
+
|
|
424
|
+
def _belongs(b: TransactionBoundary) -> bool:
|
|
425
|
+
if b.symbol in wanted:
|
|
426
|
+
return True
|
|
427
|
+
if b.scope == "class":
|
|
428
|
+
return b.symbol in declaring_classes
|
|
429
|
+
return _enclosing_class(b.symbol) in wanted
|
|
430
|
+
|
|
431
|
+
found = [b for b in index.all_declared if _belongs(b)]
|
|
432
|
+
return sorted(found, key=lambda b: (b.symbol, b.scope, b.source_file))
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
sourcecode/__init__.py,sha256=
|
|
1
|
+
sourcecode/__init__.py,sha256=4hwV87XY8dgmsv4RaWkZzBMSpB_T8EQ4A9_-xB5CzmI,308
|
|
2
2
|
sourcecode/adaptive_scanner.py,sha256=yJBKjNpkY6bpueYJ2YnRezen3sYZDecEt7WaaNWdqug,9466
|
|
3
3
|
sourcecode/archetype.py,sha256=qWMCyL2UzSGXJhCIZj38JjS7wI15EE3eILsWiaoCZXQ,35779
|
|
4
4
|
sourcecode/architectural_baseline.py,sha256=7QzJri4pbL3nzAn9gNutZW6mZR8HHD6H2C2H1EEwYPE,17904
|
|
@@ -14,7 +14,7 @@ sourcecode/chain_rules.py,sha256=1kLHv1t7g5rMmycIYSpm3az8SjCr01Xw7LtyV2tNhSA,106
|
|
|
14
14
|
sourcecode/change_plan.py,sha256=MNgNyu4zrLGvBBaXPwyCh-T2ZGaUQX3Hm4W87uuhZ3w,7750
|
|
15
15
|
sourcecode/cir_graphs.py,sha256=9G0HHj1kw2325IDyzo2OpX73BNswEckecf4MZUXB4JM,12078
|
|
16
16
|
sourcecode/classifier.py,sha256=JBzPwSSrDG-tUHAbcKB678HRbjLpD-ohzbzzO62mgpo,20114
|
|
17
|
-
sourcecode/cli.py,sha256=
|
|
17
|
+
sourcecode/cli.py,sha256=F7HXBOhTrzLppk81GDazz6sIvowkZomei1MCRXaTutc,400663
|
|
18
18
|
sourcecode/code_notes_analyzer.py,sha256=EJemNCNc9Dn-1RZYu-aNbK0ELzmsyC4s6FdHi3XyNEI,9392
|
|
19
19
|
sourcecode/compare.py,sha256=jdePg0dNCxFpysiTGMsROL5XABLjy_zVUgVeySvdb0o,7514
|
|
20
20
|
sourcecode/confidence_analyzer.py,sha256=vnbPI-20FnHdjO6STxHW8fbaxmB4A7y58io63ibFZjc,21586
|
|
@@ -48,6 +48,7 @@ sourcecode/git_analyzer.py,sha256=irvEwddbd5_tbA_eYUZebf4mH7_nFhpjwaWAV1f7jUY,15
|
|
|
48
48
|
sourcecode/graph_analyzer.py,sha256=lp0eB1PWC20BYF-GpPhAyegRpKrUKgOmXZIcZSIX_Ks,65777
|
|
49
49
|
sourcecode/graph_evidence.py,sha256=rENNsYRZeNstX_ExNCLlbHJAruFQwxo5d00x6wO3xwI,15030
|
|
50
50
|
sourcecode/hibernate_strat.py,sha256=5GmHiB865HxKdvaxeFV07ojglUgxyOP41AY2xiHfV30,61700
|
|
51
|
+
sourcecode/integration_coordinates.py,sha256=7vGFxN4tCn_KTisYFJuixa8XkyHBoasEFB3fqM62OBI,7407
|
|
51
52
|
sourcecode/jdk_exports.py,sha256=fCrlwNAXUT9gge_joq6kMnY3zJxYB2pxqy-0w3o3MJI,874
|
|
52
53
|
sourcecode/license.py,sha256=keFuwNxdAtvK2Ds91Wl79GMYxuxWYnN5Wbw1qBpaoUI,24896
|
|
53
54
|
sourcecode/mcp_nudge.py,sha256=lKemOqK_wny2u7Ymcr2Idi5Kx8pXY02jCi-_nJYLGMg,2992
|
|
@@ -71,26 +72,26 @@ sourcecode/redactor.py,sha256=SB4hwIvg8h-hvcqKcDWaZvA-aSyn-at-BIRwa0tUv5E,3227
|
|
|
71
72
|
sourcecode/relevance_scorer.py,sha256=0AgEt4KrV73nioMqBgjhGjtY7L2C7L7cSyKtj3IKcrw,9408
|
|
72
73
|
sourcecode/rename_refactor.py,sha256=h6dNFlB9aZ_3q6heeHBkgXQeXaT03nvPSsYH6P8qxFg,12965
|
|
73
74
|
sourcecode/repo_classifier.py,sha256=FG1vaWKdWXsWdl-S8hjVMiTqcwgaRXkDyvK4rPcOGtQ,22681
|
|
74
|
-
sourcecode/repository_ir.py,sha256=
|
|
75
|
+
sourcecode/repository_ir.py,sha256=2S14aUMY5vpZePObd3F87kz71KUsVXQllurXjufa6A0,344648
|
|
75
76
|
sourcecode/ris.py,sha256=Hw8TakTQ6hku-Abf2k8954NwkrH_sP73_8wVH8x5khc,22079
|
|
76
77
|
sourcecode/runtime_classifier.py,sha256=uTAD6BDCiBLUZEDRfqk718kM4RTT_vAbfkcOI2_Xx58,18432
|
|
77
78
|
sourcecode/scanner.py,sha256=z3CV0rcGunu0Y8mpNgp07wI7nxT0pxw1BkXRRtI0Rpo,9609
|
|
78
79
|
sourcecode/schema.py,sha256=aHNXDf8LGyUC8ZDE_VS9kiskC2-Oswhi_WnpdGy6HDw,24897
|
|
79
80
|
sourcecode/security_config.py,sha256=KblMEoRiEjrIE68YsPaUAFebxFp8UM7MS7lAk5CGD8U,3531
|
|
80
|
-
sourcecode/security_posture.py,sha256=
|
|
81
|
+
sourcecode/security_posture.py,sha256=0tKL_RafS0dYY5W__JTeJIfopd8f-DyKEerHrvA3-Mg,40243
|
|
81
82
|
sourcecode/semantic_analyzer.py,sha256=bpgdC6m0_ftVtRf3rSdwhbhWjnZnGxRXaZVcfe4BbcQ,95414
|
|
82
83
|
sourcecode/semantic_impact_engine.py,sha256=t09IirGC3JjQDy33JZd1_WKzQVKXkoNl3-XEUr5kjis,20563
|
|
83
|
-
sourcecode/semantic_integration_engine.py,sha256=
|
|
84
|
+
sourcecode/semantic_integration_engine.py,sha256=7a0WqAInOv39f0Yr_94TYo_JP_8QpeI9KGaknpzAyQU,18899
|
|
84
85
|
sourcecode/semantic_services.py,sha256=nbUuPv-F01USTt_9CHT8iy_ucCIw3fz4W3Aquea_pd4,10782
|
|
85
86
|
sourcecode/serializer.py,sha256=BAGL2J3G3bHvJS60OcxekPvTJg8x6b1LzeLKFbGaots,131497
|
|
86
87
|
sourcecode/spring_event_topology.py,sha256=5_ON_21Le5zbG-1GRc5GLIi5HJfy_QjcXLVPC5WeUGQ,18055
|
|
87
88
|
sourcecode/spring_findings.py,sha256=ICcR2qEZgYzWypZzsoaEmVeMYZ0YkuZ8AvTwA-rcVOs,7604
|
|
88
|
-
sourcecode/spring_impact.py,sha256=
|
|
89
|
+
sourcecode/spring_impact.py,sha256=Cm2KZplpQHjdjUC8jPX1SNnLHxOdM3HsUBNFGwgTP18,80145
|
|
89
90
|
sourcecode/spring_model.py,sha256=zOAgFmrRbG4a6KLm1TJl55aWMyPNsz3OS3FSczqPG6A,16594
|
|
90
91
|
sourcecode/spring_profiles.py,sha256=YPgBH3RuML-mtid7YwUMfA9sfSXPZwknYL7UvRMoYbE,7946
|
|
91
92
|
sourcecode/spring_properties.py,sha256=bQSOVrtj7YENsCjMQTTlVh-Gm_K0n0DdWzjPRUP38C4,8394
|
|
92
93
|
sourcecode/spring_security_audit.py,sha256=FteXv5GH-iE07GLEqPpoX7sVXf_dxRns8zxh9_m6By0,23015
|
|
93
|
-
sourcecode/spring_semantic.py,sha256=
|
|
94
|
+
sourcecode/spring_semantic.py,sha256=UrmLg_4gmBVmMqvSnUMgSReVEERVGv3X7LYDBEs6k_o,16598
|
|
94
95
|
sourcecode/spring_tx_analyzer.py,sha256=lp0h5Pzzd3fPxHAAagMmG8PqR1-v2uVpFIG1o7tCWQs,41148
|
|
95
96
|
sourcecode/summarizer.py,sha256=sr0-tfecFKCr-fSkPPWbl-t9HC7SY2ZxkjnnXX8DB2A,26621
|
|
96
97
|
sourcecode/token_estimate.py,sha256=ZP3C54aQLExuf32Yvrjok2TAS2oSSyNVRA_HjhsD6Eg,7165
|
|
@@ -112,7 +113,7 @@ sourcecode/detectors/elixir.py,sha256=jCpvt5Yi6jvplc80ovRtWh17q-11ZGo9qX7o8b57TJ
|
|
|
112
113
|
sourcecode/detectors/go.py,sha256=2r66uRQfeTWsqxr4HDhT6vExZErby0t46QXLHVBRv9w,2782
|
|
113
114
|
sourcecode/detectors/heuristic.py,sha256=7cRxrip4yIaggYzZJB6ef8yHKh-gHgiH_pXMFcjlyFU,3723
|
|
114
115
|
sourcecode/detectors/hybrid.py,sha256=IGFRUVsAZ1ooRlFdznCeJAV6vy1yVDx-VyghvLtddXc,9101
|
|
115
|
-
sourcecode/detectors/java.py,sha256=
|
|
116
|
+
sourcecode/detectors/java.py,sha256=v47yvL4mftcK35cVBpO7gru6O5AcEaKml-OoNyVU0Bg,45246
|
|
116
117
|
sourcecode/detectors/jvm_ext.py,sha256=EgHJ5W8EE-ZTN9V607mVzohyKgZE8Mc2jCi-DF8RAZU,2616
|
|
117
118
|
sourcecode/detectors/nodejs.py,sha256=Hg3Gmr7yIMJFiLoDwOTk2wtu00wxIs6kZf-oQujTFUA,13187
|
|
118
119
|
sourcecode/detectors/parsers.py,sha256=ug9K31tyHqinmv0HkIVQVjdTZpBv67FYKAEf52YXOSM,3178
|
|
@@ -126,9 +127,9 @@ sourcecode/detectors/terraform.py,sha256=cxORPR_zVLOJpHlh4e9JnFpkQsn_UnqMMom5yG6
|
|
|
126
127
|
sourcecode/detectors/tooling.py,sha256=8CKbtxwQoABP-WyBRNmdAmHDOvAH57AR1cF4UKuWEdQ,2074
|
|
127
128
|
sourcecode/mcp/__init__.py,sha256=XU4HfRGbdid8wdUA0x_4f7uKZD1z3mv_XUY_WU_T9Mw,179
|
|
128
129
|
sourcecode/mcp/orchestrator.py,sha256=diVoQgn24QmgPL3Ev8Sp6hsvh02OqY3MktHXOzrlodo,36525
|
|
129
|
-
sourcecode/mcp/registry.py,sha256=
|
|
130
|
+
sourcecode/mcp/registry.py,sha256=1qIcwtl01AGjEM3yMOwYgr_Ck3y-LZeRvh8i70Nhl_w,68258
|
|
130
131
|
sourcecode/mcp/runner.py,sha256=RGimCwhLyHxjCvH7q72y12HWEnATXZLC-NBWhkzlu0I,2852
|
|
131
|
-
sourcecode/mcp/server.py,sha256=
|
|
132
|
+
sourcecode/mcp/server.py,sha256=GH7bgJRVVkBJf-orj_Zmn_mEqMENG76-KNREJS7E5FY,65354
|
|
132
133
|
sourcecode/mcp/onboarding/__init__.py,sha256=sj2PWqEBmMc4zBNkomg89WtL0M6S7A9yb7_wAuSWNP4,66
|
|
133
134
|
sourcecode/mcp/onboarding/applier.py,sha256=Sx9vHTaXr_M1Bs4uMjO7qa_a2_p4J9oz93oJsKQ7ds4,3298
|
|
134
135
|
sourcecode/mcp/onboarding/backup.py,sha256=ihqGOR8QTX8HASRSEDyfFyXr5bkXrygPHamv4p9KTmk,1452
|
|
@@ -147,9 +148,9 @@ sourcecode/retrieval/retriever.py,sha256=YdnbaXaWhFKyShuuE6fj36GfgpuGetqOFzx7_D2
|
|
|
147
148
|
sourcecode/retrieval/runtime.py,sha256=ODQlPqyS4OfMtCb1oEA_0t7vK0uKPBLhB4K2CnytPUU,10822
|
|
148
149
|
sourcecode/retrieval/steps.py,sha256=wyOiO84fuDgdd-HdugzGSEriN-lN891PTc3gU7lgcmI,9959
|
|
149
150
|
sourcecode/retrieval/steps_endpoint.py,sha256=H_o4YsqgM198ZHtJg-UzhcmnFzbBeaYXrPNIDoc2IjI,13526
|
|
150
|
-
sourcecode/retrieval/steps_graph.py,sha256=
|
|
151
|
+
sourcecode/retrieval/steps_graph.py,sha256=E45vN9pA1lpKQ4pO2JP9-1cwIXoy1hWW6jsBDryeHGM,10684
|
|
151
152
|
sourcecode/retrieval/steps_impact.py,sha256=M0AwU5Ue7nkj-A7DYgQI2BP0BV1b0TFZRrYIydF-Ajw,11605
|
|
152
|
-
sourcecode/retrieval/steps_intf.py,sha256=
|
|
153
|
+
sourcecode/retrieval/steps_intf.py,sha256=BIopGUhxwutV51r8-1bTCC_VTYENJiHbREF13pB5mSo,11697
|
|
153
154
|
sourcecode/retrieval/steps_struct.py,sha256=jO9L49hziTlS_xhuhCGUavgyAL3ZAnJSOZB-UA4GaRM,13893
|
|
154
155
|
sourcecode/retrieval/steps_txsec.py,sha256=LKRzALzRw_0aQ3_tpbd34mt8u7o690g5tCnp871j7P4,17804
|
|
155
156
|
sourcecode/schemas/envelope-v1.schema.json,sha256=lABkyKqXND039JUY2R6KK47OCDco5mq5PLgkcCXFhOQ,3477
|
|
@@ -159,8 +160,8 @@ sourcecode/telemetry/consent.py,sha256=GuhCiA_x2n3LwGBkZnvyIwtFcf5Zwa8O0DdLqkATI
|
|
|
159
160
|
sourcecode/telemetry/events.py,sha256=4_yeO58U-Cwc1Qb27VB0_EjhmroY0k91n3_VGxeALB8,2776
|
|
160
161
|
sourcecode/telemetry/filters.py,sha256=RzxauTz8HliO4BllQnXEXc7zTeqdCZi5MgqGEDuW7OQ,6570
|
|
161
162
|
sourcecode/telemetry/transport.py,sha256=4gGHsq0WeY9VywEZXA3vUxykfiYnw9uuqfjAAec7F8o,1681
|
|
162
|
-
sourcecode-3.
|
|
163
|
-
sourcecode-3.
|
|
164
|
-
sourcecode-3.
|
|
165
|
-
sourcecode-3.
|
|
166
|
-
sourcecode-3.
|
|
163
|
+
sourcecode-3.2.0.dist-info/METADATA,sha256=O9_rjaiou-XB3P9QZsqnkmS1OAKcBCiEsxJOjjN2D9A,11286
|
|
164
|
+
sourcecode-3.2.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
165
|
+
sourcecode-3.2.0.dist-info/entry_points.txt,sha256=-JEAdChrK5We51kZcb7OaDcyil-dHBjBPL-NhuO-QY8,89
|
|
166
|
+
sourcecode-3.2.0.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
|
|
167
|
+
sourcecode-3.2.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|