sourcecode 2.6.0__py3-none-any.whl → 2.6.2__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.
- sourcecode/__init__.py +1 -1
- sourcecode/archetype.py +12 -1
- sourcecode/cli.py +229 -38
- sourcecode/context_cache.py +40 -0
- sourcecode/filter_surface.py +173 -0
- sourcecode/migrate_check.py +10 -0
- sourcecode/prepare_context.py +21 -1
- sourcecode/repository_ir.py +32 -5
- sourcecode/security_posture.py +96 -0
- sourcecode/spring_impact.py +16 -8
- {sourcecode-2.6.0.dist-info → sourcecode-2.6.2.dist-info}/METADATA +1 -1
- {sourcecode-2.6.0.dist-info → sourcecode-2.6.2.dist-info}/RECORD +15 -14
- {sourcecode-2.6.0.dist-info → sourcecode-2.6.2.dist-info}/WHEEL +0 -0
- {sourcecode-2.6.0.dist-info → sourcecode-2.6.2.dist-info}/entry_points.txt +0 -0
- {sourcecode-2.6.0.dist-info → sourcecode-2.6.2.dist-info}/licenses/LICENSE +0 -0
sourcecode/__init__.py
CHANGED
sourcecode/archetype.py
CHANGED
|
@@ -599,7 +599,18 @@ class ArchetypeClassifier:
|
|
|
599
599
|
# winner barely above the evidence floor is "undetermined-ish" and must
|
|
600
600
|
# not read as high/medium however lonely it is.
|
|
601
601
|
confident_enough = top.score >= _MIN_CONFIDENT_SCORE
|
|
602
|
-
|
|
602
|
+
# Signal diversity: "high" requires the label to be backed by MORE THAN ONE
|
|
603
|
+
# distinct signal. A lone dominant signal — however strong (e.g. a fan-in
|
|
604
|
+
# Gini alone labeling a REST API an "engine") — is a single point of view and
|
|
605
|
+
# must not read as high confidence (field-eval Fase 0).
|
|
606
|
+
distinct_signals = len({e.signal for e in top.evidence if e.contribution > 0})
|
|
607
|
+
if (
|
|
608
|
+
confident_enough
|
|
609
|
+
and rel_margin >= 0.4
|
|
610
|
+
and mass_backed
|
|
611
|
+
and graph_backed
|
|
612
|
+
and distinct_signals >= 2
|
|
613
|
+
):
|
|
603
614
|
confidence = "high"
|
|
604
615
|
elif confident_enough and rel_margin >= 0.25 and mass_backed:
|
|
605
616
|
confidence = "medium"
|
sourcecode/cli.py
CHANGED
|
@@ -160,10 +160,11 @@ def _build_help_text() -> str:
|
|
|
160
160
|
text = f"""\
|
|
161
161
|
[bold]ASK Engine[/bold] [dim]· CLI: ask[/dim] {plan_badge}
|
|
162
162
|
|
|
163
|
-
|
|
163
|
+
Deterministic Java/Spring semantics and reusable structural context for AI coding agents.
|
|
164
164
|
|
|
165
|
-
Cache warms on first scan;
|
|
166
|
-
|
|
165
|
+
Cache warms on first scan; later calls reuse pre-built context instead of rescanning.
|
|
166
|
+
Scan and warm time scale with repo size — small repos in seconds, large repos (thousands
|
|
167
|
+
of files) in minutes. Semantic analysis itself is sub-second; repo indexing dominates.
|
|
167
168
|
|
|
168
169
|
[bold]Primary usage:[/bold]
|
|
169
170
|
ask --compact high-signal summary (~2,500–4,000 tokens)
|
|
@@ -725,6 +726,31 @@ DOCS_DEPTH_CHOICES = ["module", "symbols", "full"]
|
|
|
725
726
|
# ── Module-level constants ─────────────────────────────────────────────────────
|
|
726
727
|
_FREE_TIER_NODE_CAP: int = 50 # graph/semantic node cap — applies only to large repos on free tier
|
|
727
728
|
_JAVA_MIN_SCAN_DEPTH: int = 12 # Maven src/main/java/<pkg>/<module>/File depth floor
|
|
729
|
+
_LARGE_REPO_ADVISORY_FILES: int = 3000 # cold-scan advisory threshold (files); honest expectation-setting, not a speed claim
|
|
730
|
+
|
|
731
|
+
|
|
732
|
+
def _cold_scan_advisory_message(
|
|
733
|
+
file_tree: dict,
|
|
734
|
+
*,
|
|
735
|
+
file_count: Optional[int] = None,
|
|
736
|
+
) -> Optional[str]:
|
|
737
|
+
"""Return the large-repo cold-scan advisory line, or ``None`` when the repo is
|
|
738
|
+
below :data:`_LARGE_REPO_ADVISORY_FILES`.
|
|
739
|
+
|
|
740
|
+
Honest expectation-setting (field-eval Fase 3, item E): it states the *shape* of
|
|
741
|
+
the cost — cold analysis scales with repo size, cached afterwards — and never
|
|
742
|
+
asserts a duration the tool cannot keep. Pure and TTY-agnostic: the caller owns
|
|
743
|
+
the ``sys.stderr.isatty()`` gate so this never reaches stdout (JSON) or an agent.
|
|
744
|
+
"""
|
|
745
|
+
if file_count is None:
|
|
746
|
+
from sourcecode.tree_utils import flatten_file_tree as _flatten_ft
|
|
747
|
+
file_count = sum(1 for _p in _flatten_ft(file_tree) if Path(_p).suffix)
|
|
748
|
+
if file_count < _LARGE_REPO_ADVISORY_FILES:
|
|
749
|
+
return None
|
|
750
|
+
return (
|
|
751
|
+
f"[scan] large repository (~{file_count:,} files) — cold analysis scales with "
|
|
752
|
+
"repo size and may take a while; the result is cached, so later runs are fast."
|
|
753
|
+
)
|
|
728
754
|
_JVM_STACKS: frozenset[str] = frozenset({"java", "kotlin", "scala", "groovy"})
|
|
729
755
|
_IMPACT_PRIORITY_THRESHOLDS: list[tuple[float, str]] = [
|
|
730
756
|
(0.60, "high"),
|
|
@@ -762,7 +788,7 @@ def _print_welcome_plain(tier: str) -> None:
|
|
|
762
788
|
"""Plain-text welcome — fallback when rich is unavailable."""
|
|
763
789
|
lines = ["", f" ASK Engine {__version__} · {tier} · CLI: ask",
|
|
764
790
|
" Actionable Software Knowledge Engine", "",
|
|
765
|
-
" AI coding
|
|
791
|
+
" Deep Java/Spring semantics for AI coding agents.", "", " Get started:"]
|
|
766
792
|
for cmd, desc in _WELCOME_CMDS:
|
|
767
793
|
lines.append(f" {cmd.ljust(34)}{desc}")
|
|
768
794
|
lines.append("")
|
|
@@ -812,7 +838,7 @@ def _print_welcome() -> None:
|
|
|
812
838
|
t.append(_WELCOME_ENGINE, style="bold dim cyan")
|
|
813
839
|
t.append(" Actionable Software Knowledge\n", style="dim")
|
|
814
840
|
|
|
815
|
-
t.append("\
|
|
841
|
+
t.append("\nDeep Java/Spring semantics for AI coding agents.\n\n", style="white")
|
|
816
842
|
|
|
817
843
|
for cmd, desc in _WELCOME_CMDS:
|
|
818
844
|
t.append("▸ ", style="cyan")
|
|
@@ -1483,23 +1509,60 @@ def main(
|
|
|
1483
1509
|
# Step 1: try L1 to obtain the core_hash needed for L2 key
|
|
1484
1510
|
_l1_result = _cache_mod.read_core(target, _core_key)
|
|
1485
1511
|
|
|
1486
|
-
#
|
|
1487
|
-
#
|
|
1488
|
-
#
|
|
1512
|
+
# Additive overlays (--env-map / --git-context) miss L1 because they sit in
|
|
1513
|
+
# the core key, yet neither changes the semantic core — env walks config
|
|
1514
|
+
# files, git walks history; both only ATTACH a block. So when the base
|
|
1515
|
+
# (em=False,gc=False) core exists, reuse it and inject the overlay lazily
|
|
1516
|
+
# (<1 s each) instead of triggering a full rescan. (--changed-only is a
|
|
1517
|
+
# different subsystem and is handled separately.)
|
|
1518
|
+
# Try candidate bases that differ from the current flags only by having
|
|
1519
|
+
# some subset of the requested overlays OFF, fewest flips first — so the
|
|
1520
|
+
# RICHEST already-cached core wins (e.g. with --compact --git-context, the
|
|
1521
|
+
# plain --compact core has em=True,gc=False, so we flip only gc and keep em).
|
|
1522
|
+
# Injecting a flag whose base already has it ON would corrupt the reuse, so
|
|
1523
|
+
# we inject exactly the overlays we flipped to land the hit.
|
|
1489
1524
|
_l1_needs_env_inject = False
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
_base_h8 = _hashlib.sha256(_base_flags.encode()).hexdigest()[:8]
|
|
1525
|
+
_l1_needs_git_inject = False
|
|
1526
|
+
if _l1_result is None and (env_map or git_context):
|
|
1493
1527
|
_sha_prefix = _git_sha if _git_sha else "nogit"
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1528
|
+
_flippable = []
|
|
1529
|
+
if git_context:
|
|
1530
|
+
_flippable.append("gc") # inject git is cheap + additive
|
|
1531
|
+
if env_map:
|
|
1532
|
+
_flippable.append("em")
|
|
1533
|
+
# non-empty subsets, ascending size (fewest flips = richest base first)
|
|
1534
|
+
import itertools as _it
|
|
1535
|
+
_subsets = [
|
|
1536
|
+
s for n in range(1, len(_flippable) + 1)
|
|
1537
|
+
for s in _it.combinations(_flippable, n)
|
|
1538
|
+
]
|
|
1539
|
+
for _subset in _subsets:
|
|
1540
|
+
_bf = _core_flags_str
|
|
1541
|
+
if "gc" in _subset:
|
|
1542
|
+
_bf = _bf.replace(",gc=True,", ",gc=False,")
|
|
1543
|
+
if "em" in _subset:
|
|
1544
|
+
_bf = _bf.replace(",em=True,", ",em=False,")
|
|
1545
|
+
_bk = f"{_sha_prefix}-{_hashlib.sha256(_bf.encode()).hexdigest()[:8]}"
|
|
1546
|
+
_br = _cache_mod.read_core(target, _bk)
|
|
1547
|
+
if _br is not None:
|
|
1548
|
+
_l1_result = _br
|
|
1549
|
+
_l1_needs_git_inject = "gc" in _subset
|
|
1550
|
+
_l1_needs_env_inject = "em" in _subset
|
|
1551
|
+
break
|
|
1499
1552
|
|
|
1500
1553
|
if _l1_result is not None:
|
|
1501
1554
|
_core_dict_l1, _core_hash = _l1_result
|
|
1502
1555
|
_view_key = f"{_core_hash}-{_view_h}"
|
|
1556
|
+
# On the lazy-inject path the core is a REUSED base whose _core_hash is
|
|
1557
|
+
# shared with the base's overlay-free view — suffix the view key with the
|
|
1558
|
+
# injected overlays so the injected body never collides with (or is served
|
|
1559
|
+
# from) the base view. Non-inject runs keep their exact historical key, so
|
|
1560
|
+
# existing cached views stay valid (no cross-version churn).
|
|
1561
|
+
if _l1_needs_git_inject or _l1_needs_env_inject:
|
|
1562
|
+
_view_key = (
|
|
1563
|
+
f"{_view_key}-ov:gc{int(_l1_needs_git_inject)}"
|
|
1564
|
+
f"em{int(_l1_needs_env_inject)}"
|
|
1565
|
+
)
|
|
1503
1566
|
|
|
1504
1567
|
# Step 2: try L2 (exact view match).
|
|
1505
1568
|
# Skip L2 for --changed-only: the stored view is a previous
|
|
@@ -1553,6 +1616,23 @@ def main(
|
|
|
1553
1616
|
]
|
|
1554
1617
|
except Exception:
|
|
1555
1618
|
pass # env inject failed — continue without env data
|
|
1619
|
+
# P-gc: inject git context when a gc=False base L1 was reused.
|
|
1620
|
+
# GitAnalyzer walks history only — typically <1 s. Reuses the
|
|
1621
|
+
# serializer's own compact shaping (no duplication/drift).
|
|
1622
|
+
if _rebuilt is not None and _l1_needs_git_inject and compact:
|
|
1623
|
+
try:
|
|
1624
|
+
from types import SimpleNamespace as _NS_gc
|
|
1625
|
+
from sourcecode.git_analyzer import GitAnalyzer as _GitA_gc
|
|
1626
|
+
from sourcecode.serializer import _compact_git_context as _cgc
|
|
1627
|
+
_gc_obj = _GitA_gc().analyze(
|
|
1628
|
+
target, depth=git_depth, days=git_days
|
|
1629
|
+
)
|
|
1630
|
+
_gc_block = _cgc(_NS_gc(git_context=_gc_obj))
|
|
1631
|
+
if _gc_block:
|
|
1632
|
+
_rebuilt = dict(_rebuilt)
|
|
1633
|
+
_rebuilt["git_context"] = _gc_block
|
|
1634
|
+
except Exception:
|
|
1635
|
+
pass # git inject failed — continue without git data
|
|
1556
1636
|
if _rebuilt is not None:
|
|
1557
1637
|
# Apply redaction
|
|
1558
1638
|
if not no_redact:
|
|
@@ -1729,6 +1809,17 @@ def main(
|
|
|
1729
1809
|
# 2. Filter .env and *.secret entries from file tree (SEC-02, all levels)
|
|
1730
1810
|
file_tree = filter_sensitive_files(raw_tree, redactor)
|
|
1731
1811
|
perf.stop("discovery", _perf_discovery)
|
|
1812
|
+
|
|
1813
|
+
# Cold-scan advisory (honest expectation-setting, NOT a speed claim). A cache
|
|
1814
|
+
# hit short-circuits above (line ~1647) before this scan, so reaching here means
|
|
1815
|
+
# a cold run: the full parse+analysis below scales with repo size. On a large
|
|
1816
|
+
# repo, warn once on the TTY so the wait is expected — never on stdout (JSON) and
|
|
1817
|
+
# never for agents/pipes. This does not assert a duration; it states the shape.
|
|
1818
|
+
if sys.stderr.isatty() and not no_cache:
|
|
1819
|
+
_advisory = _cold_scan_advisory_message(file_tree)
|
|
1820
|
+
if _advisory:
|
|
1821
|
+
typer.echo(_advisory, err=True)
|
|
1822
|
+
|
|
1732
1823
|
_perf_detection = perf.start()
|
|
1733
1824
|
detector = ProjectDetector(build_default_detectors())
|
|
1734
1825
|
workspace_analysis = WorkspaceAnalyzer().analyze(target, manifests)
|
|
@@ -5295,22 +5386,23 @@ def _render_spring_audit_github_comment(result: "SpringAuditResult", min_severit
|
|
|
5295
5386
|
lines.append("")
|
|
5296
5387
|
|
|
5297
5388
|
if not visible:
|
|
5389
|
+
# No findings at/above severity — but a gate-coverage escape set (a separate
|
|
5390
|
+
# signal, not a "finding") may still be worth surfacing before the footer.
|
|
5298
5391
|
lines.append(f"_No findings at or above `{min_severity}` severity._")
|
|
5299
|
-
|
|
5300
|
-
|
|
5301
|
-
|
|
5302
|
-
|
|
5303
|
-
|
|
5304
|
-
|
|
5305
|
-
|
|
5306
|
-
|
|
5307
|
-
|
|
5308
|
-
|
|
5309
|
-
|
|
5310
|
-
|
|
5311
|
-
lines.append(f"| {icon} {label} | `{f.pattern_id}` | `{short_file}` | `{short_sym}` | {title_escaped} |")
|
|
5392
|
+
else:
|
|
5393
|
+
lines += [
|
|
5394
|
+
"| Sev | Pattern | File | Symbol | Title |",
|
|
5395
|
+
"|-----|---------|------|--------|-------|",
|
|
5396
|
+
]
|
|
5397
|
+
for f in sorted(visible, key=lambda x: (SEVERITY_ORDER.get(x.severity, 3), x.source_file)):
|
|
5398
|
+
icon = _ICONS.get(f.severity, "")
|
|
5399
|
+
label = _LABELS.get(f.severity, f.severity.upper())
|
|
5400
|
+
short_file = f.source_file.split("/")[-1] if "/" in f.source_file else f.source_file
|
|
5401
|
+
short_sym = f.symbol.split(".")[-1] if "." in f.symbol else f.symbol
|
|
5402
|
+
title_escaped = f.title.replace("|", "\\|")
|
|
5403
|
+
lines.append(f"| {icon} {label} | `{f.pattern_id}` | `{short_file}` | `{short_sym}` | {title_escaped} |")
|
|
5312
5404
|
|
|
5313
|
-
|
|
5405
|
+
lines.append("")
|
|
5314
5406
|
|
|
5315
5407
|
if visible:
|
|
5316
5408
|
lines.append("<details>")
|
|
@@ -5327,6 +5419,8 @@ def _render_spring_audit_github_comment(result: "SpringAuditResult", min_severit
|
|
|
5327
5419
|
lines.append("")
|
|
5328
5420
|
lines.append("</details>")
|
|
5329
5421
|
|
|
5422
|
+
lines += _render_gate_coverage_section(result)
|
|
5423
|
+
|
|
5330
5424
|
lines += [
|
|
5331
5425
|
"",
|
|
5332
5426
|
f"_Generated by [sourcecode](https://github.com/sourcecode-ai/sourcecode) · "
|
|
@@ -5335,6 +5429,57 @@ def _render_spring_audit_github_comment(result: "SpringAuditResult", min_severit
|
|
|
5335
5429
|
return "\n".join(lines)
|
|
5336
5430
|
|
|
5337
5431
|
|
|
5432
|
+
_GATE_COVERAGE_RENDER_CAP = 25
|
|
5433
|
+
|
|
5434
|
+
|
|
5435
|
+
def _render_gate_coverage_section(result: "SpringAuditResult") -> list[str]: # type: ignore[name-defined]
|
|
5436
|
+
"""Human-readable gate-coverage block: the controller handlers that do NOT carry
|
|
5437
|
+
the auto-detected custom authorization gate — the escape set the JSON already
|
|
5438
|
+
computes, surfaced so a reviewer sees it at a glance. Never says "unsecured": a
|
|
5439
|
+
handler a servlet filter pattern covers is marked "possibly filter-covered", and the
|
|
5440
|
+
unmatched ones are flagged for review, not condemned."""
|
|
5441
|
+
gc = (result.security_posture or {}).get("gate_coverage")
|
|
5442
|
+
if not gc:
|
|
5443
|
+
return []
|
|
5444
|
+
not_covered = gc.get("not_carrying_gate", 0)
|
|
5445
|
+
gates = ", ".join(f"`{g}`" for g in gc.get("gate_annotations", [])) or "the detected gate"
|
|
5446
|
+
total = gc.get("total_controller_handlers", 0)
|
|
5447
|
+
lines: list[str] = ["", "---", ""]
|
|
5448
|
+
if not_covered == 0:
|
|
5449
|
+
lines.append(f"✅ **Gate coverage** — all {total} controller handlers carry {gates}.")
|
|
5450
|
+
return lines
|
|
5451
|
+
|
|
5452
|
+
covered = gc.get("possibly_filter_covered", 0)
|
|
5453
|
+
lines.append(
|
|
5454
|
+
f"🔓 **Gate coverage** — {not_covered} of {total} controller handlers do not carry "
|
|
5455
|
+
f"{gates}."
|
|
5456
|
+
)
|
|
5457
|
+
if gc.get("reconstructed_filter_patterns"):
|
|
5458
|
+
lines.append(
|
|
5459
|
+
f"_{covered} of those match a reconstructed servlet filter pattern "
|
|
5460
|
+
f"(possibly filter-covered); {gc.get('no_matching_filter_pattern', 0)} match none._"
|
|
5461
|
+
)
|
|
5462
|
+
lines += ["", "<details>", "<summary>Handlers without the gate</summary>", ""]
|
|
5463
|
+
lines += [
|
|
5464
|
+
"| Method | Path | Filter pattern | Policy |",
|
|
5465
|
+
"|--------|------|----------------|--------|",
|
|
5466
|
+
]
|
|
5467
|
+
handlers = gc.get("handlers_without_gate", [])
|
|
5468
|
+
for h in handlers[:_GATE_COVERAGE_RENDER_CAP]:
|
|
5469
|
+
method = (h.get("method") or "").replace("|", "\\|")
|
|
5470
|
+
path = (h.get("path") or "").replace("|", "\\|")
|
|
5471
|
+
fpat = h.get("filter_pattern_match")
|
|
5472
|
+
fcell = f"`{fpat}`" if fpat else "—"
|
|
5473
|
+
policy = (h.get("policy") or "").replace("|", "\\|")
|
|
5474
|
+
lines.append(f"| {method} | `{path}` | {fcell} | {policy} |")
|
|
5475
|
+
if len(handlers) > _GATE_COVERAGE_RENDER_CAP:
|
|
5476
|
+
lines.append(f"| … | _+{len(handlers) - _GATE_COVERAGE_RENDER_CAP} more_ | | |")
|
|
5477
|
+
lines += ["", "</details>", ""]
|
|
5478
|
+
if gc.get("note"):
|
|
5479
|
+
lines.append(f"> {gc['note']}")
|
|
5480
|
+
return lines
|
|
5481
|
+
|
|
5482
|
+
|
|
5338
5483
|
@app.command("spring-audit")
|
|
5339
5484
|
def spring_audit_cmd(
|
|
5340
5485
|
path: Path = typer.Argument(
|
|
@@ -5804,7 +5949,14 @@ def impact_chain_cmd(
|
|
|
5804
5949
|
|
|
5805
5950
|
_prog = Progress()
|
|
5806
5951
|
_prog.start(f"analyzing impact ({len(file_list)} files)")
|
|
5807
|
-
|
|
5952
|
+
# Reuse the shared context-cache CIR (same entry `cache warm` and `explain` use) so
|
|
5953
|
+
# a warmed repo skips the expensive Java parse. Best-effort — any fault falls back to
|
|
5954
|
+
# a fresh build, exactly as explain does, so impact-chain never breaks.
|
|
5955
|
+
from sourcecode import context_cache as _ctxcache
|
|
5956
|
+
try:
|
|
5957
|
+
cir, _ = _ctxcache.get_or_build_cir(_resolve_repo_root(target), target, file_list)
|
|
5958
|
+
except Exception:
|
|
5959
|
+
cir = ContextGraph.build(file_list, target).cir
|
|
5808
5960
|
_model = SpringSemanticModel.build(cir)
|
|
5809
5961
|
|
|
5810
5962
|
if query_type == "events":
|
|
@@ -8215,6 +8367,22 @@ def cache_clear_cmd(
|
|
|
8215
8367
|
typer.echo(f"Removed {removed} file(s).", err=True)
|
|
8216
8368
|
|
|
8217
8369
|
|
|
8370
|
+
def _warm_shared_cir(target: Path):
|
|
8371
|
+
"""Pre-populate the shared Canonical IR in the context cache (the entry explain and
|
|
8372
|
+
other knowledge commands reuse). Returns the KnowledgeLookup, or None on any fault —
|
|
8373
|
+
warming is best-effort and never fails the warm command."""
|
|
8374
|
+
try:
|
|
8375
|
+
from sourcecode.repository_ir import find_java_files as _fjf
|
|
8376
|
+
from sourcecode import context_cache as _ctxcache
|
|
8377
|
+
files = _fjf(target)
|
|
8378
|
+
if not files:
|
|
8379
|
+
return None
|
|
8380
|
+
_cir, look = _ctxcache.get_or_build_cir(_resolve_repo_root(target), target, files)
|
|
8381
|
+
return look
|
|
8382
|
+
except Exception:
|
|
8383
|
+
return None
|
|
8384
|
+
|
|
8385
|
+
|
|
8218
8386
|
@cache_app.command("warm")
|
|
8219
8387
|
def cache_warm_cmd(
|
|
8220
8388
|
path: Path = typer.Argument(Path("."), help="Repository path to warm (default: current directory)"),
|
|
@@ -8245,6 +8413,18 @@ def cache_warm_cmd(
|
|
|
8245
8413
|
typer.echo(result.stderr.strip(), err=True)
|
|
8246
8414
|
raise typer.Exit(code=result.returncode)
|
|
8247
8415
|
|
|
8416
|
+
# Also pre-populate the shared Canonical IR (context cache) — a DIFFERENT cache from
|
|
8417
|
+
# the L1/L2 core cache warmed above. explain (and, going forward, other knowledge
|
|
8418
|
+
# commands) reuse this CIR; without warming it here, the first explain rebuilds the
|
|
8419
|
+
# expensive Java parse despite a "warm" cache (field-eval: explain MISS after warm).
|
|
8420
|
+
_look = _warm_shared_cir(target)
|
|
8421
|
+
if _look is not None:
|
|
8422
|
+
typer.echo(
|
|
8423
|
+
f"Shared CIR {'reused' if _look.hit else 'built'} "
|
|
8424
|
+
"(explain/impact reuse this).",
|
|
8425
|
+
err=True,
|
|
8426
|
+
)
|
|
8427
|
+
|
|
8248
8428
|
|
|
8249
8429
|
@cache_app.command("freshness")
|
|
8250
8430
|
def cache_freshness_cmd(
|
|
@@ -8386,6 +8566,23 @@ def _stderr_is_interactive() -> bool:
|
|
|
8386
8566
|
return False
|
|
8387
8567
|
|
|
8388
8568
|
|
|
8569
|
+
def _force_utf8_streams() -> None:
|
|
8570
|
+
"""Force UTF-8 on stdout AND stderr so Unicode characters (em-dash, arrows, box
|
|
8571
|
+
drawing) survive on Windows where the default console codec is cp1252 (BUG-1).
|
|
8572
|
+
|
|
8573
|
+
stderr matters as much as stdout: progress/warn/gap status text is emitted there,
|
|
8574
|
+
and a cp1252 encoder turned its em-dashes into the replacement char (field-eval).
|
|
8575
|
+
Best-effort and idempotent — a stream without ``reconfigure`` (already wrapped, or a
|
|
8576
|
+
test buffer) is skipped silently.
|
|
8577
|
+
"""
|
|
8578
|
+
for stream in (sys.stdout, sys.stderr):
|
|
8579
|
+
if hasattr(stream, "reconfigure"):
|
|
8580
|
+
try:
|
|
8581
|
+
stream.reconfigure(encoding="utf-8")
|
|
8582
|
+
except Exception:
|
|
8583
|
+
pass
|
|
8584
|
+
|
|
8585
|
+
|
|
8389
8586
|
def main_entry() -> None:
|
|
8390
8587
|
"""CLI entry point for both the canonical ``ask`` command and the deprecated
|
|
8391
8588
|
``sourcecode`` compat alias — one implementation, no duplication.
|
|
@@ -8395,13 +8592,7 @@ def main_entry() -> None:
|
|
|
8395
8592
|
can consume them as positional arguments (which would prevent subcommand
|
|
8396
8593
|
routing for tokens like 'version' or 'config').
|
|
8397
8594
|
"""
|
|
8398
|
-
|
|
8399
|
-
# Windows where the default console codec is cp1252 (BUG-1).
|
|
8400
|
-
if hasattr(sys.stdout, "reconfigure"):
|
|
8401
|
-
try:
|
|
8402
|
-
sys.stdout.reconfigure(encoding="utf-8")
|
|
8403
|
-
except Exception:
|
|
8404
|
-
pass
|
|
8595
|
+
_force_utf8_streams()
|
|
8405
8596
|
# Deprecation notice when invoked through the legacy `sourcecode` alias.
|
|
8406
8597
|
# One line, and only on an interactive terminal — pipes/agents that still call
|
|
8407
8598
|
# `sourcecode` keep clean stderr (error envelopes are JSON on stderr), so the
|
sourcecode/context_cache.py
CHANGED
|
@@ -620,6 +620,46 @@ def get_or_build_cir(
|
|
|
620
620
|
return cir, KnowledgeLookup(hit=False, enabled=True, build_ms=build_ms)
|
|
621
621
|
|
|
622
622
|
|
|
623
|
+
def peek_cir(
|
|
624
|
+
repo_root: Path,
|
|
625
|
+
*,
|
|
626
|
+
since: Optional[str] = None,
|
|
627
|
+
) -> Optional[Any]:
|
|
628
|
+
"""Return the shared ``CanonicalRepositoryIR`` **only if already cached**.
|
|
629
|
+
|
|
630
|
+
Get-only sibling of :func:`get_or_build_cir`: it never builds and never
|
|
631
|
+
writes. A caller that holds only a *bounded scope* of the repo (e.g. the
|
|
632
|
+
review-pr git-first path) must use this — routing a scoped build through
|
|
633
|
+
``get_or_build_cir`` would store a truncated CIR under the repo-wide
|
|
634
|
+
knowledge key and poison every command that reads it. On a hit the caller
|
|
635
|
+
gets the full repo-wide CIR for free (O(1) reconstruction from raw IR); on
|
|
636
|
+
a miss it gets ``None`` and keeps its own scoped build, unshared.
|
|
637
|
+
|
|
638
|
+
The reconstructed CIR is left repo-consistent (``file_paths=None`` derives
|
|
639
|
+
the file list from the cached nodes, not from any caller scope).
|
|
640
|
+
"""
|
|
641
|
+
from sourcecode.canonical_ir import ir_dict_to_canonical, validate_canonical_ir
|
|
642
|
+
|
|
643
|
+
cache = ContextCache.for_repo(repo_root)
|
|
644
|
+
if not cache.enabled:
|
|
645
|
+
return None
|
|
646
|
+
key = cache.knowledge_key(SCOPE_JAVA_CIR, options={"since": since} if since else None)
|
|
647
|
+
cached = cache.get(key)
|
|
648
|
+
if cached is None:
|
|
649
|
+
return None
|
|
650
|
+
raw = cached.payload.get("raw_ir")
|
|
651
|
+
if not isinstance(raw, dict):
|
|
652
|
+
return None
|
|
653
|
+
try:
|
|
654
|
+
cir = ir_dict_to_canonical(raw, file_paths=None)
|
|
655
|
+
# validate_canonical_ir returns a LIST of problems — empty means valid.
|
|
656
|
+
if not validate_canonical_ir(cir):
|
|
657
|
+
return cir
|
|
658
|
+
except Exception:
|
|
659
|
+
pass # corrupt/incompatible payload — treat as miss
|
|
660
|
+
return None
|
|
661
|
+
|
|
662
|
+
|
|
623
663
|
# ---------------------------------------------------------------------------
|
|
624
664
|
# Module-level observability helper
|
|
625
665
|
# ---------------------------------------------------------------------------
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""filter_surface.py — servlet filter URL-pattern reconstruction (Fase 2).
|
|
2
|
+
|
|
3
|
+
Reconstructs the URL patterns that servlet filters are mapped to, from the two
|
|
4
|
+
**deterministic** sources the platform declares them in:
|
|
5
|
+
|
|
6
|
+
1. ``@WebFilter(urlPatterns=… / value=…)`` annotation arguments in ``.java``;
|
|
7
|
+
2. ``web.xml`` ``<filter-mapping><url-pattern>`` entries.
|
|
8
|
+
|
|
9
|
+
This is an EXTRACTOR (it reads source / XML), deliberately separate from the
|
|
10
|
+
Knowledge-Layer ``security_posture`` module (which never opens source). Its output
|
|
11
|
+
is a set of servlet URL patterns that the security posture then crosses against the
|
|
12
|
+
endpoint surface, so an endpoint that does NOT carry a per-method gate can still be
|
|
13
|
+
reported as *possibly covered* by a filter whose pattern matches its path — rather
|
|
14
|
+
than being over-flagged.
|
|
15
|
+
|
|
16
|
+
**Explicit scope (a measured limitation, not a bug).** Three further pattern sources
|
|
17
|
+
are NOT reconstructed here and are declared to the caller: Spring
|
|
18
|
+
``FilterRegistrationBean.addUrlPatterns()``, the ``HttpSecurity``/``requestMatchers``
|
|
19
|
+
DSL, and AspectJ ``@Aspect`` pointcuts (which are not URL patterns at all). Absence of
|
|
20
|
+
a reconstructed pattern therefore never proves an endpoint is unprotected — it means
|
|
21
|
+
"no *declarative servlet* pattern matched"; the caller frames it as review, never as
|
|
22
|
+
"unsecured".
|
|
23
|
+
"""
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import re
|
|
27
|
+
import xml.etree.ElementTree as ET
|
|
28
|
+
from dataclasses import dataclass, field
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
from typing import Optional
|
|
31
|
+
|
|
32
|
+
# @WebFilter(...) argument block. We capture the whole paren body and pull quoted
|
|
33
|
+
# tokens from the urlPatterns/value members (both are the servlet-standard members).
|
|
34
|
+
_WEBFILTER_RE = re.compile(r"@WebFilter\s*\((?P<body>[^)]*)\)", re.DOTALL)
|
|
35
|
+
_QUOTED_RE = re.compile(r'"([^"]*)"')
|
|
36
|
+
# urlPatterns=… or value=… member (either names the patterns). If neither member
|
|
37
|
+
# name is present, a bare @WebFilter("/x") uses value implicitly → all quoted tokens.
|
|
38
|
+
_MEMBER_RE = re.compile(r"(urlPatterns|value)\s*=", re.DOTALL)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class FilterSurface:
|
|
43
|
+
"""Reconstructed servlet filter URL patterns + their provenance."""
|
|
44
|
+
|
|
45
|
+
patterns: list[str] = field(default_factory=list)
|
|
46
|
+
# pattern → list of human-readable sources (filter class / web.xml) — evidence only
|
|
47
|
+
provenance: dict[str, list[str]] = field(default_factory=dict)
|
|
48
|
+
|
|
49
|
+
def is_empty(self) -> bool:
|
|
50
|
+
return not self.patterns
|
|
51
|
+
|
|
52
|
+
def matching_pattern(self, path: Optional[str]) -> Optional[str]:
|
|
53
|
+
"""The first reconstructed servlet pattern that covers ``path`` (servlet
|
|
54
|
+
matching semantics), or None. Deterministic: patterns are checked in sorted
|
|
55
|
+
order so the result is stable."""
|
|
56
|
+
if not path:
|
|
57
|
+
return None
|
|
58
|
+
for pat in sorted(self.patterns):
|
|
59
|
+
if _servlet_pattern_matches(pat, path):
|
|
60
|
+
return pat
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
def to_dict(self) -> dict:
|
|
64
|
+
return {
|
|
65
|
+
"patterns": sorted(self.patterns),
|
|
66
|
+
"provenance": {k: sorted(v) for k, v in sorted(self.provenance.items())},
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _servlet_pattern_matches(pattern: str, path: str) -> bool:
|
|
71
|
+
"""Servlet-spec URL-pattern match (SRV.12.2), the semantics @WebFilter / web.xml
|
|
72
|
+
use — NOT Ant globbing:
|
|
73
|
+
|
|
74
|
+
* ``/*`` → matches every path (catch-all);
|
|
75
|
+
* ``/prefix/*`` → path-prefix: the prefix itself or anything under it;
|
|
76
|
+
* ``*.ext`` → extension match on the final path segment;
|
|
77
|
+
* exact string → equality.
|
|
78
|
+
|
|
79
|
+
``/`` (the default-servlet mapping) is treated as exact — it does not blanket
|
|
80
|
+
every path here, to avoid over-claiming coverage.
|
|
81
|
+
"""
|
|
82
|
+
if not pattern:
|
|
83
|
+
return False
|
|
84
|
+
p = path if path.startswith("/") else "/" + path
|
|
85
|
+
if pattern == "/*":
|
|
86
|
+
return True
|
|
87
|
+
if pattern.endswith("/*"):
|
|
88
|
+
prefix = pattern[:-2] # "/api/*" → "/api"
|
|
89
|
+
return p == prefix or p.startswith(prefix + "/")
|
|
90
|
+
if pattern.startswith("*."):
|
|
91
|
+
return p.rsplit("/", 1)[-1].endswith(pattern[1:]) # "*.do" → endswith ".do"
|
|
92
|
+
return p == pattern
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _patterns_from_webfilter(source: str) -> list[str]:
|
|
96
|
+
"""Servlet URL patterns declared in ``@WebFilter(...)`` annotations in one file."""
|
|
97
|
+
out: list[str] = []
|
|
98
|
+
for m in _WEBFILTER_RE.finditer(source):
|
|
99
|
+
body = m.group("body")
|
|
100
|
+
if not body.strip():
|
|
101
|
+
continue
|
|
102
|
+
# If the body names urlPatterns=/value=, take quoted tokens AFTER that member;
|
|
103
|
+
# else (bare @WebFilter("/x")) take all quoted tokens. servletNames are not URL
|
|
104
|
+
# patterns, but they are rare and never start with '/'/'*', so filtering by shape
|
|
105
|
+
# keeps them out.
|
|
106
|
+
mem = _MEMBER_RE.search(body)
|
|
107
|
+
segment = body[mem.end():] if mem else body
|
|
108
|
+
for tok in _QUOTED_RE.findall(segment):
|
|
109
|
+
if tok and (tok.startswith("/") or tok.startswith("*.")):
|
|
110
|
+
out.append(tok)
|
|
111
|
+
return out
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _patterns_from_web_xml(xml_path: Path) -> list[str]:
|
|
115
|
+
"""Servlet URL patterns from ``<filter-mapping><url-pattern>`` in one web.xml.
|
|
116
|
+
Namespace-agnostic; never raises on malformed XML (returns what it parsed)."""
|
|
117
|
+
out: list[str] = []
|
|
118
|
+
try:
|
|
119
|
+
root = ET.parse(str(xml_path)).getroot()
|
|
120
|
+
except Exception:
|
|
121
|
+
return out
|
|
122
|
+
for elem in root.iter():
|
|
123
|
+
tag = elem.tag.rsplit("}", 1)[-1] # strip any namespace
|
|
124
|
+
if tag != "filter-mapping":
|
|
125
|
+
continue
|
|
126
|
+
for child in elem.iter():
|
|
127
|
+
if child.tag.rsplit("}", 1)[-1] == "url-pattern" and (child.text or "").strip():
|
|
128
|
+
out.append(child.text.strip())
|
|
129
|
+
return out
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def build_filter_surface(
|
|
133
|
+
root: Path, java_files: Optional[list[str]] = None
|
|
134
|
+
) -> FilterSurface:
|
|
135
|
+
"""Reconstruct the declarative servlet filter surface under ``root``.
|
|
136
|
+
|
|
137
|
+
``java_files`` are repo-relative ``.java`` paths (as ``find_java_files`` yields);
|
|
138
|
+
when omitted they are discovered. web.xml files are located by glob. Best-effort:
|
|
139
|
+
an unreadable file is skipped, never fatal.
|
|
140
|
+
"""
|
|
141
|
+
surface = FilterSurface()
|
|
142
|
+
|
|
143
|
+
if java_files is None:
|
|
144
|
+
try:
|
|
145
|
+
from sourcecode.repository_ir import find_java_files
|
|
146
|
+
java_files = find_java_files(root)
|
|
147
|
+
except Exception:
|
|
148
|
+
java_files = []
|
|
149
|
+
|
|
150
|
+
def _add(pattern: str, source: str) -> None:
|
|
151
|
+
if pattern not in surface.provenance:
|
|
152
|
+
surface.patterns.append(pattern)
|
|
153
|
+
surface.provenance[pattern] = []
|
|
154
|
+
if source not in surface.provenance[pattern]:
|
|
155
|
+
surface.provenance[pattern].append(source)
|
|
156
|
+
|
|
157
|
+
for rel in java_files:
|
|
158
|
+
abs_path = root / rel
|
|
159
|
+
try:
|
|
160
|
+
src = abs_path.read_text(encoding="utf-8", errors="replace")
|
|
161
|
+
except OSError:
|
|
162
|
+
continue
|
|
163
|
+
if "@WebFilter" not in src:
|
|
164
|
+
continue
|
|
165
|
+
for pat in _patterns_from_webfilter(src):
|
|
166
|
+
_add(pat, f"@WebFilter:{rel}")
|
|
167
|
+
|
|
168
|
+
for xml_path in sorted(root.rglob("web.xml")):
|
|
169
|
+
rel = xml_path.relative_to(root).as_posix()
|
|
170
|
+
for pat in _patterns_from_web_xml(xml_path):
|
|
171
|
+
_add(pat, f"web.xml:{rel}")
|
|
172
|
+
|
|
173
|
+
return surface
|
sourcecode/migrate_check.py
CHANGED
|
@@ -2155,6 +2155,16 @@ def run_migrate_check(
|
|
|
2155
2155
|
},
|
|
2156
2156
|
)
|
|
2157
2157
|
|
|
2158
|
+
# Stamp the shared, path-stable repo identity (the id the cache subsystem keys on)
|
|
2159
|
+
# so migrate-check no longer emits an empty repo_id while every other command emits
|
|
2160
|
+
# one. migrate-check is deliberately cir-free (its speed advantage), so it uses the
|
|
2161
|
+
# cache repo_id rather than building a CanonicalIR just to hash it.
|
|
2162
|
+
try:
|
|
2163
|
+
from sourcecode.cache import repo_id as _repo_id
|
|
2164
|
+
report.repo_id = _repo_id(root)
|
|
2165
|
+
except Exception:
|
|
2166
|
+
pass
|
|
2167
|
+
|
|
2158
2168
|
try:
|
|
2159
2169
|
import subprocess as _sub
|
|
2160
2170
|
_r = _sub.run(
|
sourcecode/prepare_context.py
CHANGED
|
@@ -1964,8 +1964,28 @@ class TaskContextBuilder:
|
|
|
1964
1964
|
if task_name == "review-pr" and _delta_files:
|
|
1965
1965
|
from sourcecode.context_graph import ContextGraph
|
|
1966
1966
|
from sourcecode.semantic_impact_engine import SemanticImpactEngine
|
|
1967
|
+
from sourcecode import context_cache as _ctxcache
|
|
1967
1968
|
_changed_sorted = sorted(_delta_files)
|
|
1968
|
-
|
|
1969
|
+
# Reuse the shared repo-wide CIR when it is ALREADY WARM: the review-pr
|
|
1970
|
+
# path builds `all_paths` as a deliberately bounded scope (changed files
|
|
1971
|
+
# + sibling dirs, no full-repo traversal), so a fresh build over it must
|
|
1972
|
+
# never reach the shared cache — `peek_cir` only reads, never builds or
|
|
1973
|
+
# stores, so it can never poison explain/impact. On a hit the engine runs
|
|
1974
|
+
# over the full-repo graph (strictly more callers, closing the DI-mediated
|
|
1975
|
+
# blind spot) at O(1); on a miss we keep the current scoped build.
|
|
1976
|
+
_graph = None
|
|
1977
|
+
try:
|
|
1978
|
+
_repo_root = self.root.resolve()
|
|
1979
|
+
while not (_repo_root / ".git").exists() and _repo_root.parent != _repo_root:
|
|
1980
|
+
_repo_root = _repo_root.parent
|
|
1981
|
+
_cached_cir = _ctxcache.peek_cir(_repo_root)
|
|
1982
|
+
if _cached_cir is not None:
|
|
1983
|
+
_graph = ContextGraph.from_cir(_cached_cir)
|
|
1984
|
+
except Exception:
|
|
1985
|
+
_graph = None
|
|
1986
|
+
if _graph is None:
|
|
1987
|
+
_graph = ContextGraph.build(all_paths, self.root)
|
|
1988
|
+
_impact_engine = SemanticImpactEngine(_graph)
|
|
1969
1989
|
_execution_paths = _impact_engine.execution_paths(_changed_sorted)
|
|
1970
1990
|
_behavioral_impact = _impact_engine.behavioral_impact(_changed_sorted)
|
|
1971
1991
|
|
sourcecode/repository_ir.py
CHANGED
|
@@ -134,7 +134,13 @@ _ANN_WITH_ARGS_RE = re.compile(
|
|
|
134
134
|
_CLASS_DECL_RE = re.compile(
|
|
135
135
|
r'(?:^|(?<=\s))'
|
|
136
136
|
r'(?P<kind>class|interface|enum|record|@interface)\s+'
|
|
137
|
-
|
|
137
|
+
# P1-H: Java identifiers may start lowercase — the uppercase-only start
|
|
138
|
+
# (PascalCase convention) silently dropped valid lowercase-initial types
|
|
139
|
+
# (Broadleaf `iFieldMetadata`, `i18nUpdateCartServiceExtensionHandler`).
|
|
140
|
+
# A literal type keyword + terminal `{` already gates the match, so widening
|
|
141
|
+
# the name-start does not admit prose/string phantoms the uppercase form
|
|
142
|
+
# was not already exposed to (verified fleet A/B, no new symbols on controls).
|
|
143
|
+
r'(?P<name>[A-Za-z]\w*)'
|
|
138
144
|
r'(?:\s*<[^{;]*?(?=>|\{))?'
|
|
139
145
|
# P1-F: record component list `record Point(int x, int y)`. `[^{;]*` is greedy
|
|
140
146
|
# to the LAST ')' before the body brace, so annotated components with
|
|
@@ -663,7 +669,22 @@ _ANN_NAME_RE = re.compile(r'^@\w+\s*')
|
|
|
663
669
|
_STRING_LITERAL_RE = re.compile(r'"(?:[^"\\]|\\.)*"|\'(?:[^\'\\]|\\.)*\'')
|
|
664
670
|
|
|
665
671
|
# Module-level cache for class-keyword detection (avoids recompilation per _extract_symbols call)
|
|
666
|
-
|
|
672
|
+
# P1-H: lowercase-initial start mirrors _CLASS_DECL_RE so multi-line lowercase-named
|
|
673
|
+
# declarations (`class i18nHandler extends A\n implements B {`) are joined too.
|
|
674
|
+
_CLASS_KW_RE = re.compile(r'\b(?:class|interface|enum|record)\s+[A-Za-z]')
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
def _blank_string_literals(text: str) -> str:
|
|
678
|
+
"""Return `text` with each string/char literal replaced by equal-length spaces.
|
|
679
|
+
|
|
680
|
+
P1-H: type-declaration scans (_CLASS_DECL_RE / _CLASS_KW_RE) run on lines whose
|
|
681
|
+
comments are stripped but whose STRING content survives. A log/message literal
|
|
682
|
+
such as `log.debug("... advice class for {} ...")` then matches `class for {`
|
|
683
|
+
and mints a phantom type `for`. Blanking the literal content (length-preserving,
|
|
684
|
+
so match offsets stay aligned with the original line) removes the false surface
|
|
685
|
+
without touching genuine declarations, which never live inside a string.
|
|
686
|
+
"""
|
|
687
|
+
return _STRING_LITERAL_RE.sub(lambda m: ' ' * len(m.group(0)), text)
|
|
667
688
|
|
|
668
689
|
|
|
669
690
|
# ---------------------------------------------------------------------------
|
|
@@ -958,7 +979,8 @@ def _extract_symbols(
|
|
|
958
979
|
_start = _i # 0-based source index where this joined entry begins
|
|
959
980
|
_line = _raw_lines[_i]
|
|
960
981
|
_stripped = _line.strip()
|
|
961
|
-
|
|
982
|
+
_kw_scan = _blank_string_literals(_stripped)
|
|
983
|
+
if (_CLASS_KW_RE.search(_kw_scan) and '{' not in _kw_scan
|
|
962
984
|
and not _stripped.startswith('//')
|
|
963
985
|
and not _stripped.startswith('*')):
|
|
964
986
|
# Continuation: join until we hit a line containing '{'
|
|
@@ -1043,7 +1065,10 @@ def _extract_symbols(
|
|
|
1043
1065
|
_pop_closed(class_stack, depth)
|
|
1044
1066
|
continue
|
|
1045
1067
|
|
|
1046
|
-
|
|
1068
|
+
# P1-H: search a string-blanked view so `class`/`interface` keywords that
|
|
1069
|
+
# appear inside string literals (log messages, templates) do not mint a
|
|
1070
|
+
# phantom type. Length-preserving, so cls_m offsets align with `stripped`.
|
|
1071
|
+
cls_m = _CLASS_DECL_RE.search(_blank_string_literals(stripped))
|
|
1047
1072
|
if cls_m:
|
|
1048
1073
|
kind_kw = cls_m.group("kind")
|
|
1049
1074
|
name = cls_m.group("name")
|
|
@@ -2311,7 +2336,9 @@ def _extract_field_types(
|
|
|
2311
2336
|
package = stripped[8:].rstrip(";").strip()
|
|
2312
2337
|
continue
|
|
2313
2338
|
net = _count_net_braces(stripped)
|
|
2314
|
-
|
|
2339
|
+
# P1-H: string-blanked search (see _blank_string_literals) — a keyword inside
|
|
2340
|
+
# a literal must not push a phantom nesting frame onto class_stack.
|
|
2341
|
+
cls_m = _CLASS_DECL_RE.search(_blank_string_literals(stripped))
|
|
2315
2342
|
if cls_m:
|
|
2316
2343
|
name = cls_m.group("name")
|
|
2317
2344
|
fqn = f"{class_stack[-1][0]}.{name}" if class_stack else (
|
sourcecode/security_posture.py
CHANGED
|
@@ -452,6 +452,10 @@ def gate_bearing_handlers(
|
|
|
452
452
|
}
|
|
453
453
|
|
|
454
454
|
|
|
455
|
+
# Cap on the explicitly-listed escape set (rollup counts are always exact).
|
|
456
|
+
_GATE_COVERAGE_LIST_CAP = 200
|
|
457
|
+
|
|
458
|
+
|
|
455
459
|
@dataclass
|
|
456
460
|
class SecurityPostureResult:
|
|
457
461
|
"""Repo-level security-posture projection (P1-A)."""
|
|
@@ -462,6 +466,11 @@ class SecurityPostureResult:
|
|
|
462
466
|
endpoint_verdicts: list[dict] = field(default_factory=list)
|
|
463
467
|
rollup: dict = field(default_factory=dict)
|
|
464
468
|
limitations: list[str] = field(default_factory=list)
|
|
469
|
+
# Gate-coverage projection (P1-A follow-up): when a custom gate IS the detected
|
|
470
|
+
# mechanism, which controller handlers do NOT carry it and are not standard-guarded
|
|
471
|
+
# — the residue that relies solely on a centralized filter, if one exists. None when
|
|
472
|
+
# no custom gate was detected (the question does not apply). Never "unsecured".
|
|
473
|
+
gate_coverage: Optional[dict] = None
|
|
465
474
|
|
|
466
475
|
def gate_simple_names(self) -> set[str]:
|
|
467
476
|
out: set[str] = set()
|
|
@@ -481,6 +490,7 @@ class SecurityPostureResult:
|
|
|
481
490
|
),
|
|
482
491
|
"endpoints": self.endpoint_verdicts,
|
|
483
492
|
"rollup": self.rollup,
|
|
493
|
+
**({"gate_coverage": self.gate_coverage} if self.gate_coverage else {}),
|
|
484
494
|
"limitations": list(self.limitations),
|
|
485
495
|
}
|
|
486
496
|
|
|
@@ -490,6 +500,7 @@ def infer_security_posture(
|
|
|
490
500
|
*,
|
|
491
501
|
root: Optional[Path] = None,
|
|
492
502
|
coverage_threshold: float = _DEFAULT_COVERAGE,
|
|
503
|
+
filter_surface: Optional[Any] = None,
|
|
493
504
|
) -> SecurityPostureResult:
|
|
494
505
|
"""Infer the repo's endpoint security posture (P1-A).
|
|
495
506
|
|
|
@@ -499,6 +510,12 @@ def infer_security_posture(
|
|
|
499
510
|
3. upgrade any that a config file confirms (Likely → Verified);
|
|
500
511
|
4. classify every endpoint into the 4-way posture with its confidence.
|
|
501
512
|
|
|
513
|
+
``filter_surface`` (a :class:`filter_surface.FilterSurface`) is an optional
|
|
514
|
+
reconstruction of declarative servlet filter URL patterns; when absent it is built
|
|
515
|
+
from ``root`` (best-effort). It is used only to *sharpen* the gate-coverage escape
|
|
516
|
+
set — marking a gate-less handler whose path a filter pattern covers — never to
|
|
517
|
+
assert protection. Parsing lives in the extractor module, not here.
|
|
518
|
+
|
|
502
519
|
Never raises; never emits a categorical "unsecured" verdict.
|
|
503
520
|
"""
|
|
504
521
|
security_model = str(cir.metadata.get("security_model", "unknown"))
|
|
@@ -535,6 +552,9 @@ def infer_security_posture(
|
|
|
535
552
|
verdicts: list[dict] = []
|
|
536
553
|
rollup = {"protected_standard": 0, "protected_custom": 0,
|
|
537
554
|
"coverage_unknown": 0, "probably_exposed": 0}
|
|
555
|
+
# escape set: handlers that do NOT carry the detected custom gate and have no
|
|
556
|
+
# standard guard of their own — the residue that relies solely on a filter.
|
|
557
|
+
escaping: list[dict] = []
|
|
538
558
|
|
|
539
559
|
for ep in cir.endpoints:
|
|
540
560
|
policy = ep.security.policy if ep.security is not None else "none_detected"
|
|
@@ -585,6 +605,22 @@ def infer_security_posture(
|
|
|
585
605
|
"policy": policy,
|
|
586
606
|
})
|
|
587
607
|
|
|
608
|
+
# escape set (only meaningful when a custom gate is the mechanism): a handler
|
|
609
|
+
# that neither carries the gate nor a standard guard nor a config-custom policy.
|
|
610
|
+
if (
|
|
611
|
+
has_custom_gate
|
|
612
|
+
and not carries_gate
|
|
613
|
+
and policy != "custom"
|
|
614
|
+
and not _is_standard_guard_policy(policy)
|
|
615
|
+
):
|
|
616
|
+
escaping.append({
|
|
617
|
+
"endpoint_id": ep.id,
|
|
618
|
+
"method": ep.method,
|
|
619
|
+
"path": ep.path,
|
|
620
|
+
"handler_symbol": ep.handler_symbol,
|
|
621
|
+
"policy": policy,
|
|
622
|
+
})
|
|
623
|
+
|
|
588
624
|
limitations = [
|
|
589
625
|
"Posture is inferred from static structure; a runtime security config "
|
|
590
626
|
"(WebSecurity DSL, XML) may protect routes this projection marks unknown.",
|
|
@@ -595,6 +631,65 @@ def infer_security_posture(
|
|
|
595
631
|
"it are reported 'coverage_unknown', never 'unsecured'."
|
|
596
632
|
)
|
|
597
633
|
|
|
634
|
+
# Gate-coverage projection: the escape set relative to the detected custom gate.
|
|
635
|
+
# Turns a "N endpoints none_detected" non-signal into a reviewable list of the
|
|
636
|
+
# handlers that do NOT carry the gate the rest of the surface uses. Honest framing:
|
|
637
|
+
# a centralized filter (if present) MAY still cover them — its per-URL patterns are
|
|
638
|
+
# not reconstructed in this increment, so the verdict is review, never "unsecured".
|
|
639
|
+
gate_coverage: Optional[dict] = None
|
|
640
|
+
if has_custom_gate:
|
|
641
|
+
# Reconstruct declarative servlet filter patterns (extractor; best-effort) and
|
|
642
|
+
# cross each escaping handler against them. A match => the handler MAY be filter-
|
|
643
|
+
# covered (still review — pattern presence is not proof of enforcement); no match
|
|
644
|
+
# => no declarative servlet pattern covers it (the sharper escape signal).
|
|
645
|
+
if filter_surface is None and root is not None:
|
|
646
|
+
try:
|
|
647
|
+
from sourcecode.filter_surface import build_filter_surface
|
|
648
|
+
filter_surface = build_filter_surface(root)
|
|
649
|
+
except Exception:
|
|
650
|
+
filter_surface = None
|
|
651
|
+
|
|
652
|
+
possibly_covered = 0
|
|
653
|
+
no_pattern = 0
|
|
654
|
+
for h in escaping:
|
|
655
|
+
match = filter_surface.matching_pattern(h.get("path")) if filter_surface else None
|
|
656
|
+
if match:
|
|
657
|
+
h["filter_pattern_match"] = match
|
|
658
|
+
possibly_covered += 1
|
|
659
|
+
else:
|
|
660
|
+
no_pattern += 1
|
|
661
|
+
|
|
662
|
+
total_handlers = len(cir.endpoints)
|
|
663
|
+
not_covered = len(escaping)
|
|
664
|
+
_patterns = (
|
|
665
|
+
filter_surface.patterns if filter_surface and not filter_surface.is_empty() else []
|
|
666
|
+
)
|
|
667
|
+
gate_coverage = {
|
|
668
|
+
"gate_annotations": sorted(gate_names),
|
|
669
|
+
"total_controller_handlers": total_handlers,
|
|
670
|
+
"gate_bearing": len(gate_bearers),
|
|
671
|
+
"not_carrying_gate": not_covered,
|
|
672
|
+
"interception_present": interception is not None,
|
|
673
|
+
"reconstructed_filter_patterns": sorted(_patterns),
|
|
674
|
+
"possibly_filter_covered": possibly_covered,
|
|
675
|
+
"no_matching_filter_pattern": no_pattern,
|
|
676
|
+
"confidence": (
|
|
677
|
+
Confidence.REQUIRES_MANUAL_REVIEW.value if not_covered else Confidence.VERIFIED.value
|
|
678
|
+
),
|
|
679
|
+
"handlers_without_gate": sorted(
|
|
680
|
+
escaping, key=lambda d: (d["path"] or "", d["method"] or "")
|
|
681
|
+
)[:_GATE_COVERAGE_LIST_CAP],
|
|
682
|
+
"note": (
|
|
683
|
+
"Handlers that do not carry the detected custom gate and have no standard "
|
|
684
|
+
"guard. 'filter_pattern_match' marks those a declarative servlet filter "
|
|
685
|
+
"(@WebFilter / web.xml) pattern covers — still review, pattern presence is "
|
|
686
|
+
"not proof of enforcement. Spring FilterRegistrationBean / HttpSecurity DSL "
|
|
687
|
+
"/ AspectJ pointcuts are NOT reconstructed, so a missing match is not proof "
|
|
688
|
+
"of exposure."
|
|
689
|
+
),
|
|
690
|
+
"truncated": not_covered > _GATE_COVERAGE_LIST_CAP,
|
|
691
|
+
}
|
|
692
|
+
|
|
598
693
|
result = SecurityPostureResult(
|
|
599
694
|
security_model=security_model,
|
|
600
695
|
custom_gates=custom_gates,
|
|
@@ -602,6 +697,7 @@ def infer_security_posture(
|
|
|
602
697
|
endpoint_verdicts=verdicts,
|
|
603
698
|
rollup=rollup,
|
|
604
699
|
limitations=limitations,
|
|
700
|
+
gate_coverage=gate_coverage,
|
|
605
701
|
)
|
|
606
702
|
# keep reference to suppress-set so callers (SEC-001) can dedupe false alarms
|
|
607
703
|
result._gate_bearing_handlers = gate_bearers # type: ignore[attr-defined]
|
sourcecode/spring_impact.py
CHANGED
|
@@ -119,7 +119,7 @@ class ImpactChainResult:
|
|
|
119
119
|
"""
|
|
120
120
|
schema_version: str = _SCHEMA_VERSION
|
|
121
121
|
symbol: str = "" # resolved FQN (or original input if not_found)
|
|
122
|
-
resolution: str = "not_found" #
|
|
122
|
+
resolution: str = "not_found" # exact | class_expanded | method_resolved | partial | not_found
|
|
123
123
|
direct_callers: list[str] = field(default_factory=list)
|
|
124
124
|
indirect_callers: list[str] = field(default_factory=list)
|
|
125
125
|
# BUG #2: count of own-class members dropped from callers (members, not callers).
|
|
@@ -378,12 +378,15 @@ def _resolve_symbol(
|
|
|
378
378
|
4. Not found.
|
|
379
379
|
|
|
380
380
|
Resolution values:
|
|
381
|
-
"exact"
|
|
382
|
-
"class_expanded"
|
|
383
|
-
|
|
384
|
-
"
|
|
385
|
-
|
|
386
|
-
|
|
381
|
+
"exact" — full FQN provided and matched exactly.
|
|
382
|
+
"class_expanded" — short class name (no method) matched one class by suffix;
|
|
383
|
+
all its symbols included. Confidence stays high.
|
|
384
|
+
"method_resolved" — short class name + method matched one method node by
|
|
385
|
+
suffix; scope is method-precise (SIM-4: not an expansion).
|
|
386
|
+
Confidence stays high.
|
|
387
|
+
"partial" — ambiguous (multiple classes matched) or method not found
|
|
388
|
+
on matched class. Confidence degrades to medium.
|
|
389
|
+
"not_found" — no match.
|
|
387
390
|
|
|
388
391
|
Returns (resolution, matched_fqns, warnings).
|
|
389
392
|
Returned list is always deduplicated (preserving order).
|
|
@@ -437,7 +440,12 @@ def _resolve_symbol(
|
|
|
437
440
|
if f"{class_input}#{method_input}" in cir_symbols:
|
|
438
441
|
resolution = "exact"
|
|
439
442
|
elif len(method_matched_classes) == 1:
|
|
440
|
-
|
|
443
|
+
# SIM-4: the caller named a method and it resolved to exactly one
|
|
444
|
+
# method node — the scope is method-precise, nothing was widened.
|
|
445
|
+
# "class_expanded" wrongly signalled "I broadened your query to the
|
|
446
|
+
# whole class" over a correct, narrow answer. Distinguish it from
|
|
447
|
+
# the genuine class-expansion case (no method given, line above).
|
|
448
|
+
resolution = "method_resolved"
|
|
441
449
|
else:
|
|
442
450
|
resolution = "partial"
|
|
443
451
|
return resolution, method_matched, []
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
sourcecode/__init__.py,sha256=
|
|
1
|
+
sourcecode/__init__.py,sha256=Xl4vKr6-4l-yWpoN7FzgqDyXd54m2LAMPUtDWX2CdD0,308
|
|
2
2
|
sourcecode/adaptive_scanner.py,sha256=yJBKjNpkY6bpueYJ2YnRezen3sYZDecEt7WaaNWdqug,9466
|
|
3
|
-
sourcecode/archetype.py,sha256=
|
|
3
|
+
sourcecode/archetype.py,sha256=UrmiONHfqLl7UfGzjECoQBO28hcM_1rvzn0hCt0EtjU,35033
|
|
4
4
|
sourcecode/architectural_baseline.py,sha256=7QzJri4pbL3nzAn9gNutZW6mZR8HHD6H2C2H1EEwYPE,17904
|
|
5
5
|
sourcecode/architectural_delta.py,sha256=E6MkyjWl-1ZgR4KSKnI785gUAfc8nq-lK-jd_rupQBU,12496
|
|
6
6
|
sourcecode/architecture_analyzer.py,sha256=GFc4ek-s1IHWM7pl-0L32WahZ93AmDgrAMcOuBKA5Dk,61463
|
|
@@ -13,12 +13,12 @@ sourcecode/canonical_ir.py,sha256=LdP_Ri3rl0M5p-MY0ypVvQwhq1WuUF1FmaHid7zyzEg,29
|
|
|
13
13
|
sourcecode/change_plan.py,sha256=MNgNyu4zrLGvBBaXPwyCh-T2ZGaUQX3Hm4W87uuhZ3w,7750
|
|
14
14
|
sourcecode/cir_graphs.py,sha256=9G0HHj1kw2325IDyzo2OpX73BNswEckecf4MZUXB4JM,12078
|
|
15
15
|
sourcecode/classifier.py,sha256=JBzPwSSrDG-tUHAbcKB678HRbjLpD-ohzbzzO62mgpo,20114
|
|
16
|
-
sourcecode/cli.py,sha256=
|
|
16
|
+
sourcecode/cli.py,sha256=kivJiIF7Rw-Eal-3_m2CG5E9i66UUuOLLyquLgUM0N0,359485
|
|
17
17
|
sourcecode/code_notes_analyzer.py,sha256=EJemNCNc9Dn-1RZYu-aNbK0ELzmsyC4s6FdHi3XyNEI,9392
|
|
18
18
|
sourcecode/compare.py,sha256=jdePg0dNCxFpysiTGMsROL5XABLjy_zVUgVeySvdb0o,7514
|
|
19
19
|
sourcecode/confidence_analyzer.py,sha256=vnbPI-20FnHdjO6STxHW8fbaxmB4A7y58io63ibFZjc,21586
|
|
20
20
|
sourcecode/constraint_diff.py,sha256=hWIO5vsxvHjsgQx23K3r_JMy1W6wQ2Ca4DqB9R_iMnY,5831
|
|
21
|
-
sourcecode/context_cache.py,sha256=
|
|
21
|
+
sourcecode/context_cache.py,sha256=bQiKlZKA-3xpvp1eWG6vOPADTl2YkYVIO434zGyl02c,26648
|
|
22
22
|
sourcecode/context_graph.py,sha256=i2kOX3XiaCqcR20ojbUnqoX-hJbMyLNYc7rl-n15bwE,46426
|
|
23
23
|
sourcecode/context_scorer.py,sha256=QpChSpsmaAYz91rXA4Ue5xzQmNz_ZboZN09YOHScq1U,14679
|
|
24
24
|
sourcecode/context_summarizer.py,sha256=cI2TZMvEhl0BEma12VtPaX6z03ZVBAetVzuK5GaCOvg,6852
|
|
@@ -38,6 +38,7 @@ sourcecode/evidence_provider.py,sha256=GSSL44JEaouO5AHks2sB3d1YvC9xIKIld1yBYxZpX
|
|
|
38
38
|
sourcecode/explain.py,sha256=HnHWVTNNf9fzeR3FP-A-eXKeKZvBcUEuODgIO3rJQkQ,22312
|
|
39
39
|
sourcecode/file_chunker.py,sha256=3vkM3mDQ5eE_yTPvUgjyjpGFBIjkW6_mrBmIbrylnA8,16444
|
|
40
40
|
sourcecode/file_classifier.py,sha256=A0fEABqtfVu1MfoaxnPAvGpZgneGgVXlJDhT74NYXxE,15314
|
|
41
|
+
sourcecode/filter_surface.py,sha256=qunis3-yY_nhOIj5K3mVFtbSyKfc6ugIX23OSxTolwE,7079
|
|
41
42
|
sourcecode/format_contract.py,sha256=U83J_LtrpMQSN3_H_HBBxpanlo7xnfvbZw7sP3ew8_M,3678
|
|
42
43
|
sourcecode/fqn_utils.py,sha256=XLU7zDkNBXz_RZkIUNfpPmp1nekWtqP-fxV92tDV1vg,2158
|
|
43
44
|
sourcecode/git_analyzer.py,sha256=JStxTQXNjBWi_wLdwhsZs9mT-v50cSJIz4Agzn6Kh9I,13362
|
|
@@ -48,14 +49,14 @@ sourcecode/jdk_exports.py,sha256=fCrlwNAXUT9gge_joq6kMnY3zJxYB2pxqy-0w3o3MJI,874
|
|
|
48
49
|
sourcecode/license.py,sha256=keFuwNxdAtvK2Ds91Wl79GMYxuxWYnN5Wbw1qBpaoUI,24896
|
|
49
50
|
sourcecode/mcp_nudge.py,sha256=yjwclsBVsDg1BtocGdEi6GYV4vzPo-SBlC9pIcNmlDM,2910
|
|
50
51
|
sourcecode/metrics_analyzer.py,sha256=m0ENgtqKeBL17kUIK3fmGkgo7UfXBNHxCMj0H_Y5K7c,22750
|
|
51
|
-
sourcecode/migrate_check.py,sha256=
|
|
52
|
+
sourcecode/migrate_check.py,sha256=hsKNwipl-bSqRsYqG1O_CLbXpMlqtnN-Bif3hpHpnjg,108676
|
|
52
53
|
sourcecode/openapi_surface.py,sha256=BTt0K-woZbkbWTN77IkqeBm_Okag9owR0848fmot8sk,16207
|
|
53
54
|
sourcecode/output_budget.py,sha256=Js9yUlfQtPhqBl9R6wn_9UHVjjJc3GtLcqyfjf5t50Q,9869
|
|
54
55
|
sourcecode/path_filters.py,sha256=qPKO7kRmVp2y9zjLNSuAVCbcpZrIInHE4QulLUlzPFI,10412
|
|
55
56
|
sourcecode/perf.py,sha256=GAcEoouPIlPMCQIcHNToxK6K3WdIR-lj9aFg4prOYJI,9743
|
|
56
57
|
sourcecode/pr_comment_renderer.py,sha256=KmcjMruhR44gjzMDJwjBSkWP9QEvh8xWBLyxzxoRbj0,14542
|
|
57
58
|
sourcecode/pr_impact.py,sha256=dCDVw83EDbyVf6F9ZmEQmsFz8ruVH7d4mpeKQCIZHM0,16805
|
|
58
|
-
sourcecode/prepare_context.py,sha256
|
|
59
|
+
sourcecode/prepare_context.py,sha256=GkdI_a0RG8Y8dh8x6SpCNKZwWi7qs8sqAbFSYJegBxY,225473
|
|
59
60
|
sourcecode/progress.py,sha256=qn30sWaHOkjTgXsSBmiPkz7Rsbwc5oSlIe6JNEMYp_k,3149
|
|
60
61
|
sourcecode/ranking_engine.py,sha256=ZAucq_YX2KkWUuAZf4P0lhtQ_38vEFnUhuGtSZd1S0E,12970
|
|
61
62
|
sourcecode/reconciliation.py,sha256=GU-1PTcVr8zcbtC7BASfpHcZndP9AdboXPNQiBc0fzo,34251
|
|
@@ -63,13 +64,13 @@ sourcecode/redactor.py,sha256=SB4hwIvg8h-hvcqKcDWaZvA-aSyn-at-BIRwa0tUv5E,3227
|
|
|
63
64
|
sourcecode/relevance_scorer.py,sha256=0AgEt4KrV73nioMqBgjhGjtY7L2C7L7cSyKtj3IKcrw,9408
|
|
64
65
|
sourcecode/rename_refactor.py,sha256=h6dNFlB9aZ_3q6heeHBkgXQeXaT03nvPSsYH6P8qxFg,12965
|
|
65
66
|
sourcecode/repo_classifier.py,sha256=FG1vaWKdWXsWdl-S8hjVMiTqcwgaRXkDyvK4rPcOGtQ,22681
|
|
66
|
-
sourcecode/repository_ir.py,sha256=
|
|
67
|
+
sourcecode/repository_ir.py,sha256=9ZGRWy8g4_PH_XvdCm6i1UOHYX-qO3sxxodXytf3m0Q,320870
|
|
67
68
|
sourcecode/ris.py,sha256=Hw8TakTQ6hku-Abf2k8954NwkrH_sP73_8wVH8x5khc,22079
|
|
68
69
|
sourcecode/runtime_classifier.py,sha256=uTAD6BDCiBLUZEDRfqk718kM4RTT_vAbfkcOI2_Xx58,18432
|
|
69
70
|
sourcecode/scanner.py,sha256=z3CV0rcGunu0Y8mpNgp07wI7nxT0pxw1BkXRRtI0Rpo,9609
|
|
70
71
|
sourcecode/schema.py,sha256=aHNXDf8LGyUC8ZDE_VS9kiskC2-Oswhi_WnpdGy6HDw,24897
|
|
71
72
|
sourcecode/security_config.py,sha256=KblMEoRiEjrIE68YsPaUAFebxFp8UM7MS7lAk5CGD8U,3531
|
|
72
|
-
sourcecode/security_posture.py,sha256=
|
|
73
|
+
sourcecode/security_posture.py,sha256=NNmYanFQZQ_hIIeTGQnvTAETMT6hxO5I56Bez0ABbBg,30945
|
|
73
74
|
sourcecode/semantic_analyzer.py,sha256=bpgdC6m0_ftVtRf3rSdwhbhWjnZnGxRXaZVcfe4BbcQ,95414
|
|
74
75
|
sourcecode/semantic_impact_engine.py,sha256=t09IirGC3JjQDy33JZd1_WKzQVKXkoNl3-XEUr5kjis,20563
|
|
75
76
|
sourcecode/semantic_integration_engine.py,sha256=rxoCuP-uM_Y4-ELeBN0sz1a88e2eI1XTJDgsS57IOLI,15513
|
|
@@ -77,7 +78,7 @@ sourcecode/semantic_services.py,sha256=nbUuPv-F01USTt_9CHT8iy_ucCIw3fz4W3Aquea_p
|
|
|
77
78
|
sourcecode/serializer.py,sha256=zmN3pOcfxaQdxCE-jKoKudFjof9Sc_q7jpcBzNIA0EY,129493
|
|
78
79
|
sourcecode/spring_event_topology.py,sha256=5_ON_21Le5zbG-1GRc5GLIi5HJfy_QjcXLVPC5WeUGQ,18055
|
|
79
80
|
sourcecode/spring_findings.py,sha256=EX7kLZLN74CFyR9iZPm3CI115BfFKNd4WRPuErNljZM,5729
|
|
80
|
-
sourcecode/spring_impact.py,sha256=
|
|
81
|
+
sourcecode/spring_impact.py,sha256=ATOa7jOg7kL78vW0uZiGBvtB-ph9bCiEGH0kRtABHxg,73108
|
|
81
82
|
sourcecode/spring_model.py,sha256=zOAgFmrRbG4a6KLm1TJl55aWMyPNsz3OS3FSczqPG6A,16594
|
|
82
83
|
sourcecode/spring_security_audit.py,sha256=Rk-aSohezdc7YDYbSoJquVnwpkDB8ty1BCD-4Hc4R5A,22832
|
|
83
84
|
sourcecode/spring_semantic.py,sha256=jteQ1PkY9ArFJv0embg_jBIdbOxqrk9mQ2Xz8OF_FKA,14214
|
|
@@ -144,8 +145,8 @@ sourcecode/telemetry/consent.py,sha256=LIAO9ohJZF8OuZwM4u1VWtALlYfTCCKq4wV3Vwc7i
|
|
|
144
145
|
sourcecode/telemetry/events.py,sha256=LtzYfaX9Ilckj5PTvAcTpDa9mLqDsYPDUiDkRa58piY,2580
|
|
145
146
|
sourcecode/telemetry/filters.py,sha256=NHa5T-6DaZduQPFuC34jOqHWQgSizM-Ygq8aZ4j19ng,5834
|
|
146
147
|
sourcecode/telemetry/transport.py,sha256=4gGHsq0WeY9VywEZXA3vUxykfiYnw9uuqfjAAec7F8o,1681
|
|
147
|
-
sourcecode-2.6.
|
|
148
|
-
sourcecode-2.6.
|
|
149
|
-
sourcecode-2.6.
|
|
150
|
-
sourcecode-2.6.
|
|
151
|
-
sourcecode-2.6.
|
|
148
|
+
sourcecode-2.6.2.dist-info/METADATA,sha256=7dMltG6Rgrg_RmRX5tEh13LJZIYv1ouwALVAcjHHY2I,10851
|
|
149
|
+
sourcecode-2.6.2.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
150
|
+
sourcecode-2.6.2.dist-info/entry_points.txt,sha256=-JEAdChrK5We51kZcb7OaDcyil-dHBjBPL-NhuO-QY8,89
|
|
151
|
+
sourcecode-2.6.2.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
|
|
152
|
+
sourcecode-2.6.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|