sourcecode 1.35.0__tar.gz → 1.35.1__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.
- {sourcecode-1.35.0 → sourcecode-1.35.1}/PKG-INFO +2 -2
- {sourcecode-1.35.0 → sourcecode-1.35.1}/README.md +1 -1
- {sourcecode-1.35.0 → sourcecode-1.35.1}/pyproject.toml +1 -1
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/__init__.py +1 -1
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/cli.py +16 -4
- sourcecode-1.35.1/src/sourcecode/spring_model.py +258 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/spring_security_audit.py +57 -13
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/spring_tx_analyzer.py +50 -7
- {sourcecode-1.35.0 → sourcecode-1.35.1}/.github/workflows/build-windows.yml +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/.gitignore +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/.ruff.toml +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/CHANGELOG.md +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/CONTRIBUTING.md +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/LICENSE +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/SECURITY.md +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/raw +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/adaptive_scanner.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/architecture_analyzer.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/architecture_summary.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/ast_extractor.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/cache.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/canonical_ir.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/classifier.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/code_notes_analyzer.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/confidence_analyzer.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/context_scorer.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/context_summarizer.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/contract_model.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/contract_pipeline.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/coverage_parser.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/dependency_analyzer.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/detectors/__init__.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/detectors/base.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/detectors/csproj_parser.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/detectors/dart.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/detectors/dotnet.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/detectors/elixir.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/detectors/go.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/detectors/heuristic.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/detectors/hybrid.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/detectors/java.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/detectors/jvm_ext.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/detectors/nodejs.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/detectors/parsers.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/detectors/php.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/detectors/project.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/detectors/python.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/detectors/ruby.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/detectors/rust.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/detectors/systems.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/detectors/terraform.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/detectors/tooling.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/doc_analyzer.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/entrypoint_classifier.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/env_analyzer.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/error_schema.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/file_classifier.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/flow_analyzer.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/git_analyzer.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/graph_analyzer.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/license.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/mcp/__init__.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/mcp/onboarding/applier.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/mcp/onboarding/backup.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/mcp/onboarding/detector.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/mcp/onboarding/planner.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/mcp/orchestrator.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/mcp/registry.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/mcp/runner.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/mcp/server.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/mcp_nudge.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/metrics_analyzer.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/output_budget.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/path_filters.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/pr_comment_renderer.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/prepare_context.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/progress.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/ranking_engine.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/redactor.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/relevance_scorer.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/repo_classifier.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/repository_ir.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/ris.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/runtime_classifier.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/scanner.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/schema.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/semantic_analyzer.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/serializer.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/spring_findings.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/spring_semantic.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/summarizer.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/telemetry/__init__.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/telemetry/config.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/telemetry/consent.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/telemetry/events.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/telemetry/filters.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/telemetry/transport.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/tree_utils.py +0 -0
- {sourcecode-1.35.0 → sourcecode-1.35.1}/src/sourcecode/workspace.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sourcecode
|
|
3
|
-
Version: 1.35.
|
|
3
|
+
Version: 1.35.1
|
|
4
4
|
Summary: Persistent structural context and ultra-fast repeated analysis for AI coding agents
|
|
5
5
|
License-File: LICENSE
|
|
6
6
|
Keywords: agents,ai,codebase,context,developer-tools,llm
|
|
@@ -39,7 +39,7 @@ Description-Content-Type: text/markdown
|
|
|
39
39
|
|
|
40
40
|
**Persistent structural context and ultra-fast repeated analysis for AI coding agents.**
|
|
41
41
|
|
|
42
|
-

|
|
43
43
|

|
|
44
44
|
|
|
45
45
|
---
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**Persistent structural context and ultra-fast repeated analysis for AI coding agents.**
|
|
4
4
|
|
|
5
|
-

|
|
6
6
|

