sourcecode 3.0.0__tar.gz → 3.1.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-3.0.0 → sourcecode-3.1.1}/CHANGELOG.md +124 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/PKG-INFO +3 -3
- {sourcecode-3.0.0 → sourcecode-3.1.1}/README.md +2 -2
- {sourcecode-3.0.0 → sourcecode-3.1.1}/pyproject.toml +1 -1
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/__init__.py +1 -1
- sourcecode-3.1.1/src/sourcecode/chain_rules.py +289 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/cli.py +138 -6
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/migrate_check.py +22 -1
- sourcecode-3.1.1/src/sourcecode/migration_blast.py +232 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/parse_cache.py +24 -5
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/posture.py +373 -31
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/repository_ir.py +75 -15
- sourcecode-3.1.1/src/sourcecode/spring_properties.py +217 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/.github/workflows/build-windows.yml +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/.gitignore +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/.ruff.toml +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/CONTRIBUTING.md +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/LICENSE +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/SECURITY.md +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/raw +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/scripts/compare_integration_engines.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/scripts/customer_smoke_test.sh +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/scripts/generate_jdk_exports.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/scripts/perf_harness.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/adaptive_scanner.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/archetype.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/architectural_baseline.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/architectural_delta.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/architecture_analyzer.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/architecture_summary.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/ast_extractor.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/cache.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/call_surface.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/caller_metrics.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/canonical_ir.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/change_plan.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/cir_graphs.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/classifier.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/code_notes_analyzer.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/compare.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/confidence_analyzer.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/constraint_diff.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/context_cache.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/context_graph.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/context_scorer.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/context_summarizer.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/contract_diff.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/contract_model.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/contract_pipeline.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/coverage_parser.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/dependency_analyzer.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/deployment_prefix.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/detectors/__init__.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/detectors/base.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/detectors/csproj_parser.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/detectors/dart.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/detectors/dotnet.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/detectors/elixir.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/detectors/go.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/detectors/heuristic.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/detectors/hybrid.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/detectors/java.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/detectors/jvm_ext.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/detectors/nodejs.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/detectors/parsers.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/detectors/php.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/detectors/project.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/detectors/python.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/detectors/ruby.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/detectors/rust.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/detectors/systems.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/detectors/terraform.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/detectors/tooling.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/doc_analyzer.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/dynamic_argument_surface.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/endpoint_literals.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/endpoint_metrics.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/entrypoint_classifier.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/env_analyzer.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/envelope.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/error_schema.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/evidence_provider.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/explain.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/file_chunker.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/file_classifier.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/filter_surface.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/format_contract.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/fqn_utils.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/git_analyzer.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/graph_analyzer.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/graph_evidence.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/hibernate_strat.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/jdk_exports.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/license.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/mcp/__init__.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/mcp/onboarding/applier.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/mcp/onboarding/backup.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/mcp/onboarding/detector.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/mcp/onboarding/planner.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/mcp/orchestrator.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/mcp/registry.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/mcp/runner.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/mcp/server.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/mcp_nudge.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/metrics_analyzer.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/openapi_surface.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/output_budget.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/path_filters.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/perf.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/pr_comment_renderer.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/pr_impact.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/prepare_context.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/progress.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/ranking_engine.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/readiness_timeline.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/reconciliation.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/redactor.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/relevance_scorer.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/rename_refactor.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/repo_classifier.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/retrieval/__init__.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/retrieval/context.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/retrieval/errors.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/retrieval/executor.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/retrieval/planner.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/retrieval/query.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/retrieval/request.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/retrieval/resolution.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/retrieval/result.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/retrieval/retriever.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/retrieval/runtime.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/retrieval/steps.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/retrieval/steps_endpoint.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/retrieval/steps_graph.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/retrieval/steps_impact.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/retrieval/steps_intf.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/retrieval/steps_struct.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/retrieval/steps_txsec.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/ris.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/runtime_classifier.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/scanner.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/schema.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/schemas/envelope-v1.schema.json +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/security_config.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/security_posture.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/semantic_analyzer.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/semantic_impact_engine.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/semantic_integration_engine.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/semantic_services.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/serializer.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/spring_event_topology.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/spring_findings.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/spring_impact.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/spring_model.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/spring_profiles.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/spring_security_audit.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/spring_semantic.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/spring_tx_analyzer.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/summarizer.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/telemetry/__init__.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/telemetry/config.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/telemetry/consent.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/telemetry/events.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/telemetry/filters.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/telemetry/transport.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/token_estimate.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/tree_utils.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/type_usage_surface.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/validation_inference.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/validation_surface.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/verify_edit.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/verify_repo.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/verify_rules.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/version_check.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/src/sourcecode/workspace.py +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/supabase/functions/README.md +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/supabase/functions/get-license/index.ts +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/supabase/functions/lemonsqueezy-webhook/index.ts +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/supabase/functions/telemetry/index.ts +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/supabase/sql/license_event_ordering.sql +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/supabase/sql/licensing_schema.sql +0 -0
- {sourcecode-3.0.0 → sourcecode-3.1.1}/supabase/sql/telemetry_events.sql +0 -0
|
@@ -1,5 +1,129 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [Unreleased]
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **`migrate-check --blast-radius` states the regression scope behind each blocker.** The
|
|
8
|
+
finding list says what must change; a migration plan is built around what has to be
|
|
9
|
+
re-tested when that change lands. Each affected product file now resolves to the types
|
|
10
|
+
it declares and runs through the same caller traversal `impact-chain` uses, so the
|
|
11
|
+
block reports the HTTP endpoints whose call path passes through it, ranked. Measured on
|
|
12
|
+
BroadleafCommerce: 62.3% of the request surface (137 of 220 endpoints) runs through a
|
|
13
|
+
file with a finding, and 52 of the 166 affected files carry all of it. Implemented as a
|
|
14
|
+
rule over the existing IR — no new detector, ~3 ms per affected file on top of an IR
|
|
15
|
+
build shared with `impact-chain` / `explain` through the context cache.
|
|
16
|
+
- Reach is regression scope, never build breakage: a `javax→jakarta` finding fails
|
|
17
|
+
compilation whether or not any endpoint reaches it, and the payload says so.
|
|
18
|
+
- Unresolved is not zero — a file whose declared types the IR never resolved is listed
|
|
19
|
+
in `files_unresolved` with reach *unknown*.
|
|
20
|
+
- A repository the IR resolves no endpoints in (alfresco's webscript framework,
|
|
21
|
+
openmrs-core) gets `request_surface_share: null` and a note that ranking fell back to
|
|
22
|
+
caller counts, never a manufactured `0.0`.
|
|
23
|
+
- Opt-in and additive: without the flag the report is byte-identical to before, and the
|
|
24
|
+
key is absent rather than `null`.
|
|
25
|
+
|
|
26
|
+
## [3.1.1] — 2026-07-28
|
|
27
|
+
|
|
28
|
+
Documentation only; no behaviour changed.
|
|
29
|
+
|
|
30
|
+
- **`contracts.md` gains an enforcement-maturity section**: which contract kinds have been
|
|
31
|
+
run against real code and with what result, the one false block found and fixed (a
|
|
32
|
+
fully-qualified security annotation reading as no guard, 3.1.0), and what is still
|
|
33
|
+
unproven — the GitHub Actions workflow has never run in a hosted runner. Plus a
|
|
34
|
+
three-week rollout that starts advisory (`--fail-on never`), because a contract whose
|
|
35
|
+
first act is blocking a merge gets deleted.
|
|
36
|
+
- **`migrate-check.md` documents the output-size guard** added in 3.1.0: the measured
|
|
37
|
+
sizes that motivated it, the emitted `OUTPUT_TOO_LARGE` payload, and the four ways past
|
|
38
|
+
it in the order worth trying.
|
|
39
|
+
- Doc-accuracy tests cover the new claims: the rollout flags exist, every documented
|
|
40
|
+
contract kind is implemented, and the quoted guard hint is the one the code emits.
|
|
41
|
+
|
|
42
|
+
## [3.1.0] — 2026-07-28
|
|
43
|
+
|
|
44
|
+
**Posture answers about requests, not only about beans.** Milestone M4 of the strategic
|
|
45
|
+
roadmap: the conditional-resolution kernel now decides `@ConditionalOnProperty` from the
|
|
46
|
+
repository's own configuration, and the request chain a profile set activates is read down
|
|
47
|
+
to per-endpoint access — with the rule that decided each endpoint cited by file and line.
|
|
48
|
+
Plus the field-defect queue: a fully-qualified annotation was invisible to the whole IR,
|
|
49
|
+
`@Bean` bodies were skipped by every body scan, the custom-gate label disagreed across
|
|
50
|
+
three surfaces, the parse cache served pre-fix parses, and `migrate-check` emitted 900KB
|
|
51
|
+
to stdout.
|
|
52
|
+
|
|
53
|
+
### Added
|
|
54
|
+
|
|
55
|
+
- **`posture` resolves `@ConditionalOnProperty`** against the properties the repository's
|
|
56
|
+
own configuration sets under the same profile set — `application.yml` /
|
|
57
|
+
`application.properties`, the `application-{profile}` filename convention, and
|
|
58
|
+
multi-document `spring.config.activate.on-profile`. A profile-activated document
|
|
59
|
+
outranks an unconditional one. Previously every property condition was reported
|
|
60
|
+
unresolved, which on a property-driven repository was most of the answer.
|
|
61
|
+
- **`posture --property key=value`** (repeatable) states a deployment value the
|
|
62
|
+
repository's files do not carry; it outranks every file. Measured on jobrunr's Spring
|
|
63
|
+
Boot starter: `--property jobrunr.database.type=mem` turns 4 undecided storage
|
|
64
|
+
auto-configurations into 1 active and 3 inactive, each citing the value that decided it.
|
|
65
|
+
- **`properties` block in the payload** — which configuration files were read, how many
|
|
66
|
+
keys resolved, which keys nobody could decide, and which values the caller supplied.
|
|
67
|
+
- **`posture` resolves what the active filter chain permits, per endpoint.** The
|
|
68
|
+
authorization DSL of the chain configuration a profile set activates
|
|
69
|
+
(`requestMatchers` / `antMatchers` / `mvcMatchers` / `anyRequest` and the access method
|
|
70
|
+
that decides them) is read in declaration order, and each endpoint is resolved against
|
|
71
|
+
the first rule that matches it — `endpoints.effective_access`, with the deciding rule's
|
|
72
|
+
file and line cited for every stated endpoint. HTTP methods on a rule are honoured.
|
|
73
|
+
- **`posture --diff` states the endpoint-level security delta** (`endpoint_access`): the
|
|
74
|
+
requests two profile sets decide differently, with the count that require no
|
|
75
|
+
authentication on one side and do on the other. Bean-level movement was the previous
|
|
76
|
+
answer; this is the one a reviewer acts on.
|
|
77
|
+
|
|
78
|
+
### Fixed
|
|
79
|
+
|
|
80
|
+
- **A fully-qualified annotation is the same annotation.** `@a.b.Foo` and `@Foo` are one
|
|
81
|
+
annotation to the compiler, but the IR stored the qualified token and matched a
|
|
82
|
+
simple-name vocabulary against it. A controller written as
|
|
83
|
+
`@org.springframework.web.bind.annotation.RestController` produced **zero endpoints**,
|
|
84
|
+
and a handler guarded with `@jakarta.annotation.security.RolesAllowed` read as carrying
|
|
85
|
+
no guard — which an endpoint-security contract then reported as a violation on a
|
|
86
|
+
guarded endpoint. Found by field-testing `require_endpoint_security` on
|
|
87
|
+
spring-petclinic. A/B on six battery repos: endpoint and security counts unchanged, so
|
|
88
|
+
this recovers a blind spot without widening any claim.
|
|
89
|
+
- **The parse cache keys on the analyzer, not only on the file.** It was described as
|
|
90
|
+
content-addressed and never stale, but the content it addressed was the source file
|
|
91
|
+
alone: after any change to symbol extraction, an updated install kept serving pre-fix
|
|
92
|
+
parses for every unchanged file, so a shipped fix read as inert until the cache was
|
|
93
|
+
cleared by hand — exactly the failure the whole-CIR cache was fixed for. The key now
|
|
94
|
+
carries `analyzer_fingerprint()`.
|
|
95
|
+
- **`migrate-check` guards its output size**, the way `repo-ir` already did. A default
|
|
96
|
+
run emitted 909KB on openmrs and 702KB on keycloak (~227K and ~175K tokens) straight to
|
|
97
|
+
stdout — past every context window it is meant to be pasted into, and only discovered
|
|
98
|
+
after the wait. Over ~50K estimated tokens it now stops with `OUTPUT_TOO_LARGE` naming
|
|
99
|
+
the four ways out (`--compact`, `--min-severity high`, `--output`, `--force`), and warns
|
|
100
|
+
from ~10K. Writing to a file is exempt; `--compact` on openmrs is 23KB.
|
|
101
|
+
- **The custom-gate label agrees across every surface.** A repository whose authorization
|
|
102
|
+
runs through its own annotation read `custom_annotation_based` from `endpoints`,
|
|
103
|
+
`unknown` from `spring-audit`, and `evidence_absent` from `retrieve endpoint-security`
|
|
104
|
+
— same repo, same evidence, three answers. The gate predicate was applied to the
|
|
105
|
+
`endpoints` surface only; the assembled surface every CIR consumer reads never saw it,
|
|
106
|
+
so those endpoints arrived carrying no security policy at all. It is now applied once,
|
|
107
|
+
at the seam all surfaces share, and the inferred gate is named in
|
|
108
|
+
`security_model_detail.custom_spi_detected`. Measured on the battery: no repository's
|
|
109
|
+
model changed — the declared-in-repo discriminator still rejects library annotations.
|
|
110
|
+
- **`@Bean` method bodies are scanned like any other body.** `@Bean` classifies a symbol
|
|
111
|
+
as `bean` rather than `method`, and every body-level scan — literal facts, body facts,
|
|
112
|
+
guard clauses, intra-class call edges — filtered on `method`. A configuration class's
|
|
113
|
+
own body was therefore invisible, which is precisely where datasource URLs, directory
|
|
114
|
+
URIs and client base paths are written. Measured on a clean cache: spring-petclinic
|
|
115
|
+
recovered 67 → 128 literal atoms across 23 → 36 owners; jobrunr gained 20 body-fact
|
|
116
|
+
owners. A `@Bean` method is also a call target now (a configuration wiring one bean
|
|
117
|
+
from another calls it by name).
|
|
118
|
+
- **Conditions are ANDed the way Spring ANDs them.** A bean excluded by its `@Profile` is
|
|
119
|
+
now `inactive` even when another condition on it is undecided; it was reported
|
|
120
|
+
unresolved, which hid a bean that provably does not register.
|
|
121
|
+
|
|
122
|
+
Unresolved stays first-class: a key no configuration source sets, a `${…}` placeholder,
|
|
123
|
+
two documents disagreeing at the same precedence, and arguments the parser does not read
|
|
124
|
+
are each reported unresolved with the key named — `matchIfMissing` is *reported*, never
|
|
125
|
+
applied, because a key absent from the repository is not a key absent from the deployment.
|
|
126
|
+
|
|
3
127
|
## [3.0.0] — 2026-07-28
|
|
4
128
|
|
|
5
129
|
**One answer per question, a contract around every answer, and a gate that can be
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sourcecode
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.1.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
|
|
@@ -139,7 +139,7 @@ The system map: module graph, dependency views, REST surface, per-class summarie
|
|
|
139
139
|
→ [reference](docs/USER_GUIDE.md#core-commands)
|
|
140
140
|
|
|
141
141
|
### 4 · Migration & Modernization
|
|
142
|
-
Is this codebase ready to upgrade? Per-dimension readiness (Jakarta / Spring Boot / JDK / Hibernate), located blockers,
|
|
142
|
+
Is this codebase ready to upgrade? Per-dimension readiness (Jakarta / Spring Boot / JDK / Hibernate), located blockers, an effort estimate, and — with `--blast-radius` — the endpoints whose call path runs through each blocker, so the re-test plan is ordered by regression scope.
|
|
143
143
|
`ask migrate-check` · `ask modernize`
|
|
144
144
|
→ [migrate-check reference](docs/migrate-check.md) · [MODERNIZATION.md](docs/MODERNIZATION.md)
|
|
145
145
|
|
|
@@ -233,7 +233,7 @@ Matching endpoints report `policy: "custom"` and drop out of the `no_security_si
|
|
|
233
233
|
|-----|----------------|
|
|
234
234
|
| [USER_GUIDE.md](docs/USER_GUIDE.md) | Full command reference, flags, output schema, workflows |
|
|
235
235
|
| [contracts.md](docs/contracts.md) | Declare invariants in `.ask/contracts.yml`, enforce them in the edit loop and in CI |
|
|
236
|
-
| [posture.md](docs/posture.md) | What a profile set actually wires
|
|
236
|
+
| [posture.md](docs/posture.md) | What a profile set actually wires — down to which endpoints its filter chain permits — and what could not be decided (experimental) |
|
|
237
237
|
| [migrate-check.md](docs/migrate-check.md) | Migration rule catalogue (MIG-001..043) + Hibernate stratification |
|
|
238
238
|
| [MODERNIZATION.md](docs/MODERNIZATION.md) | The modernization product: assess → understand → plan → execute |
|
|
239
239
|
| [PRODUCT_TIERS.md](docs/PRODUCT_TIERS.md) | Free vs Pro, pricing model |
|
|
@@ -101,7 +101,7 @@ The system map: module graph, dependency views, REST surface, per-class summarie
|
|
|
101
101
|
→ [reference](docs/USER_GUIDE.md#core-commands)
|
|
102
102
|
|
|
103
103
|
### 4 · Migration & Modernization
|
|
104
|
-
Is this codebase ready to upgrade? Per-dimension readiness (Jakarta / Spring Boot / JDK / Hibernate), located blockers,
|
|
104
|
+
Is this codebase ready to upgrade? Per-dimension readiness (Jakarta / Spring Boot / JDK / Hibernate), located blockers, an effort estimate, and — with `--blast-radius` — the endpoints whose call path runs through each blocker, so the re-test plan is ordered by regression scope.
|
|
105
105
|
`ask migrate-check` · `ask modernize`
|
|
106
106
|
→ [migrate-check reference](docs/migrate-check.md) · [MODERNIZATION.md](docs/MODERNIZATION.md)
|
|
107
107
|
|
|
@@ -195,7 +195,7 @@ Matching endpoints report `policy: "custom"` and drop out of the `no_security_si
|
|
|
195
195
|
|-----|----------------|
|
|
196
196
|
| [USER_GUIDE.md](docs/USER_GUIDE.md) | Full command reference, flags, output schema, workflows |
|
|
197
197
|
| [contracts.md](docs/contracts.md) | Declare invariants in `.ask/contracts.yml`, enforce them in the edit loop and in CI |
|
|
198
|
-
| [posture.md](docs/posture.md) | What a profile set actually wires
|
|
198
|
+
| [posture.md](docs/posture.md) | What a profile set actually wires — down to which endpoints its filter chain permits — and what could not be decided (experimental) |
|
|
199
199
|
| [migrate-check.md](docs/migrate-check.md) | Migration rule catalogue (MIG-001..043) + Hibernate stratification |
|
|
200
200
|
| [MODERNIZATION.md](docs/MODERNIZATION.md) | The modernization product: assess → understand → plan → execute |
|
|
201
201
|
| [PRODUCT_TIERS.md](docs/PRODUCT_TIERS.md) | Free vs Pro, pricing model |
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
"""chain_rules.py — the path rules a security filter chain declares.
|
|
2
|
+
|
|
3
|
+
`posture` could say *which* chain configuration a profile activates, never what
|
|
4
|
+
that chain permits. "Under `prod` the security configuration is active" is a fact
|
|
5
|
+
about a bean; "under `prod`, `POST /admin/**` requires ROLE_ADMIN and `/health` is
|
|
6
|
+
open to anyone" is a fact about the request, and only the second one is what a
|
|
7
|
+
reviewer is asking.
|
|
8
|
+
|
|
9
|
+
This is an EXTRACTOR: it reads Java source for the published `HttpSecurity`
|
|
10
|
+
authorization DSL (`requestMatchers` / `antMatchers` / `mvcMatchers` /
|
|
11
|
+
`anyRequest`, followed by the access method that decides them) and returns the
|
|
12
|
+
rules **in declaration order**, because Spring applies the first one that matches
|
|
13
|
+
and any other order would answer a different question.
|
|
14
|
+
|
|
15
|
+
What it deliberately does not do: evaluate `access(...)` expressions, follow
|
|
16
|
+
matchers built by a helper method, or read matchers passed as constructed
|
|
17
|
+
objects. Those are recorded as rules whose decision is *not evaluated*, so the
|
|
18
|
+
caller reports the endpoint undecided rather than silently open.
|
|
19
|
+
|
|
20
|
+
VAI: only the published Spring Security DSL vocabulary appears. Path patterns and
|
|
21
|
+
role names are DATA — carried as evidence, never branched on.
|
|
22
|
+
"""
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import re
|
|
26
|
+
from dataclasses import dataclass
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
from typing import Optional
|
|
29
|
+
|
|
30
|
+
#: Published matcher methods that introduce a rule.
|
|
31
|
+
_MATCHER_METHODS = ("requestMatchers", "antMatchers", "mvcMatchers", "regexMatchers")
|
|
32
|
+
_ANY_REQUEST = "anyRequest"
|
|
33
|
+
|
|
34
|
+
#: Published access methods → the decision they express. `access`/`hasIpAddress`
|
|
35
|
+
#: are listed so the rule is SEEN; their verdict is that they were not evaluated.
|
|
36
|
+
_ACCESS_DECISIONS = {
|
|
37
|
+
"permitAll": "permit_all",
|
|
38
|
+
"denyAll": "deny_all",
|
|
39
|
+
"authenticated": "authenticated",
|
|
40
|
+
"fullyAuthenticated": "authenticated",
|
|
41
|
+
"rememberMe": "authenticated",
|
|
42
|
+
"anonymous": "anonymous",
|
|
43
|
+
"hasRole": "role_required",
|
|
44
|
+
"hasAnyRole": "role_required",
|
|
45
|
+
"hasAuthority": "role_required",
|
|
46
|
+
"hasAnyAuthority": "role_required",
|
|
47
|
+
"access": "not_evaluated",
|
|
48
|
+
"hasIpAddress": "not_evaluated",
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
_QUOTED_RE = re.compile(r'"([^"]*)"')
|
|
52
|
+
_HTTP_METHOD_RE = re.compile(r"\bHttpMethod\s*\.\s*([A-Z]+)\b")
|
|
53
|
+
_MATCHER_CALL_RE = re.compile(
|
|
54
|
+
r"\.\s*(" + "|".join(_MATCHER_METHODS + (_ANY_REQUEST,)) + r")\s*\("
|
|
55
|
+
)
|
|
56
|
+
_ACCESS_CALL_RE = re.compile(
|
|
57
|
+
r"\s*\.\s*(" + "|".join(_ACCESS_DECISIONS) + r")\s*\("
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass(frozen=True)
|
|
62
|
+
class AccessRule:
|
|
63
|
+
"""One `matcher → access` pair, positioned in the order Spring applies it."""
|
|
64
|
+
|
|
65
|
+
patterns: "tuple[str, ...]" # empty → anyRequest (matches everything)
|
|
66
|
+
methods: "tuple[str, ...]" # empty → any HTTP method
|
|
67
|
+
decision: str
|
|
68
|
+
authorities: "tuple[str, ...]" = ()
|
|
69
|
+
source_file: str = ""
|
|
70
|
+
line: int = 0
|
|
71
|
+
order: int = 0
|
|
72
|
+
matcher: str = "" # the DSL method that declared it
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def is_any_request(self) -> bool:
|
|
76
|
+
"""`anyRequest()` — the catch-all, and the only patternless rule that
|
|
77
|
+
Spring resolves. A matcher call with no readable pattern is a different
|
|
78
|
+
thing entirely: see `paths_unknown`."""
|
|
79
|
+
return self.matcher == _ANY_REQUEST
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def paths_unknown(self) -> bool:
|
|
83
|
+
"""A matcher whose paths this parser could not read. It may cover any
|
|
84
|
+
request, so nothing after it can be concluded from the rules alone."""
|
|
85
|
+
return self.matcher != _ANY_REQUEST and not self.patterns
|
|
86
|
+
|
|
87
|
+
def to_dict(self) -> dict:
|
|
88
|
+
out: dict = {
|
|
89
|
+
"matcher": self.matcher,
|
|
90
|
+
"patterns": (
|
|
91
|
+
["**"] if self.is_any_request
|
|
92
|
+
else (list(self.patterns) or ["(paths this parser could not read)"])
|
|
93
|
+
),
|
|
94
|
+
"decision": self.decision,
|
|
95
|
+
"source_file": self.source_file,
|
|
96
|
+
"line": self.line,
|
|
97
|
+
}
|
|
98
|
+
if self.methods:
|
|
99
|
+
out["methods"] = list(self.methods)
|
|
100
|
+
if self.authorities:
|
|
101
|
+
out["authorities"] = list(self.authorities)
|
|
102
|
+
return out
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _balanced(text: str, open_index: int) -> "tuple[str, int]":
|
|
106
|
+
"""(body, index after ')') for the parenthesis opening at `open_index`.
|
|
107
|
+
|
|
108
|
+
String literals are respected so a `)` inside a pattern does not close the
|
|
109
|
+
call early. Returns ("", -1) when the call is not balanced in this file.
|
|
110
|
+
"""
|
|
111
|
+
depth = 0
|
|
112
|
+
i = open_index
|
|
113
|
+
in_string = False
|
|
114
|
+
escaped = False
|
|
115
|
+
while i < len(text):
|
|
116
|
+
char = text[i]
|
|
117
|
+
if in_string:
|
|
118
|
+
if escaped:
|
|
119
|
+
escaped = False
|
|
120
|
+
elif char == "\\":
|
|
121
|
+
escaped = True
|
|
122
|
+
elif char == '"':
|
|
123
|
+
in_string = False
|
|
124
|
+
elif char == '"':
|
|
125
|
+
in_string = True
|
|
126
|
+
elif char == "(":
|
|
127
|
+
depth += 1
|
|
128
|
+
elif char == ")":
|
|
129
|
+
depth -= 1
|
|
130
|
+
if depth == 0:
|
|
131
|
+
return text[open_index + 1:i], i + 1
|
|
132
|
+
i += 1
|
|
133
|
+
return "", -1
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def rules_from_source(source: str, source_file: str = "") -> "list[AccessRule]":
|
|
137
|
+
"""Every authorization rule in one file, in declaration order."""
|
|
138
|
+
rules: list[AccessRule] = []
|
|
139
|
+
position = 0
|
|
140
|
+
while True:
|
|
141
|
+
match = _MATCHER_CALL_RE.search(source, position)
|
|
142
|
+
if match is None:
|
|
143
|
+
break
|
|
144
|
+
matcher = match.group(1)
|
|
145
|
+
body, after = _balanced(source, match.end() - 1)
|
|
146
|
+
if after < 0:
|
|
147
|
+
break
|
|
148
|
+
access = _ACCESS_CALL_RE.match(source, after)
|
|
149
|
+
if access is None:
|
|
150
|
+
# A matcher whose access method is not adjacent (chained through a
|
|
151
|
+
# variable, or a form this parser does not read). Recorded so the
|
|
152
|
+
# caller knows the path is governed by something it could not read.
|
|
153
|
+
position = after
|
|
154
|
+
patterns = tuple(p for p in _QUOTED_RE.findall(body) if p)
|
|
155
|
+
if matcher != _ANY_REQUEST and not patterns:
|
|
156
|
+
continue
|
|
157
|
+
rules.append(AccessRule(
|
|
158
|
+
patterns=() if matcher == _ANY_REQUEST else patterns,
|
|
159
|
+
methods=tuple(sorted(set(_HTTP_METHOD_RE.findall(body)))),
|
|
160
|
+
decision="not_evaluated",
|
|
161
|
+
source_file=source_file,
|
|
162
|
+
line=source.count("\n", 0, match.start()) + 1,
|
|
163
|
+
order=len(rules),
|
|
164
|
+
matcher=matcher,
|
|
165
|
+
))
|
|
166
|
+
continue
|
|
167
|
+
method_name = access.group(1)
|
|
168
|
+
access_body, access_end = _balanced(source, access.end() - 1)
|
|
169
|
+
position = access_end if access_end > 0 else after
|
|
170
|
+
patterns = tuple(p for p in _QUOTED_RE.findall(body) if p)
|
|
171
|
+
if matcher != _ANY_REQUEST and not patterns:
|
|
172
|
+
# `requestMatchers(new AntPathRequestMatcher(...))` and friends: the
|
|
173
|
+
# rule exists, its paths are not literals in this call.
|
|
174
|
+
rules.append(AccessRule(
|
|
175
|
+
patterns=(), methods=(), decision="not_evaluated",
|
|
176
|
+
source_file=source_file,
|
|
177
|
+
line=source.count("\n", 0, match.start()) + 1,
|
|
178
|
+
order=len(rules), matcher=matcher,
|
|
179
|
+
))
|
|
180
|
+
continue
|
|
181
|
+
rules.append(AccessRule(
|
|
182
|
+
patterns=() if matcher == _ANY_REQUEST else patterns,
|
|
183
|
+
methods=tuple(sorted(set(_HTTP_METHOD_RE.findall(body)))),
|
|
184
|
+
decision=_ACCESS_DECISIONS[method_name],
|
|
185
|
+
authorities=tuple(_QUOTED_RE.findall(access_body)),
|
|
186
|
+
source_file=source_file,
|
|
187
|
+
line=source.count("\n", 0, match.start()) + 1,
|
|
188
|
+
order=len(rules),
|
|
189
|
+
matcher=matcher,
|
|
190
|
+
))
|
|
191
|
+
return rules
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def extract_chain_rules(
|
|
195
|
+
root: Path, java_files: "Optional[list[str]]" = None
|
|
196
|
+
) -> "dict[str, list[AccessRule]]":
|
|
197
|
+
"""`{repo-relative file: rules in declaration order}`. Never raises."""
|
|
198
|
+
root = Path(root)
|
|
199
|
+
if java_files is None:
|
|
200
|
+
try:
|
|
201
|
+
from sourcecode.repository_ir import find_java_files
|
|
202
|
+
|
|
203
|
+
java_files = find_java_files(root)
|
|
204
|
+
except Exception:
|
|
205
|
+
java_files = []
|
|
206
|
+
out: dict[str, list[AccessRule]] = {}
|
|
207
|
+
for rel in java_files or []:
|
|
208
|
+
try:
|
|
209
|
+
source = (root / rel).read_text(encoding="utf-8", errors="replace")
|
|
210
|
+
except OSError:
|
|
211
|
+
continue
|
|
212
|
+
# Cheap pre-check: the DSL always names one of these methods.
|
|
213
|
+
if not any(name in source for name in _MATCHER_METHODS + (_ANY_REQUEST,)):
|
|
214
|
+
continue
|
|
215
|
+
rules = rules_from_source(source, rel)
|
|
216
|
+
if rules:
|
|
217
|
+
out[rel] = rules
|
|
218
|
+
return out
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def ant_matches(pattern: str, path: str) -> bool:
|
|
222
|
+
"""Spring Ant path semantics: `?` one char, `*` one segment, `**` many.
|
|
223
|
+
|
|
224
|
+
A trailing `/**` also matches the base path itself, as Spring's matcher does
|
|
225
|
+
(`/admin/**` covers `/admin`).
|
|
226
|
+
"""
|
|
227
|
+
if not pattern or not path:
|
|
228
|
+
return False
|
|
229
|
+
if pattern == path:
|
|
230
|
+
return True
|
|
231
|
+
regex = _ant_regex(pattern)
|
|
232
|
+
if regex.match(path):
|
|
233
|
+
return True
|
|
234
|
+
if pattern.endswith("/**"):
|
|
235
|
+
return _ant_regex(pattern[:-3] or "/").match(path) is not None
|
|
236
|
+
return False
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
_ANT_CACHE: "dict[str, re.Pattern]" = {}
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _ant_regex(pattern: str) -> "re.Pattern":
|
|
243
|
+
cached = _ANT_CACHE.get(pattern)
|
|
244
|
+
if cached is not None:
|
|
245
|
+
return cached
|
|
246
|
+
out: list[str] = ["^"]
|
|
247
|
+
i = 0
|
|
248
|
+
while i < len(pattern):
|
|
249
|
+
char = pattern[i]
|
|
250
|
+
if pattern.startswith("**", i):
|
|
251
|
+
out.append(".*")
|
|
252
|
+
i += 2
|
|
253
|
+
elif char == "*":
|
|
254
|
+
out.append("[^/]*")
|
|
255
|
+
i += 1
|
|
256
|
+
elif char == "?":
|
|
257
|
+
out.append("[^/]")
|
|
258
|
+
i += 1
|
|
259
|
+
else:
|
|
260
|
+
out.append(re.escape(char))
|
|
261
|
+
i += 1
|
|
262
|
+
out.append("$")
|
|
263
|
+
compiled = re.compile("".join(out))
|
|
264
|
+
_ANT_CACHE[pattern] = compiled
|
|
265
|
+
return compiled
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def rule_matches(rule: AccessRule, method: str, path: str) -> bool:
|
|
269
|
+
"""Does this rule govern `method path`? Method-less rules cover every method."""
|
|
270
|
+
if rule.methods and method and method.upper() not in rule.methods:
|
|
271
|
+
return False
|
|
272
|
+
if rule.is_any_request:
|
|
273
|
+
return True
|
|
274
|
+
return any(ant_matches(pattern, path) for pattern in rule.patterns)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def first_matching_rule(
|
|
278
|
+
rules: "list[AccessRule]", method: str, path: str
|
|
279
|
+
) -> "Optional[AccessRule]":
|
|
280
|
+
"""The rule Spring would apply: the first declared one that matches."""
|
|
281
|
+
for rule in rules:
|
|
282
|
+
if rule.paths_unknown:
|
|
283
|
+
# A rule whose paths could not be read may cover this request, and
|
|
284
|
+
# everything after it is only reachable if it does not. Stopping here
|
|
285
|
+
# is what keeps a later `permitAll` from being reported as the answer.
|
|
286
|
+
return rule
|
|
287
|
+
if rule_matches(rule, method, path):
|
|
288
|
+
return rule
|
|
289
|
+
return None
|