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

|
|
44
44
|

|
|
45
45
|
|
|
46
46
|
---
|
|
@@ -114,7 +114,9 @@ pipx install sourcecode
|
|
|
114
114
|
|
|
115
115
|
```bash
|
|
116
116
|
sourcecode version
|
|
117
|
-
# sourcecode 1.35.
|
|
117
|
+
# sourcecode 1.35.28
|
|
118
|
+
|
|
119
|
+
**v1.35.28** — 7 bug fixes: `rename-class` cross-package disambiguation (BUG-4), `rename-class` collision detection (BUG-2), `find_java_files` false positive on `com/test/` package paths (BUG-1), `cold-start --compact` correct key names (BUG-6), `@EnableMethodSecurity` no longer suppresses SEC-001 (BUG-3), `explain` @Entity stereotype detection (BUG-5), XML+annotation mixed security retagging (BUG-7).
|
|
118
120
|
```
|
|
119
121
|
|
|
120
122
|
---
|
|
@@ -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
|
---
|
|
@@ -76,7 +76,9 @@ pipx install sourcecode
|
|
|
76
76
|
|
|
77
77
|
```bash
|
|
78
78
|
sourcecode version
|
|
79
|
-
# sourcecode 1.35.
|
|
79
|
+
# sourcecode 1.35.28
|
|
80
|
+
|
|
81
|
+
**v1.35.28** — 7 bug fixes: `rename-class` cross-package disambiguation (BUG-4), `rename-class` collision detection (BUG-2), `find_java_files` false positive on `com/test/` package paths (BUG-1), `cold-start --compact` correct key names (BUG-6), `@EnableMethodSecurity` no longer suppresses SEC-001 (BUG-3), `explain` @Entity stereotype detection (BUG-5), XML+annotation mixed security retagging (BUG-7).
|
|
80
82
|
```
|
|
81
83
|
|
|
82
84
|
---
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "sourcecode"
|
|
7
|
-
version = "1.35.
|
|
7
|
+
version = "1.35.28"
|
|
8
8
|
description = "Persistent structural context and ultra-fast repeated analysis for AI coding agents"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -5397,10 +5397,13 @@ def cold_start_cmd(
|
|
|
5397
5397
|
result = _gcs(target)
|
|
5398
5398
|
if compact:
|
|
5399
5399
|
# P1-C: cap at ~10K tokens — keep only fields essential for orientation.
|
|
5400
|
-
|
|
5401
|
-
|
|
5402
|
-
"validation", "_meta"}
|
|
5400
|
+
# BUG-6 fix: use actual RIS key names (summary/entrypoints, not stacks/entry_points)
|
|
5401
|
+
_cs_keys = {"status", "git_head", "summary", "entrypoints", "endpoints",
|
|
5402
|
+
"project_type", "validation", "_meta"}
|
|
5403
5403
|
result = {k: v for k, v in result.items() if k in _cs_keys}
|
|
5404
|
+
# Truncate endpoints to first 30 to stay within ~10K token budget
|
|
5405
|
+
if isinstance(result.get("endpoints"), list):
|
|
5406
|
+
result["endpoints"] = result["endpoints"][:30]
|
|
5404
5407
|
result["_meta"] = {**(result.get("_meta") or {}), "compact_mode": True,
|
|
5405
5408
|
"full_available": "sourcecode cold-start (without --compact)"}
|
|
5406
5409
|
_out = _json.dumps(result, indent=2, ensure_ascii=False)
|
|
@@ -28,6 +28,9 @@ _STEREOTYPE_DESC: dict[str, str] = {
|
|
|
28
28
|
"component": "Spring @Component — general-purpose bean",
|
|
29
29
|
"configuration": "Spring @Configuration — bean factory / config",
|
|
30
30
|
"bean": "Spring @Bean — managed component",
|
|
31
|
+
"entity": "JPA @Entity — persistent domain object mapped to a database table",
|
|
32
|
+
"mappedsuperclass": "JPA @MappedSuperclass — base class sharing persistent state with subclasses",
|
|
33
|
+
"embeddable": "JPA @Embeddable — value object embedded in owning entity table",
|
|
31
34
|
}
|
|
32
35
|
|
|
33
36
|
_SECURITY_ANNOTATION_PREFIXES = (
|
|
@@ -106,7 +106,11 @@ def _collect_java_files(root: Path, *, include_tests: bool = True) -> list[Path]
|
|
|
106
106
|
if any(part in _VENDOR_DIRS for part in parts[:-1]):
|
|
107
107
|
continue
|
|
108
108
|
if not include_tests:
|
|
109
|
-
if
|
|
109
|
+
if (
|
|
110
|
+
"/src/test/" in rel or rel.startswith("src/test/")
|
|
111
|
+
or "/src/tests/" in rel or rel.startswith("src/tests/")
|
|
112
|
+
or rel.startswith("test/") or rel.startswith("tests/")
|
|
113
|
+
):
|
|
110
114
|
continue
|
|
111
115
|
results.append(p)
|
|
112
116
|
return results
|
|
@@ -150,10 +154,42 @@ def _find_class_file(
|
|
|
150
154
|
|
|
151
155
|
def _apply_rename(source: str, old_name: str, new_name: str) -> str:
|
|
152
156
|
"""Apply word-boundary replacement for class name (PascalCase and camelCase forms)."""
|
|
153
|
-
# PascalCase replacement: all type references, declarations, imports
|
|
154
157
|
result = re.sub(r'\b' + re.escape(old_name) + r'\b', new_name, source)
|
|
155
158
|
|
|
156
|
-
|
|
159
|
+
old_camel = _to_camel(old_name)
|
|
160
|
+
new_camel = _to_camel(new_name)
|
|
161
|
+
if old_camel != old_name and old_camel in result:
|
|
162
|
+
result = re.sub(r'\b' + re.escape(old_camel) + r'\b', new_camel, result)
|
|
163
|
+
|
|
164
|
+
return result
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
# Matches a class/interface/enum/record declaration of a given name
|
|
168
|
+
_CLASS_DECL_RE_TMPL = r'\b(?:class|interface|enum|record)\s+{name}\b'
|
|
169
|
+
# Matches a constructor declaration: optional access modifier + ClassName + (
|
|
170
|
+
_CTOR_DECL_RE_TMPL = r'^\s*(?:(?:public|protected|private)\s+)?' + r'{name}\s*\('
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _apply_rename_refs_only(source: str, old_name: str, new_name: str) -> str:
|
|
174
|
+
"""Rename old_name→new_name in a non-source file (import/type references only).
|
|
175
|
+
|
|
176
|
+
Skips lines containing a class/interface/enum/record declaration or constructor
|
|
177
|
+
declaration of old_name, so that a class sharing the simple name in another
|
|
178
|
+
package is not corrupted.
|
|
179
|
+
"""
|
|
180
|
+
class_decl_re = re.compile(_CLASS_DECL_RE_TMPL.format(name=re.escape(old_name)))
|
|
181
|
+
ctor_decl_re = re.compile(_CTOR_DECL_RE_TMPL.format(name=re.escape(old_name)))
|
|
182
|
+
ref_re = re.compile(r'\b' + re.escape(old_name) + r'\b')
|
|
183
|
+
|
|
184
|
+
lines = source.splitlines(keepends=True)
|
|
185
|
+
result_lines = []
|
|
186
|
+
for line in lines:
|
|
187
|
+
if class_decl_re.search(line) or ctor_decl_re.search(line):
|
|
188
|
+
result_lines.append(line)
|
|
189
|
+
else:
|
|
190
|
+
result_lines.append(ref_re.sub(new_name, line))
|
|
191
|
+
result = ''.join(result_lines)
|
|
192
|
+
|
|
157
193
|
old_camel = _to_camel(old_name)
|
|
158
194
|
new_camel = _to_camel(new_name)
|
|
159
195
|
if old_camel != old_name and old_camel in result:
|
|
@@ -245,6 +281,19 @@ def rename_class(
|
|
|
245
281
|
result.old_file = str(source_file.relative_to(root)).replace("\\", "/")
|
|
246
282
|
result.new_file = str(new_file_path.relative_to(root)).replace("\\", "/")
|
|
247
283
|
|
|
284
|
+
# BUG-2: check for collision anywhere in the repo, not just same directory
|
|
285
|
+
collision = next(
|
|
286
|
+
(f for f in java_files if f.stem == new_name and f.resolve() != new_file_path.resolve()),
|
|
287
|
+
None,
|
|
288
|
+
)
|
|
289
|
+
if collision is not None:
|
|
290
|
+
collision_rel = str(collision.relative_to(root)).replace("\\", "/")
|
|
291
|
+
result.errors.append(
|
|
292
|
+
f"'{new_name}' already exists at '{collision_rel}' — "
|
|
293
|
+
f"rename would create a duplicate class name. Pass --force to override."
|
|
294
|
+
)
|
|
295
|
+
return result
|
|
296
|
+
|
|
248
297
|
if new_file_path.exists() and new_file_path != source_file:
|
|
249
298
|
result.errors.append(
|
|
250
299
|
f"Target file '{result.new_file}' already exists — aborting to avoid overwrite."
|
|
@@ -260,15 +309,18 @@ def rename_class(
|
|
|
260
309
|
result.errors.append(f"Could not read '{java_file}': {e}")
|
|
261
310
|
continue
|
|
262
311
|
|
|
263
|
-
|
|
312
|
+
is_source = java_file == source_file
|
|
313
|
+
if is_source:
|
|
314
|
+
new_text = _apply_rename(old_text, old_name, new_name)
|
|
315
|
+
else:
|
|
316
|
+
# BUG-4: use refs-only variant to avoid clobbering same-named class in other package
|
|
317
|
+
new_text = _apply_rename_refs_only(old_text, old_name, new_name)
|
|
264
318
|
if new_text == old_text:
|
|
265
319
|
continue
|
|
266
320
|
|
|
267
321
|
rel_path = str(java_file.relative_to(root)).replace("\\", "/")
|
|
268
322
|
diff = _make_diff(old_text, new_text, rel_path)
|
|
269
323
|
|
|
270
|
-
# Determine intent
|
|
271
|
-
is_source = java_file == source_file
|
|
272
324
|
if is_source:
|
|
273
325
|
intent = f"Renamed class declaration: {old_name} → {new_name}"
|
|
274
326
|
else:
|
|
@@ -202,8 +202,9 @@ _SECURITY_MARKER_ANNOTATIONS: frozenset[str] = frozenset({
|
|
|
202
202
|
# is expected and does NOT mean endpoints are unprotected.
|
|
203
203
|
_FILTER_SECURITY_ANNOTATIONS: frozenset[str] = frozenset({
|
|
204
204
|
"@EnableWebSecurity",
|
|
205
|
-
|
|
206
|
-
|
|
205
|
+
# @EnableMethodSecurity / @EnableGlobalMethodSecurity enable per-method annotation
|
|
206
|
+
# security (@PreAuthorize/@Secured), NOT a filter chain — must NOT be treated as
|
|
207
|
+
# filter_based or SEC-001 is suppressed for every unannotated endpoint.
|
|
207
208
|
})
|
|
208
209
|
|
|
209
210
|
# Programmatic security: method-call patterns that indicate runtime auth enforcement.
|
|
@@ -2893,6 +2894,48 @@ def build_repo_ir(
|
|
|
2893
2894
|
)
|
|
2894
2895
|
ir = _assemble(all_symbols, unique_relations, all_changed, spring_summary, route_diffs_arg)
|
|
2895
2896
|
|
|
2897
|
+
# BUG-7: XML Spring Security detection for the canonical CIR pipeline.
|
|
2898
|
+
# _assemble only sees Java symbols — XML config is invisible to it.
|
|
2899
|
+
# Scan here (where root is available) and retag route_surface entries so
|
|
2900
|
+
# build_canonical_ir produces correct CanonicalEndpoint.security values.
|
|
2901
|
+
_xml_sec_re = re.compile(
|
|
2902
|
+
r'(?:xmlns(?::[a-z]+)?="http://www\.springframework\.org/schema/security"'
|
|
2903
|
+
r'|<security:http\b'
|
|
2904
|
+
r'|<http\s[^>]*use-expressions'
|
|
2905
|
+
r'|spring-security-[2345]'
|
|
2906
|
+
r'|xmlns:security="http://www\.springframework\.org/schema/security")',
|
|
2907
|
+
re.IGNORECASE,
|
|
2908
|
+
)
|
|
2909
|
+
_xml_sec_detected = False
|
|
2910
|
+
for _xml_glob in (
|
|
2911
|
+
"*security*.xml", "*Security*.xml",
|
|
2912
|
+
"*applicationContext*.xml", "*-context.xml", "*Context.xml",
|
|
2913
|
+
"*spring*.xml", "*Spring*.xml",
|
|
2914
|
+
):
|
|
2915
|
+
for _xf in root.rglob(_xml_glob):
|
|
2916
|
+
if "target/" in str(_xf).replace("\\", "/"):
|
|
2917
|
+
continue
|
|
2918
|
+
try:
|
|
2919
|
+
_xt = _xf.read_text(encoding="utf-8", errors="replace")
|
|
2920
|
+
except OSError:
|
|
2921
|
+
continue
|
|
2922
|
+
if _xml_sec_re.search(_xt):
|
|
2923
|
+
_xml_sec_detected = True
|
|
2924
|
+
break
|
|
2925
|
+
if _xml_sec_detected:
|
|
2926
|
+
break
|
|
2927
|
+
if _xml_sec_detected:
|
|
2928
|
+
_sec_model = ir.get("security_model", "unknown")
|
|
2929
|
+
if _sec_model == "unknown":
|
|
2930
|
+
ir["security_model"] = "xml_or_filter_chain"
|
|
2931
|
+
elif _sec_model in ("annotation_based", "mixed"):
|
|
2932
|
+
ir["security_model"] = "mixed"
|
|
2933
|
+
# Retag route_surface entries that have no security (would become none_detected in CIR)
|
|
2934
|
+
for _r in ir.get("route_surface") or []:
|
|
2935
|
+
_r_sec = _r.get("security_annotations")
|
|
2936
|
+
if _r_sec is None or (isinstance(_r_sec, dict) and _r_sec.get("policy") == "none_detected"):
|
|
2937
|
+
_r["security_annotations"] = {"policy": "xml_or_filter_chain"}
|
|
2938
|
+
|
|
2896
2939
|
# L-6: inject analysis_meta — files_read, lines_read, symbols_analyzed, token_estimate
|
|
2897
2940
|
ir["analysis_meta"] = {
|
|
2898
2941
|
"files_read": _meta_files_read,
|
|
@@ -3358,13 +3401,18 @@ def extract_java_endpoints(root: Path) -> "dict[str, Any]":
|
|
|
3358
3401
|
if _xml_security_detected:
|
|
3359
3402
|
break
|
|
3360
3403
|
|
|
3361
|
-
if _xml_security_detected
|
|
3362
|
-
|
|
3363
|
-
#
|
|
3364
|
-
#
|
|
3404
|
+
if _xml_security_detected:
|
|
3405
|
+
# Re-tag per-endpoint none_detected → xml_or_filter_chain regardless of security_model.
|
|
3406
|
+
# BUG-7 fix: previously only ran when model == "unknown", causing false-positive SEC-001
|
|
3407
|
+
# when annotation security (@PreAuthorize) coexisted with XML security config.
|
|
3365
3408
|
for ep in endpoints:
|
|
3366
3409
|
if ep.get("security", {}).get("policy") == "none_detected":
|
|
3367
3410
|
ep["security"] = {"policy": "xml_or_filter_chain"}
|
|
3411
|
+
if security_model == "unknown":
|
|
3412
|
+
security_model = "xml_or_filter_chain"
|
|
3413
|
+
elif security_model in ("annotation_based", "mixed"):
|
|
3414
|
+
security_model = "mixed"
|
|
3415
|
+
# filter_based stays filter_based — XML + filter chain is still filter_based
|
|
3368
3416
|
# Recompute no_security_signal (now counts only truly unknown endpoints)
|
|
3369
3417
|
no_security_signal = sum(
|
|
3370
3418
|
1 for e in endpoints
|
|
@@ -3395,7 +3443,11 @@ def find_java_files(root: Path, *, max_files: int = 8000, limitations: list[str]
|
|
|
3395
3443
|
continue
|
|
3396
3444
|
parts = rel.split("/")
|
|
3397
3445
|
# Skip test dirs
|
|
3398
|
-
if
|
|
3446
|
+
if (
|
|
3447
|
+
"/src/test/" in rel or rel.startswith("src/test/")
|
|
3448
|
+
or "/src/tests/" in rel or rel.startswith("src/tests/")
|
|
3449
|
+
or rel.startswith("test/") or rel.startswith("tests/")
|
|
3450
|
+
):
|
|
3399
3451
|
continue
|
|
3400
3452
|
# Skip vendor/generated/build dirs
|
|
3401
3453
|
if any(part in _VENDOR_DIRS for part in parts[:-1]):
|
|
@@ -41,6 +41,8 @@ _CALL_SKIP: frozenset[str] = frozenset({"annotated_with", "mapped_to", "containe
|
|
|
41
41
|
_BEAN_ANNOTATIONS: frozenset[str] = frozenset({
|
|
42
42
|
"@Component", "@Service", "@Repository",
|
|
43
43
|
"@Controller", "@RestController", "@Configuration", "@Bean",
|
|
44
|
+
# JPA persistence annotations — not Spring beans but need stereotype recognition in explain
|
|
45
|
+
"@Entity", "@MappedSuperclass", "@Embeddable",
|
|
44
46
|
})
|
|
45
47
|
|
|
46
48
|
_GENERIC_PARAM_RE = re.compile(r"<[A-Z][\w,\s<>?]*>")
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|