|
|
7
7
|
|
|
8
8
|
---
|
|
@@ -3766,7 +3766,7 @@ def spring_audit_cmd(
|
|
|
3766
3766
|
from sourcecode.spring_findings import SpringAuditResult, SpringFinding
|
|
3767
3767
|
from sourcecode.spring_tx_analyzer import run_tx_audit
|
|
3768
3768
|
from sourcecode.spring_security_audit import run_security_audit
|
|
3769
|
-
from sourcecode.
|
|
3769
|
+
from sourcecode.spring_model import SpringSemanticModel
|
|
3770
3770
|
|
|
3771
3771
|
target = path.resolve()
|
|
3772
3772
|
if not target.exists() or not target.is_dir():
|
|
@@ -3816,13 +3816,13 @@ def spring_audit_cmd(
|
|
|
3816
3816
|
return
|
|
3817
3817
|
|
|
3818
3818
|
cir = build_canonical_ir(file_list, target)
|
|
3819
|
-
|
|
3819
|
+
_model = SpringSemanticModel.build(cir)
|
|
3820
3820
|
|
|
3821
3821
|
results: list[SpringAuditResult] = []
|
|
3822
3822
|
if scope in ("all", "tx"):
|
|
3823
|
-
results.append(run_tx_audit(cir, root=target, min_severity=min_severity))
|
|
3823
|
+
results.append(run_tx_audit(cir, root=target, min_severity=min_severity, model=_model))
|
|
3824
3824
|
if scope in ("all", "security"):
|
|
3825
|
-
results.append(run_security_audit(cir, root=target, min_severity=min_severity,
|
|
3825
|
+
results.append(run_security_audit(cir, root=target, min_severity=min_severity, model=_model))
|
|
3826
3826
|
|
|
3827
3827
|
if len(results) == 1:
|
|
3828
3828
|
combined = results[0]
|
|
@@ -3843,6 +3843,18 @@ def spring_audit_cmd(
|
|
|
3843
3843
|
metadata=merged_meta,
|
|
3844
3844
|
).finalize()
|
|
3845
3845
|
|
|
3846
|
+
# Populate git_head from repo HEAD — non-fatal.
|
|
3847
|
+
try:
|
|
3848
|
+
import subprocess as _sub_sa
|
|
3849
|
+
_sha_r = _sub_sa.run(
|
|
3850
|
+
["git", "-C", str(target), "rev-parse", "--short", "HEAD"],
|
|
3851
|
+
capture_output=True, text=True, timeout=3,
|
|
3852
|
+
)
|
|
3853
|
+
if _sha_r.returncode == 0:
|
|
3854
|
+
combined.git_head = _sha_r.stdout.strip()
|
|
3855
|
+
except Exception:
|
|
3856
|
+
pass
|
|
3857
|
+
|
|
3846
3858
|
data = combined.to_dict()
|
|
3847
3859
|
|
|
3848
3860
|
# Non-fatal RIS side-effect — persist summary only (not full findings).
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"""spring_model.py — Shared Spring semantic model.
|
|
2
|
+
|
|
3
|
+
Builds once per analysis run from a CanonicalRepositoryIR.
|
|
4
|
+
All pattern analyzers consume this rather than re-deriving shared structures.
|
|
5
|
+
|
|
6
|
+
Components:
|
|
7
|
+
CallAdjacency — forward call adjacency (caller → callees)
|
|
8
|
+
InheritanceGraph — extends/implements graph with generic-parent detection
|
|
9
|
+
BeanGraph — Spring bean registry + injection graph
|
|
10
|
+
SpringSemanticModel — umbrella: tx_index + call_adj + inheritance + bean_graph
|
|
11
|
+
|
|
12
|
+
Eliminates per-pattern duplicate traversals:
|
|
13
|
+
build_tx_index() was called 2× per scope=all run → now 1×
|
|
14
|
+
_build_forward_adjacency() was called 3× (TX-002/003/004) → now 1×
|
|
15
|
+
_build_extends_map() was called per-run (SEC-002) → now 1×
|
|
16
|
+
|
|
17
|
+
Usage:
|
|
18
|
+
model = SpringSemanticModel.build(cir)
|
|
19
|
+
# or with pre-built tx_index (avoids double-build in CLI):
|
|
20
|
+
model = SpringSemanticModel.build(cir, tx_index=existing_index)
|
|
21
|
+
"""
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import re
|
|
25
|
+
import time
|
|
26
|
+
from dataclasses import dataclass, field
|
|
27
|
+
from typing import TYPE_CHECKING, Optional
|
|
28
|
+
|
|
29
|
+
from sourcecode.spring_semantic import TransactionBoundaryIndex, build_tx_index
|
|
30
|
+
|
|
31
|
+
if TYPE_CHECKING:
|
|
32
|
+
from sourcecode.canonical_ir import CanonicalRepositoryIR
|
|
33
|
+
|
|
34
|
+
# Edge types excluded from forward adjacency (structural, not call edges)
|
|
35
|
+
_CALL_SKIP: frozenset[str] = frozenset({"annotated_with", "mapped_to", "contained_in"})
|
|
36
|
+
|
|
37
|
+
# Spring bean stereotype annotations
|
|
38
|
+
_BEAN_ANNOTATIONS: frozenset[str] = frozenset({
|
|
39
|
+
"@Component", "@Service", "@Repository",
|
|
40
|
+
"@Controller", "@RestController", "@Configuration", "@Bean",
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
_GENERIC_PARAM_RE = re.compile(r"<[A-Z][\w,\s<>?]*>")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# ---------------------------------------------------------------------------
|
|
47
|
+
# CallAdjacency
|
|
48
|
+
# ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class CallAdjacency:
|
|
52
|
+
"""Forward call adjacency built from CIR call_graph edges.
|
|
53
|
+
|
|
54
|
+
Shared across TX patterns (TX-002, TX-003, TX-004) to avoid one rebuild
|
|
55
|
+
per pattern per analysis run.
|
|
56
|
+
"""
|
|
57
|
+
adjacency: dict[str, list[str]] = field(default_factory=dict) # caller → [callees]
|
|
58
|
+
|
|
59
|
+
@classmethod
|
|
60
|
+
def build(cls, cir: "CanonicalRepositoryIR") -> "CallAdjacency":
|
|
61
|
+
adj: dict[str, list[str]] = {}
|
|
62
|
+
for edge in cir.call_graph:
|
|
63
|
+
if not isinstance(edge, dict):
|
|
64
|
+
continue
|
|
65
|
+
if edge.get("type") in _CALL_SKIP:
|
|
66
|
+
continue
|
|
67
|
+
frm = edge.get("from") or ""
|
|
68
|
+
to = edge.get("to") or ""
|
|
69
|
+
if frm and to:
|
|
70
|
+
adj.setdefault(frm, []).append(to)
|
|
71
|
+
return cls(adjacency=adj)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# ---------------------------------------------------------------------------
|
|
75
|
+
# InheritanceGraph
|
|
76
|
+
# ---------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
@dataclass
|
|
79
|
+
class InheritanceGraph:
|
|
80
|
+
"""Extends/implements graph derived from CIR dependency edges.
|
|
81
|
+
|
|
82
|
+
Provides inheritance relationships for SEC-002 (@PreAuthorize on generic
|
|
83
|
+
supertype) and future self-invocation detection.
|
|
84
|
+
|
|
85
|
+
generic_parents: FQNs whose immediate parent signature has type parameters.
|
|
86
|
+
Computed once; avoids per-pattern regex matching at analysis time.
|
|
87
|
+
"""
|
|
88
|
+
parent_of: dict[str, str] = field(default_factory=dict) # child FQN → parent signature
|
|
89
|
+
generic_parents: set[str] = field(default_factory=set) # FQNs with a generic parent
|
|
90
|
+
|
|
91
|
+
@classmethod
|
|
92
|
+
def build(cls, cir: "CanonicalRepositoryIR") -> "InheritanceGraph":
|
|
93
|
+
parent_of: dict[str, str] = {}
|
|
94
|
+
generic_parents: set[str] = set()
|
|
95
|
+
for edge in cir.dependencies:
|
|
96
|
+
if not isinstance(edge, dict):
|
|
97
|
+
continue
|
|
98
|
+
if edge.get("type") != "extends":
|
|
99
|
+
continue
|
|
100
|
+
child = edge.get("from") or ""
|
|
101
|
+
parent = edge.get("to") or ""
|
|
102
|
+
if child and parent:
|
|
103
|
+
parent_of[child] = parent
|
|
104
|
+
if _GENERIC_PARAM_RE.search(parent):
|
|
105
|
+
generic_parents.add(child)
|
|
106
|
+
return cls(parent_of=parent_of, generic_parents=generic_parents)
|
|
107
|
+
|
|
108
|
+
def immediate_parent(self, fqn: str) -> str:
|
|
109
|
+
"""Return the immediate parent signature for fqn, or empty string."""
|
|
110
|
+
return self.parent_of.get(fqn, "")
|
|
111
|
+
|
|
112
|
+
def has_generic_parent(self, fqn: str) -> bool:
|
|
113
|
+
"""True when the immediate parent of fqn has type parameters."""
|
|
114
|
+
return fqn in self.generic_parents
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
# ---------------------------------------------------------------------------
|
|
118
|
+
# BeanGraph
|
|
119
|
+
# ---------------------------------------------------------------------------
|
|
120
|
+
|
|
121
|
+
@dataclass
|
|
122
|
+
class BeanNode:
|
|
123
|
+
"""Minimal representation of a detected Spring bean."""
|
|
124
|
+
fqn: str
|
|
125
|
+
stereotype: str # component|service|repository|controller|configuration|bean
|
|
126
|
+
source_file: str
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@dataclass
|
|
130
|
+
class BeanGraph:
|
|
131
|
+
"""Spring bean registry derived from _raw_ir graph nodes.
|
|
132
|
+
|
|
133
|
+
Foundation for future capabilities:
|
|
134
|
+
- self-invocation detection (proxy cannot intercept this.method())
|
|
135
|
+
- conditional bean analysis (@ConditionalOn* chains)
|
|
136
|
+
- module impact analysis (inter-bean dependency tracing)
|
|
137
|
+
|
|
138
|
+
injections: @Autowired / constructor injection edges (type="injects").
|
|
139
|
+
"""
|
|
140
|
+
beans: dict[str, BeanNode] = field(default_factory=dict) # fqn → node
|
|
141
|
+
injections: dict[str, list[str]] = field(default_factory=dict) # fqn → [injected FQNs]
|
|
142
|
+
|
|
143
|
+
@classmethod
|
|
144
|
+
def build(cls, cir: "CanonicalRepositoryIR") -> "BeanGraph":
|
|
145
|
+
beans: dict[str, BeanNode] = {}
|
|
146
|
+
injections: dict[str, list[str]] = {}
|
|
147
|
+
|
|
148
|
+
raw_ir = getattr(cir, "_raw_ir", {}) or {}
|
|
149
|
+
nodes = (raw_ir.get("graph") or {}).get("nodes") or []
|
|
150
|
+
|
|
151
|
+
for node in nodes:
|
|
152
|
+
if not isinstance(node, dict):
|
|
153
|
+
continue
|
|
154
|
+
ann_set = set(node.get("annotations") or [])
|
|
155
|
+
match = ann_set & _BEAN_ANNOTATIONS
|
|
156
|
+
if not match:
|
|
157
|
+
continue
|
|
158
|
+
fqn = node.get("fqn") or ""
|
|
159
|
+
if not fqn:
|
|
160
|
+
continue
|
|
161
|
+
ann = next(iter(match))
|
|
162
|
+
stereotype = ann.lstrip("@").lower()
|
|
163
|
+
beans[fqn] = BeanNode(
|
|
164
|
+
fqn=fqn,
|
|
165
|
+
stereotype=stereotype,
|
|
166
|
+
source_file=node.get("source_file") or "",
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
for edge in cir.call_graph:
|
|
170
|
+
if not isinstance(edge, dict):
|
|
171
|
+
continue
|
|
172
|
+
if edge.get("type") != "injects":
|
|
173
|
+
continue
|
|
174
|
+
frm = edge.get("from") or ""
|
|
175
|
+
to = edge.get("to") or ""
|
|
176
|
+
if frm and to and frm in beans:
|
|
177
|
+
injections.setdefault(frm, []).append(to)
|
|
178
|
+
|
|
179
|
+
return cls(beans=beans, injections=injections)
|
|
180
|
+
|
|
181
|
+
def is_bean(self, fqn: str) -> bool:
|
|
182
|
+
return fqn in self.beans
|
|
183
|
+
|
|
184
|
+
def get_stereotype(self, fqn: str) -> str:
|
|
185
|
+
node = self.beans.get(fqn)
|
|
186
|
+
return node.stereotype if node else ""
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
# ---------------------------------------------------------------------------
|
|
190
|
+
# SpringSemanticModel
|
|
191
|
+
# ---------------------------------------------------------------------------
|
|
192
|
+
|
|
193
|
+
@dataclass
|
|
194
|
+
class SpringSemanticModel:
|
|
195
|
+
"""Shared semantic context. Built once per analysis run.
|
|
196
|
+
|
|
197
|
+
Eliminates duplicate CIR traversals when multiple patterns run together.
|
|
198
|
+
Pattern analyzers that receive a model should consume it rather than
|
|
199
|
+
re-deriving from the CIR.
|
|
200
|
+
|
|
201
|
+
Fields:
|
|
202
|
+
tx_index: @Transactional boundary index.
|
|
203
|
+
call_adj: Forward call adjacency (caller → callees).
|
|
204
|
+
inheritance: Extends/implements graph + generic-parent detection.
|
|
205
|
+
bean_graph: Spring bean registry and injection graph.
|
|
206
|
+
build_time_ms: Wall-clock ms to build all sub-models (not counting
|
|
207
|
+
a pre-built tx_index passed by the caller).
|
|
208
|
+
"""
|
|
209
|
+
tx_index: TransactionBoundaryIndex
|
|
210
|
+
call_adj: CallAdjacency
|
|
211
|
+
inheritance: InheritanceGraph
|
|
212
|
+
bean_graph: BeanGraph
|
|
213
|
+
build_time_ms: float = 0.0
|
|
214
|
+
|
|
215
|
+
@classmethod
|
|
216
|
+
def build(
|
|
217
|
+
cls,
|
|
218
|
+
cir: "CanonicalRepositoryIR",
|
|
219
|
+
*,
|
|
220
|
+
tx_index: Optional[TransactionBoundaryIndex] = None,
|
|
221
|
+
) -> "SpringSemanticModel":
|
|
222
|
+
"""Build all sub-models from a CIR. Never raises.
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
cir: CanonicalRepositoryIR from build_canonical_ir().
|
|
226
|
+
tx_index: Pre-built index to reuse — avoids double build in CLI
|
|
227
|
+
when tx_index was already computed for another purpose.
|
|
228
|
+
"""
|
|
229
|
+
t0 = time.monotonic()
|
|
230
|
+
|
|
231
|
+
try:
|
|
232
|
+
tx = tx_index if tx_index is not None else build_tx_index(cir)
|
|
233
|
+
except Exception:
|
|
234
|
+
tx = TransactionBoundaryIndex()
|
|
235
|
+
|
|
236
|
+
try:
|
|
237
|
+
adj = CallAdjacency.build(cir)
|
|
238
|
+
except Exception:
|
|
239
|
+
adj = CallAdjacency()
|
|
240
|
+
|
|
241
|
+
try:
|
|
242
|
+
inh = InheritanceGraph.build(cir)
|
|
243
|
+
except Exception:
|
|
244
|
+
inh = InheritanceGraph()
|
|
245
|
+
|
|
246
|
+
try:
|
|
247
|
+
bg = BeanGraph.build(cir)
|
|
248
|
+
except Exception:
|
|
249
|
+
bg = BeanGraph()
|
|
250
|
+
|
|
251
|
+
elapsed = round((time.monotonic() - t0) * 1000, 2)
|
|
252
|
+
return cls(
|
|
253
|
+
tx_index=tx,
|
|
254
|
+
call_adj=adj,
|
|
255
|
+
inheritance=inh,
|
|
256
|
+
bean_graph=bg,
|
|
257
|
+
build_time_ms=elapsed,
|
|
258
|
+
)
|
|
@@ -9,12 +9,14 @@ All patterns are deterministic and never raise.
|
|
|
9
9
|
"""
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
|
+
import inspect
|
|
12
13
|
import re
|
|
13
14
|
import time
|
|
14
15
|
from pathlib import Path
|
|
15
|
-
from typing import TYPE_CHECKING, Optional, Protocol, runtime_checkable
|
|
16
|
+
from typing import TYPE_CHECKING, Any, Optional, Protocol, runtime_checkable
|
|
16
17
|
|
|
17
18
|
from sourcecode.spring_findings import SpringAuditResult, SpringFinding
|
|
19
|
+
from sourcecode.spring_model import SpringSemanticModel
|
|
18
20
|
from sourcecode.spring_semantic import TransactionBoundaryIndex, build_tx_index
|
|
19
21
|
|
|
20
22
|
if TYPE_CHECKING:
|
|
@@ -38,10 +40,29 @@ class SecurityPattern(Protocol):
|
|
|
38
40
|
cir: "CanonicalRepositoryIR",
|
|
39
41
|
tx_index: Optional[TransactionBoundaryIndex],
|
|
40
42
|
root: Optional[Path],
|
|
43
|
+
*,
|
|
44
|
+
model: Optional[SpringSemanticModel] = None,
|
|
41
45
|
) -> list[SpringFinding]:
|
|
42
46
|
...
|
|
43
47
|
|
|
44
48
|
|
|
49
|
+
def _call_pattern_analyze(
|
|
50
|
+
pattern: Any,
|
|
51
|
+
cir: "CanonicalRepositoryIR",
|
|
52
|
+
tx_index: Optional[TransactionBoundaryIndex],
|
|
53
|
+
root: Optional[Path],
|
|
54
|
+
model: Optional[SpringSemanticModel],
|
|
55
|
+
) -> list[SpringFinding]:
|
|
56
|
+
"""Dispatch to pattern.analyze(), injecting model if the pattern accepts it."""
|
|
57
|
+
try:
|
|
58
|
+
sig = inspect.signature(pattern.analyze)
|
|
59
|
+
if "model" in sig.parameters:
|
|
60
|
+
return pattern.analyze(cir, tx_index, root, model=model)
|
|
61
|
+
except (ValueError, TypeError):
|
|
62
|
+
pass
|
|
63
|
+
return pattern.analyze(cir, tx_index, root)
|
|
64
|
+
|
|
65
|
+
|
|
45
66
|
# ---------------------------------------------------------------------------
|
|
46
67
|
# SEC-001: Endpoint without security guard (annotation_based model)
|
|
47
68
|
# ---------------------------------------------------------------------------
|
|
@@ -55,6 +76,8 @@ class _SEC001UnsecuredEndpoint:
|
|
|
55
76
|
cir: "CanonicalRepositoryIR",
|
|
56
77
|
tx_index: Optional[TransactionBoundaryIndex],
|
|
57
78
|
root: Optional[Path],
|
|
79
|
+
*,
|
|
80
|
+
model: Optional[SpringSemanticModel] = None,
|
|
58
81
|
) -> list[SpringFinding]:
|
|
59
82
|
security_model = cir.metadata.get("security_model", "unknown")
|
|
60
83
|
# filter_based model has centralized config — per-endpoint annotation absence is expected
|
|
@@ -119,9 +142,16 @@ class _SEC002PreAuthorizeGenericInheritance:
|
|
|
119
142
|
cir: "CanonicalRepositoryIR",
|
|
120
143
|
tx_index: Optional[TransactionBoundaryIndex],
|
|
121
144
|
root: Optional[Path],
|
|
145
|
+
*,
|
|
146
|
+
model: Optional[SpringSemanticModel] = None,
|
|
122
147
|
) -> list[SpringFinding]:
|
|
123
|
-
#
|
|
124
|
-
|
|
148
|
+
# Use shared inheritance graph when available; fall back to local build.
|
|
149
|
+
if model is not None:
|
|
150
|
+
_parent_of = model.inheritance.parent_of
|
|
151
|
+
_generic_parents = model.inheritance.generic_parents
|
|
152
|
+
else:
|
|
153
|
+
_parent_of = _build_extends_map(cir)
|
|
154
|
+
_generic_parents = None # determined via regex below
|
|
125
155
|
|
|
126
156
|
findings: list[SpringFinding] = []
|
|
127
157
|
seen: set[str] = set()
|
|
@@ -135,8 +165,11 @@ class _SEC002PreAuthorizeGenericInheritance:
|
|
|
135
165
|
continue
|
|
136
166
|
|
|
137
167
|
# Resolve parent class signature for this controller
|
|
138
|
-
parent_sig =
|
|
139
|
-
|
|
168
|
+
parent_sig = _parent_of.get(ep.controller_class, "")
|
|
169
|
+
if _generic_parents is not None:
|
|
170
|
+
has_generics = ep.controller_class in _generic_parents
|
|
171
|
+
else:
|
|
172
|
+
has_generics = bool(_GENERIC_PARAM_RE.search(parent_sig))
|
|
140
173
|
confidence = "high" if has_generics else "medium"
|
|
141
174
|
|
|
142
175
|
key = f"{ep.controller_class}#{ep.handler_symbol}"
|
|
@@ -205,6 +238,8 @@ class _SEC003TransactionalOnController:
|
|
|
205
238
|
cir: "CanonicalRepositoryIR",
|
|
206
239
|
tx_index: Optional[TransactionBoundaryIndex],
|
|
207
240
|
root: Optional[Path],
|
|
241
|
+
*,
|
|
242
|
+
model: Optional[SpringSemanticModel] = None,
|
|
208
243
|
) -> list[SpringFinding]:
|
|
209
244
|
if tx_index is None:
|
|
210
245
|
return []
|
|
@@ -337,11 +372,12 @@ def _build_extends_map(cir: "CanonicalRepositoryIR") -> dict[str, str]:
|
|
|
337
372
|
def _controller_source_file(cir: "CanonicalRepositoryIR", controller_fqn: str) -> str:
|
|
338
373
|
"""Return the source_file for a controller class from cir.files, or empty string."""
|
|
339
374
|
simple = controller_fqn.split(".")[-1]
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
375
|
+
if not simple:
|
|
376
|
+
return ""
|
|
377
|
+
for path in cir.files:
|
|
378
|
+
if isinstance(path, str) and (
|
|
379
|
+
path.endswith(f"{simple}.java") or path.endswith(f"{simple}.kt")
|
|
380
|
+
):
|
|
345
381
|
return path
|
|
346
382
|
return ""
|
|
347
383
|
|
|
@@ -379,6 +415,8 @@ class SecurityScanner:
|
|
|
379
415
|
Usage:
|
|
380
416
|
scanner = SecurityScanner()
|
|
381
417
|
findings = scanner.analyze(cir, tx_index=tx_index, root=Path("/repo"))
|
|
418
|
+
# or with pre-built model (shares inheritance graph, avoids rebuilds):
|
|
419
|
+
findings = scanner.analyze(cir, tx_index=tx_index, root=Path("/repo"), model=model)
|
|
382
420
|
|
|
383
421
|
Never raises. Pattern errors are silently swallowed (finding missed, not crash).
|
|
384
422
|
"""
|
|
@@ -393,11 +431,13 @@ class SecurityScanner:
|
|
|
393
431
|
cir: "CanonicalRepositoryIR",
|
|
394
432
|
tx_index: Optional[TransactionBoundaryIndex] = None,
|
|
395
433
|
root: Optional[Path] = None,
|
|
434
|
+
*,
|
|
435
|
+
model: Optional[SpringSemanticModel] = None,
|
|
396
436
|
) -> list[SpringFinding]:
|
|
397
437
|
all_findings: list[SpringFinding] = []
|
|
398
438
|
for pattern in self.patterns:
|
|
399
439
|
try:
|
|
400
|
-
found = pattern
|
|
440
|
+
found = _call_pattern_analyze(pattern, cir, tx_index, root, model)
|
|
401
441
|
all_findings.extend(found)
|
|
402
442
|
except Exception:
|
|
403
443
|
pass
|
|
@@ -417,6 +457,7 @@ def run_security_audit(
|
|
|
417
457
|
min_severity: str = "low",
|
|
418
458
|
patterns: Optional[list[SecurityPattern]] = None,
|
|
419
459
|
tx_index: Optional[TransactionBoundaryIndex] = None,
|
|
460
|
+
model: Optional[SpringSemanticModel] = None,
|
|
420
461
|
) -> SpringAuditResult:
|
|
421
462
|
"""Run security surface audit and return a SpringAuditResult.
|
|
422
463
|
|
|
@@ -427,17 +468,20 @@ def run_security_audit(
|
|
|
427
468
|
min_severity: Filter findings below this severity.
|
|
428
469
|
patterns: Override default pattern list (for testing).
|
|
429
470
|
tx_index: Pre-built TransactionBoundaryIndex (built from cir if None).
|
|
471
|
+
model: Pre-built SpringSemanticModel (avoids duplicate build in CLI).
|
|
430
472
|
"""
|
|
431
473
|
_sev_rank = {"critical": 0, "high": 1, "medium": 2, "low": 3}
|
|
432
474
|
min_rank = _sev_rank.get(min_severity, 3)
|
|
433
475
|
|
|
434
476
|
t0 = time.monotonic()
|
|
435
477
|
|
|
436
|
-
if
|
|
478
|
+
if model is not None:
|
|
479
|
+
tx_index = model.tx_index
|
|
480
|
+
elif tx_index is None:
|
|
437
481
|
tx_index = build_tx_index(cir)
|
|
438
482
|
|
|
439
483
|
scanner = SecurityScanner(patterns=patterns)
|
|
440
|
-
findings = scanner.analyze(cir, tx_index=tx_index, root=root)
|
|
484
|
+
findings = scanner.analyze(cir, tx_index=tx_index, root=root, model=model)
|
|
441
485
|
|
|
442
486
|
findings = [f for f in findings if _sev_rank.get(f.severity, 9) <= min_rank]
|
|
443
487
|
|
|
@@ -14,13 +14,15 @@ All patterns are deterministic and never raise.
|
|
|
14
14
|
"""
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
17
|
+
import inspect
|
|
17
18
|
import re
|
|
18
19
|
import time
|
|
19
20
|
from collections import deque
|
|
20
21
|
from pathlib import Path
|
|
21
|
-
from typing import TYPE_CHECKING, Optional, Protocol, runtime_checkable
|
|
22
|
+
from typing import TYPE_CHECKING, Any, Optional, Protocol, runtime_checkable
|
|
22
23
|
|
|
23
24
|
from sourcecode.spring_findings import SpringAuditResult, SpringFinding
|
|
25
|
+
from sourcecode.spring_model import SpringSemanticModel
|
|
24
26
|
from sourcecode.spring_semantic import (
|
|
25
27
|
PROPAGATION_DEFAULT,
|
|
26
28
|
TransactionBoundary,
|
|
@@ -70,10 +72,33 @@ class TxPattern(Protocol):
|
|
|
70
72
|
cir: "CanonicalRepositoryIR",
|
|
71
73
|
tx_index: TransactionBoundaryIndex,
|
|
72
74
|
root: Optional[Path],
|
|
75
|
+
*,
|
|
76
|
+
model: Optional[SpringSemanticModel] = None,
|
|
73
77
|
) -> list[SpringFinding]:
|
|
74
78
|
...
|
|
75
79
|
|
|
76
80
|
|
|
81
|
+
def _call_pattern_analyze(
|
|
82
|
+
pattern: Any,
|
|
83
|
+
cir: "CanonicalRepositoryIR",
|
|
84
|
+
tx_index: TransactionBoundaryIndex,
|
|
85
|
+
root: Optional[Path],
|
|
86
|
+
model: Optional[SpringSemanticModel],
|
|
87
|
+
) -> list[SpringFinding]:
|
|
88
|
+
"""Dispatch to pattern.analyze(), injecting model if the pattern accepts it.
|
|
89
|
+
|
|
90
|
+
Patterns that declare `model` in their signature receive the shared model.
|
|
91
|
+
Patterns without it (e.g. test doubles) are called with the legacy signature.
|
|
92
|
+
"""
|
|
93
|
+
try:
|
|
94
|
+
sig = inspect.signature(pattern.analyze)
|
|
95
|
+
if "model" in sig.parameters:
|
|
96
|
+
return pattern.analyze(cir, tx_index, root, model=model)
|
|
97
|
+
except (ValueError, TypeError):
|
|
98
|
+
pass
|
|
99
|
+
return pattern.analyze(cir, tx_index, root)
|
|
100
|
+
|
|
101
|
+
|
|
77
102
|
# ---------------------------------------------------------------------------
|
|
78
103
|
# TX-001: @Transactional on private or final method
|
|
79
104
|
# ---------------------------------------------------------------------------
|
|
@@ -87,6 +112,8 @@ class _TX001ProxyBypass:
|
|
|
87
112
|
cir: "CanonicalRepositoryIR",
|
|
88
113
|
tx_index: TransactionBoundaryIndex,
|
|
89
114
|
root: Optional[Path],
|
|
115
|
+
*,
|
|
116
|
+
model: Optional[SpringSemanticModel] = None,
|
|
90
117
|
) -> list[SpringFinding]:
|
|
91
118
|
findings: list[SpringFinding] = []
|
|
92
119
|
|
|
@@ -210,11 +237,13 @@ class _TX002RequiresNewNested:
|
|
|
210
237
|
cir: "CanonicalRepositoryIR",
|
|
211
238
|
tx_index: TransactionBoundaryIndex,
|
|
212
239
|
root: Optional[Path],
|
|
240
|
+
*,
|
|
241
|
+
model: Optional[SpringSemanticModel] = None,
|
|
213
242
|
) -> list[SpringFinding]:
|
|
214
243
|
findings: list[SpringFinding] = []
|
|
215
244
|
seen_pairs: set[tuple[str, str]] = set()
|
|
216
245
|
|
|
217
|
-
adj = _build_forward_adjacency(cir)
|
|
246
|
+
adj = model.call_adj.adjacency if model is not None else _build_forward_adjacency(cir)
|
|
218
247
|
deadline = time.monotonic_ns() + _BFS_TIMEOUT_MS * 1_000_000
|
|
219
248
|
|
|
220
249
|
for boundary in tx_index.all_boundaries():
|
|
@@ -284,11 +313,13 @@ class _TX003ReadOnlyWritePropagation:
|
|
|
284
313
|
cir: "CanonicalRepositoryIR",
|
|
285
314
|
tx_index: TransactionBoundaryIndex,
|
|
286
315
|
root: Optional[Path],
|
|
316
|
+
*,
|
|
317
|
+
model: Optional[SpringSemanticModel] = None,
|
|
287
318
|
) -> list[SpringFinding]:
|
|
288
319
|
findings: list[SpringFinding] = []
|
|
289
320
|
seen_pairs: set[tuple[str, str]] = set()
|
|
290
321
|
|
|
291
|
-
adj = _build_forward_adjacency(cir)
|
|
322
|
+
adj = model.call_adj.adjacency if model is not None else _build_forward_adjacency(cir)
|
|
292
323
|
deadline = time.monotonic_ns() + _BFS_TIMEOUT_MS * 1_000_000
|
|
293
324
|
|
|
294
325
|
for boundary in tx_index.all_boundaries():
|
|
@@ -364,11 +395,13 @@ class _TX004TxSuspensionRisk:
|
|
|
364
395
|
cir: "CanonicalRepositoryIR",
|
|
365
396
|
tx_index: TransactionBoundaryIndex,
|
|
366
397
|
root: Optional[Path],
|
|
398
|
+
*,
|
|
399
|
+
model: Optional[SpringSemanticModel] = None,
|
|
367
400
|
) -> list[SpringFinding]:
|
|
368
401
|
findings: list[SpringFinding] = []
|
|
369
402
|
seen_pairs: set[tuple[str, str]] = set()
|
|
370
403
|
|
|
371
|
-
adj = _build_forward_adjacency(cir)
|
|
404
|
+
adj = model.call_adj.adjacency if model is not None else _build_forward_adjacency(cir)
|
|
372
405
|
deadline = time.monotonic_ns() + _BFS_TIMEOUT_MS * 1_000_000
|
|
373
406
|
|
|
374
407
|
for boundary in tx_index.all_boundaries():
|
|
@@ -448,6 +481,8 @@ class _TX005ExceptionSwallowing:
|
|
|
448
481
|
cir: "CanonicalRepositoryIR",
|
|
449
482
|
tx_index: TransactionBoundaryIndex,
|
|
450
483
|
root: Optional[Path],
|
|
484
|
+
*,
|
|
485
|
+
model: Optional[SpringSemanticModel] = None,
|
|
451
486
|
) -> list[SpringFinding]:
|
|
452
487
|
if root is None:
|
|
453
488
|
return []
|
|
@@ -551,6 +586,8 @@ class TxPatternEngine:
|
|
|
551
586
|
Usage:
|
|
552
587
|
engine = TxPatternEngine()
|
|
553
588
|
findings = engine.analyze(cir, tx_index, root=Path("/repo"))
|
|
589
|
+
# or with pre-built model (eliminates duplicate adjacency builds):
|
|
590
|
+
findings = engine.analyze(cir, tx_index, root=Path("/repo"), model=model)
|
|
554
591
|
|
|
555
592
|
Never raises. Pattern errors are silently swallowed (finding missed, not crash).
|
|
556
593
|
"""
|
|
@@ -563,11 +600,13 @@ class TxPatternEngine:
|
|
|
563
600
|
cir: "CanonicalRepositoryIR",
|
|
564
601
|
tx_index: TransactionBoundaryIndex,
|
|
565
602
|
root: Optional[Path] = None,
|
|
603
|
+
*,
|
|
604
|
+
model: Optional[SpringSemanticModel] = None,
|
|
566
605
|
) -> list[SpringFinding]:
|
|
567
606
|
all_findings: list[SpringFinding] = []
|
|
568
607
|
for pattern in self.patterns:
|
|
569
608
|
try:
|
|
570
|
-
found = pattern
|
|
609
|
+
found = _call_pattern_analyze(pattern, cir, tx_index, root, model)
|
|
571
610
|
all_findings.extend(found)
|
|
572
611
|
except Exception:
|
|
573
612
|
pass
|
|
@@ -586,6 +625,7 @@ def run_tx_audit(
|
|
|
586
625
|
scope: str = "all",
|
|
587
626
|
min_severity: str = "low",
|
|
588
627
|
patterns: Optional[list[TxPattern]] = None,
|
|
628
|
+
model: Optional[SpringSemanticModel] = None,
|
|
589
629
|
) -> SpringAuditResult:
|
|
590
630
|
"""Run TX anomaly detection and return a SpringAuditResult.
|
|
591
631
|
|
|
@@ -595,15 +635,18 @@ def run_tx_audit(
|
|
|
595
635
|
scope: "all" | "tx" (reserved — always "tx" for this function).
|
|
596
636
|
min_severity: Filter findings below this severity.
|
|
597
637
|
patterns: Override default pattern list (for testing).
|
|
638
|
+
model: Pre-built SpringSemanticModel (avoids duplicate build in CLI).
|
|
598
639
|
"""
|
|
599
640
|
_sev_rank = {"critical": 0, "high": 1, "medium": 2, "low": 3}
|
|
600
641
|
min_rank = _sev_rank.get(min_severity, 3)
|
|
601
642
|
|
|
602
643
|
t0 = time.monotonic()
|
|
603
644
|
|
|
604
|
-
|
|
645
|
+
if model is None:
|
|
646
|
+
model = SpringSemanticModel.build(cir)
|
|
647
|
+
tx_index = model.tx_index
|
|
605
648
|
engine = TxPatternEngine(patterns=patterns)
|
|
606
|
-
findings = engine.analyze(cir, tx_index, root=root)
|
|
649
|
+
findings = engine.analyze(cir, tx_index, root=root, model=model)
|
|
607
650
|
|
|
608
651
|
# Filter by min_severity
|
|
609
652
|
findings = [f for f in findings if _sev_rank.get(f.severity, 9) <= min_rank]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|