sourcecode 1.36.2__tar.gz → 1.36.3__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.
Files changed (118) hide show
  1. {sourcecode-1.36.2 → sourcecode-1.36.3}/PKG-INFO +18 -1
  2. {sourcecode-1.36.2 → sourcecode-1.36.3}/README.md +17 -0
  3. {sourcecode-1.36.2 → sourcecode-1.36.3}/pyproject.toml +1 -1
  4. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/__init__.py +1 -1
  5. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/canonical_ir.py +12 -0
  6. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/cli.py +12 -0
  7. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/repository_ir.py +96 -13
  8. sourcecode-1.36.3/src/sourcecode/security_config.py +99 -0
  9. {sourcecode-1.36.2 → sourcecode-1.36.3}/.github/workflows/build-windows.yml +0 -0
  10. {sourcecode-1.36.2 → sourcecode-1.36.3}/.gitignore +0 -0
  11. {sourcecode-1.36.2 → sourcecode-1.36.3}/.ruff.toml +0 -0
  12. {sourcecode-1.36.2 → sourcecode-1.36.3}/CHANGELOG.md +0 -0
  13. {sourcecode-1.36.2 → sourcecode-1.36.3}/CONTRIBUTING.md +0 -0
  14. {sourcecode-1.36.2 → sourcecode-1.36.3}/LICENSE +0 -0
  15. {sourcecode-1.36.2 → sourcecode-1.36.3}/SECURITY.md +0 -0
  16. {sourcecode-1.36.2 → sourcecode-1.36.3}/raw +0 -0
  17. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/adaptive_scanner.py +0 -0
  18. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/architecture_analyzer.py +0 -0
  19. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/architecture_summary.py +0 -0
  20. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/ast_extractor.py +0 -0
  21. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/cache.py +0 -0
  22. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/cir_graphs.py +0 -0
  23. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/classifier.py +0 -0
  24. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/code_notes_analyzer.py +0 -0
  25. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/confidence_analyzer.py +0 -0
  26. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/context_scorer.py +0 -0
  27. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/context_summarizer.py +0 -0
  28. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/contract_model.py +0 -0
  29. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/contract_pipeline.py +0 -0
  30. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/coverage_parser.py +0 -0
  31. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/dependency_analyzer.py +0 -0
  32. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/detectors/__init__.py +0 -0
  33. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/detectors/base.py +0 -0
  34. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/detectors/csproj_parser.py +0 -0
  35. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/detectors/dart.py +0 -0
  36. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/detectors/dotnet.py +0 -0
  37. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/detectors/elixir.py +0 -0
  38. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/detectors/go.py +0 -0
  39. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/detectors/heuristic.py +0 -0
  40. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/detectors/hybrid.py +0 -0
  41. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/detectors/java.py +0 -0
  42. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/detectors/jvm_ext.py +0 -0
  43. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/detectors/nodejs.py +0 -0
  44. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/detectors/parsers.py +0 -0
  45. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/detectors/php.py +0 -0
  46. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/detectors/project.py +0 -0
  47. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/detectors/python.py +0 -0
  48. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/detectors/ruby.py +0 -0
  49. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/detectors/rust.py +0 -0
  50. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/detectors/systems.py +0 -0
  51. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/detectors/terraform.py +0 -0
  52. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/detectors/tooling.py +0 -0
  53. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/doc_analyzer.py +0 -0
  54. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/entrypoint_classifier.py +0 -0
  55. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/env_analyzer.py +0 -0
  56. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/error_schema.py +0 -0
  57. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/explain.py +0 -0
  58. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/file_chunker.py +0 -0
  59. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/file_classifier.py +0 -0
  60. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/flow_analyzer.py +0 -0
  61. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/fqn_utils.py +0 -0
  62. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/git_analyzer.py +0 -0
  63. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/graph_analyzer.py +0 -0
  64. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/license.py +0 -0
  65. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/mcp/__init__.py +0 -0
  66. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
  67. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/mcp/onboarding/applier.py +0 -0
  68. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/mcp/onboarding/backup.py +0 -0
  69. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/mcp/onboarding/detector.py +0 -0
  70. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/mcp/onboarding/planner.py +0 -0
  71. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/mcp/orchestrator.py +0 -0
  72. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/mcp/registry.py +0 -0
  73. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/mcp/runner.py +0 -0
  74. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/mcp/server.py +0 -0
  75. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/mcp_nudge.py +0 -0
  76. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/metrics_analyzer.py +0 -0
  77. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/migrate_check.py +0 -0
  78. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/output_budget.py +0 -0
  79. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/path_filters.py +0 -0
  80. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/pr_comment_renderer.py +0 -0
  81. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/pr_impact.py +0 -0
  82. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/prepare_context.py +0 -0
  83. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/progress.py +0 -0
  84. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/ranking_engine.py +0 -0
  85. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/redactor.py +0 -0
  86. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/relevance_scorer.py +0 -0
  87. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/rename_refactor.py +0 -0
  88. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/repo_classifier.py +0 -0
  89. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/ris.py +0 -0
  90. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/runtime_classifier.py +0 -0
  91. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/scanner.py +0 -0
  92. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/schema.py +0 -0
  93. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/semantic_analyzer.py +0 -0
  94. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/serializer.py +0 -0
  95. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/spring_event_topology.py +0 -0
  96. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/spring_findings.py +0 -0
  97. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/spring_impact.py +0 -0
  98. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/spring_model.py +0 -0
  99. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/spring_security_audit.py +0 -0
  100. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/spring_semantic.py +0 -0
  101. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/spring_tx_analyzer.py +0 -0
  102. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/summarizer.py +0 -0
  103. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/telemetry/__init__.py +0 -0
  104. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/telemetry/config.py +0 -0
  105. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/telemetry/consent.py +0 -0
  106. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/telemetry/events.py +0 -0
  107. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/telemetry/filters.py +0 -0
  108. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/telemetry/transport.py +0 -0
  109. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/tree_utils.py +0 -0
  110. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/version_check.py +0 -0
  111. {sourcecode-1.36.2 → sourcecode-1.36.3}/src/sourcecode/workspace.py +0 -0
  112. {sourcecode-1.36.2 → sourcecode-1.36.3}/supabase/.temp/cli-latest +0 -0
  113. {sourcecode-1.36.2 → sourcecode-1.36.3}/supabase/functions/README.md +0 -0
  114. {sourcecode-1.36.2 → sourcecode-1.36.3}/supabase/functions/get-license/index.ts +0 -0
  115. {sourcecode-1.36.2 → sourcecode-1.36.3}/supabase/functions/lemonsqueezy-webhook/index.ts +0 -0
  116. {sourcecode-1.36.2 → sourcecode-1.36.3}/supabase/functions/telemetry/index.ts +0 -0
  117. {sourcecode-1.36.2 → sourcecode-1.36.3}/supabase/sql/license_event_ordering.sql +0 -0
  118. {sourcecode-1.36.2 → sourcecode-1.36.3}/supabase/sql/telemetry_events.sql +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 1.36.2
3
+ Version: 1.36.3
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
@@ -365,6 +365,23 @@ sourcecode endpoints /path/to/repo --output endpoints.json
365
365
 
366
366
  Extracts all Spring MVC (`@GetMapping`, `@PostMapping`, `@RequestMapping`, etc.) and JAX-RS (`@GET`, `@POST`, `@Path`) endpoint methods. Returns HTTP method, path, controller class, and handler method.
367
367
 
368
+ **Custom security annotations.** Enterprise repos often guard endpoints with a bespoke annotation instead of `@PreAuthorize`/`@Secured`. Drop a `sourcecode.config.json` at the repo root to teach the scanner about it — otherwise those endpoints report `policy: "none_detected"`:
369
+
370
+ ```json
371
+ {
372
+ "customSecurityAnnotations": [
373
+ {
374
+ "fullyQualifiedName": "com.example.security.M3FiltroSeguridad",
375
+ "shortName": "M3FiltroSeguridad",
376
+ "resourceParam": "nombreRecurso",
377
+ "levelParam": "nivelRequerido"
378
+ }
379
+ ]
380
+ }
381
+ ```
382
+
383
+ Matching endpoints then report `policy: "custom"` with `annotation`, `resourceName`, and `requiredLevel`, and are no longer counted in `no_security_signal`. Repos without the config behave exactly as before.
384
+
368
385
  ### `spring-audit` — Spring semantic audit [free]
369
386
 
370
387
  ```bash
@@ -327,6 +327,23 @@ sourcecode endpoints /path/to/repo --output endpoints.json
327
327
 
328
328
  Extracts all Spring MVC (`@GetMapping`, `@PostMapping`, `@RequestMapping`, etc.) and JAX-RS (`@GET`, `@POST`, `@Path`) endpoint methods. Returns HTTP method, path, controller class, and handler method.
329
329
 
330
+ **Custom security annotations.** Enterprise repos often guard endpoints with a bespoke annotation instead of `@PreAuthorize`/`@Secured`. Drop a `sourcecode.config.json` at the repo root to teach the scanner about it — otherwise those endpoints report `policy: "none_detected"`:
331
+
332
+ ```json
333
+ {
334
+ "customSecurityAnnotations": [
335
+ {
336
+ "fullyQualifiedName": "com.example.security.M3FiltroSeguridad",
337
+ "shortName": "M3FiltroSeguridad",
338
+ "resourceParam": "nombreRecurso",
339
+ "levelParam": "nivelRequerido"
340
+ }
341
+ ]
342
+ }
343
+ ```
344
+
345
+ Matching endpoints then report `policy: "custom"` with `annotation`, `resourceName`, and `requiredLevel`, and are no longer counted in `no_security_signal`. Repos without the config behave exactly as before.
346
+
330
347
  ### `spring-audit` — Spring semantic audit [free]
331
348
 
332
349
  ```bash
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sourcecode"
7
- version = "1.36.2"
7
+ version = "1.36.3"
8
8
  description = "Persistent structural context and ultra-fast repeated analysis for AI coding agents"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -1,3 +1,3 @@
1
1
  """sourcecode — Deterministic codebase context maps for AI coding agents."""
2
2
 
3
- __version__ = "1.36.2"
3
+ __version__ = "1.36.3"
@@ -59,6 +59,9 @@ class CanonicalSecurity:
59
59
  effective_roles: list[str] = field(default_factory=list)
60
60
  expression: str = "" # SpEL for @PreAuthorize/@PostAuthorize
61
61
  required_permission: str = "" # for custom permission annotations
62
+ annotation: str = "" # custom security annotation short name (BUG-3)
63
+ resource_name: str = "" # resource guarded by custom annotation
64
+ required_level: str = "" # access level required by custom annotation
62
65
  raw: dict = field(default_factory=dict) # full original policy dict
63
66
 
64
67
  def to_dict(self) -> dict:
@@ -70,6 +73,12 @@ class CanonicalSecurity:
70
73
  out["expression"] = self.expression
71
74
  if self.required_permission:
72
75
  out["required_permission"] = self.required_permission
76
+ if self.annotation:
77
+ out["annotation"] = self.annotation
78
+ if self.resource_name:
79
+ out["resourceName"] = self.resource_name
80
+ if self.required_level:
81
+ out["requiredLevel"] = self.required_level
73
82
  return out
74
83
 
75
84
  def to_full_dict(self) -> dict:
@@ -89,6 +98,9 @@ class CanonicalSecurity:
89
98
  effective_roles=list(d.get("roles", [])),
90
99
  expression=d.get("expression", ""),
91
100
  required_permission=d.get("required_permission", ""),
101
+ annotation=d.get("annotation", ""),
102
+ resource_name=d.get("resourceName", ""),
103
+ required_level=d.get("requiredLevel", ""),
92
104
  raw=dict(d),
93
105
  )
94
106
 
@@ -3853,6 +3853,18 @@ def endpoints_cmd(
3853
3853
  and JAX-RS (@GET/@POST/@PUT/@DELETE/@PATCH with @Path) annotations.
3854
3854
  Extracts HTTP method, path, controller class, and handler method.
3855
3855
 
3856
+ \b
3857
+ Custom security annotations: add a sourcecode.config.json at the repo root to
3858
+ teach the scanner project-specific authorization annotations (otherwise reported
3859
+ as policy "none_detected"):
3860
+ {
3861
+ "customSecurityAnnotations": [
3862
+ {"shortName": "M3FiltroSeguridad",
3863
+ "resourceParam": "nombreRecurso", "levelParam": "nivelRequerido"}
3864
+ ]
3865
+ }
3866
+ Matching endpoints report policy "custom" with annotation/resourceName/requiredLevel.
3867
+
3856
3868
  \b
3857
3869
  Examples:
3858
3870
  sourcecode endpoints .
@@ -24,6 +24,11 @@ from typing import Any, Optional
24
24
 
25
25
  from sourcecode.fqn_utils import normalize_owner_fqn as _normalize_owner_fqn
26
26
  from sourcecode.path_filters import is_test_path as _is_test_path
27
+ from sourcecode.security_config import (
28
+ CustomSecuritySpec,
29
+ capture_markers as _capture_markers,
30
+ load_custom_security as _load_custom_security,
31
+ )
27
32
 
28
33
  # ---------------------------------------------------------------------------
29
34
  # Data classes — Phases 1–4
@@ -595,9 +600,18 @@ def _resolve_types_from_text(text: str, import_map: dict[str, str]) -> list[str]
595
600
  # Phase 1 — Symbol extraction
596
601
  # ---------------------------------------------------------------------------
597
602
 
598
- def _extract_symbols(source: str, rel_path: str) -> tuple[str, list[SymbolRecord], list[str]]:
603
+ def _extract_symbols(
604
+ source: str,
605
+ rel_path: str,
606
+ *,
607
+ extra_capture: "frozenset[str]" = frozenset(),
608
+ ) -> tuple[str, list[SymbolRecord], list[str]]:
599
609
  """Phase 1: Extract symbols from a Java source file.
600
610
 
611
+ extra_capture: extra annotation tokens (e.g. custom security annotations like
612
+ "@M3FiltroSeguridad") whose argument lists must be stored in annotation_values
613
+ even though they are not in the built-in _CAPTURE_ANN_ARGS set.
614
+
601
615
  Returns (package, symbols, raw_imports).
602
616
  """
603
617
  package = ""
@@ -675,7 +689,7 @@ def _extract_symbols(source: str, rel_path: str) -> tuple[str, list[SymbolRecord
675
689
  if ann:
676
690
  if ann not in pending_anns:
677
691
  pending_anns.append(ann)
678
- if ann_args and ann in _CAPTURE_ANN_ARGS:
692
+ if ann_args and (ann in _CAPTURE_ANN_ARGS or ann in extra_capture):
679
693
  # P1 fix: attempt to resolve constant expressions before storing.
680
694
  # Transforms '"/" + SECTION_KEY' → '"/category"' when constant
681
695
  # is defined in this file. Falls back to original if unresolvable.
@@ -2225,6 +2239,7 @@ def _assemble(
2225
2239
  changed_symbols: list[ChangedSymbol],
2226
2240
  spring_summary: dict, # noqa: ARG001 — used internally via _spring_role on symbols
2227
2241
  route_diffs: list[dict] | None = None,
2242
+ custom_security: "tuple[CustomSecuritySpec, ...]" = (),
2228
2243
  ) -> dict:
2229
2244
  """Phase 5: Final assembly — single deterministic output contract."""
2230
2245
  sorted_syms = sorted(symbols, key=lambda s: s.symbol)
@@ -2485,7 +2500,9 @@ def _assemble(
2485
2500
  e.from_symbol: e.to_symbol.split(".")[-1]
2486
2501
  for e in sorted_rels if e.type == "extends"
2487
2502
  }
2488
- _route_surface = _build_route_surface(sorted_syms, route_diffs, extends_map=_extends_map)
2503
+ _route_surface = _build_route_surface(
2504
+ sorted_syms, route_diffs, extends_map=_extends_map, custom_security=custom_security
2505
+ )
2489
2506
  _analysis_gaps = _compute_analysis_gaps(sorted_syms, spring_summary, _route_surface, sorted_rels)
2490
2507
 
2491
2508
  # Detect filter-based security model for the assembled IR.
@@ -2536,9 +2553,29 @@ def _assemble(
2536
2553
  # Route surface security extraction
2537
2554
  # ---------------------------------------------------------------------------
2538
2555
 
2556
+ def _custom_ann_param(raw: str, key: str) -> str:
2557
+ """Extract `key = value` from a raw annotation argument string.
2558
+
2559
+ Prefers a quoted string literal; falls back to a bare token (constant ref
2560
+ such as ``SeguridadRecursosConst.RRHH_MOVADMINISTRATIVOS``). Returns "" when
2561
+ the key is absent.
2562
+ """
2563
+ import re as _re
2564
+ if not key:
2565
+ return ""
2566
+ m = _re.search(rf'\b{_re.escape(key)}\s*=\s*"([^"]+)"', raw)
2567
+ if m:
2568
+ return m.group(1)
2569
+ m = _re.search(rf'\b{_re.escape(key)}\s*=\s*([A-Za-z_][\w.]*)', raw)
2570
+ if m:
2571
+ return m.group(1)
2572
+ return ""
2573
+
2574
+
2539
2575
  def _route_security_from_sym(
2540
2576
  method_sym: "Optional[SymbolRecord]",
2541
2577
  class_sym: "Optional[SymbolRecord]",
2578
+ custom_security: "tuple[CustomSecuritySpec, ...]" = (),
2542
2579
  ) -> "Optional[dict]":
2543
2580
  """Extract security policy from method and/or class-level annotations.
2544
2581
 
@@ -2557,6 +2594,10 @@ def _route_security_from_sym(
2557
2594
  @RequiresRoles → {policy: requiresroles, roles: [...]}
2558
2595
  @RequiresPermissions → {policy: requirespermissions, roles: [...]}
2559
2596
  @SecurityRequirement → {policy: openapi_security, spec: ...}
2597
+ <custom> → {policy: custom, annotation, resourceName?, requiredLevel?}
2598
+
2599
+ custom_security: project-defined security annotations from sourcecode.config.json
2600
+ (BUG-3). Checked after the built-in set so standard annotations always win.
2560
2601
 
2561
2602
  Falls back to class-level annotations if no method-level security found.
2562
2603
  Returns None if no security signal detected at either level.
@@ -2595,6 +2636,20 @@ def _route_security_from_sym(
2595
2636
  if "@SecurityRequirement" in anns:
2596
2637
  raw = vals.get("@SecurityRequirement", "")
2597
2638
  return {"policy": "openapi_security", "spec": raw.strip()}
2639
+ # Project-defined custom security annotations (BUG-3).
2640
+ for spec in custom_security:
2641
+ if spec.marker in anns:
2642
+ raw = vals.get(spec.marker, "")
2643
+ out: dict = {"policy": "custom", "annotation": spec.short_name}
2644
+ res = _custom_ann_param(raw, spec.resource_param)
2645
+ lvl = _custom_ann_param(raw, spec.level_param)
2646
+ if res:
2647
+ out["resourceName"] = res
2648
+ if lvl:
2649
+ out["requiredLevel"] = lvl
2650
+ if spec.risk_level and spec.risk_level != "custom":
2651
+ out["riskLevel"] = spec.risk_level
2652
+ return out
2598
2653
  return None
2599
2654
 
2600
2655
  # Method-level first, then class-level fallback
@@ -2614,6 +2669,7 @@ def _build_route_surface(
2614
2669
  symbols: list[SymbolRecord],
2615
2670
  route_diffs: Optional[list[dict]],
2616
2671
  extends_map: Optional[dict[str, str]] = None,
2672
+ custom_security: "tuple[CustomSecuritySpec, ...]" = (),
2617
2673
  ) -> list[dict]:
2618
2674
  """Return route surface with inheritance projection and JAX-RS sub-resource locator resolution.
2619
2675
 
@@ -2719,7 +2775,7 @@ def _build_route_surface(
2719
2775
 
2720
2776
  # P1 FIX: extract security annotations (method-level first, class fallback)
2721
2777
  _cls_sym_for_sec = class_sym_by_simple.get(cls_simple)
2722
- _sec = _route_security_from_sym(sym, _cls_sym_for_sec)
2778
+ _sec = _route_security_from_sym(sym, _cls_sym_for_sec, custom_security)
2723
2779
 
2724
2780
  # Programmatic security fallback: scan controller file when no annotation found.
2725
2781
  if _sec is None:
@@ -2857,16 +2913,24 @@ def build_repo_ir(
2857
2913
  root: Path,
2858
2914
  *,
2859
2915
  since: Optional[str] = None,
2916
+ custom_security: "Optional[list[CustomSecuritySpec]]" = None,
2860
2917
  ) -> dict:
2861
2918
  """Build IR across multiple Java files in a repo.
2862
2919
 
2863
2920
  Args:
2864
- file_paths: Relative paths to Java files to analyze.
2865
- root: Absolute repo root.
2866
- since: Git ref for symbol diff (e.g. "HEAD~1", "main").
2921
+ file_paths: Relative paths to Java files to analyze.
2922
+ root: Absolute repo root.
2923
+ since: Git ref for symbol diff (e.g. "HEAD~1", "main").
2924
+ custom_security: Custom security annotation specs (BUG-3). When None,
2925
+ loaded from <root>/sourcecode.config.json.
2867
2926
 
2868
2927
  Returns aggregated deterministic IR dict (schema_version=final-v1).
2869
2928
  """
2929
+ if custom_security is None:
2930
+ custom_security = _load_custom_security(root)
2931
+ _custom_sec_tuple = tuple(custom_security)
2932
+ _extra_capture = _capture_markers(custom_security)
2933
+
2870
2934
  all_symbols: list[SymbolRecord] = []
2871
2935
  all_relations: list[RelationEdge] = []
2872
2936
  all_changed: list[ChangedSymbol] = []
@@ -2926,7 +2990,11 @@ def build_repo_ir(
2926
2990
  continue
2927
2991
  for _m in re.finditer(r'@interface\s+(\w+)', _src):
2928
2992
  _custom_meta_markers.add(f"@{_m.group(1)}")
2929
- _effective_markers = _ANNOTATION_MARKERS + tuple(_custom_meta_markers)
2993
+ # Custom security annotations (BUG-3) are also pre-scan markers so files
2994
+ # whose only relevant annotation is a custom one aren't filtered out.
2995
+ _effective_markers = (
2996
+ _ANNOTATION_MARKERS + tuple(_custom_meta_markers) + tuple(_extra_capture)
2997
+ )
2930
2998
 
2931
2999
  _per_file: list[tuple[str, str, str, list[str], list[SymbolRecord]]] = []
2932
3000
  for rel_path in sorted(file_paths):
@@ -2955,7 +3023,9 @@ def build_repo_ir(
2955
3023
  all_symbols.extend(_min_syms)
2956
3024
  # No relations needed for non-annotated files
2957
3025
  continue
2958
- package, symbols, raw_imports = _extract_symbols(source, rel_path)
3026
+ package, symbols, raw_imports = _extract_symbols(
3027
+ source, rel_path, extra_capture=_extra_capture
3028
+ )
2959
3029
  all_symbols.extend(symbols)
2960
3030
  _per_file.append((rel_path, source, package, raw_imports, symbols))
2961
3031
 
@@ -2977,7 +3047,9 @@ def build_repo_ir(
2977
3047
  old_source = _get_git_old_content(root, rel_path, since)
2978
3048
 
2979
3049
  if old_source is not None:
2980
- _, old_symbols, _ = _extract_symbols(old_source, rel_path)
3050
+ _, old_symbols, _ = _extract_symbols(
3051
+ old_source, rel_path, extra_capture=_extra_capture
3052
+ )
2981
3053
  all_changed.extend(_diff_symbols(old_symbols, symbols))
2982
3054
  all_route_diffs.extend(_diff_routes(old_symbols, symbols))
2983
3055
  elif since and (_since_changed is None or rel_path in _since_changed):
@@ -3008,7 +3080,10 @@ def build_repo_ir(
3008
3080
  route_diffs_arg: Optional[list[dict]] = (
3009
3081
  sorted(all_route_diffs, key=lambda d: d["symbol"]) if since else None
3010
3082
  )
3011
- ir = _assemble(all_symbols, unique_relations, all_changed, spring_summary, route_diffs_arg)
3083
+ ir = _assemble(
3084
+ all_symbols, unique_relations, all_changed, spring_summary, route_diffs_arg,
3085
+ custom_security=_custom_sec_tuple,
3086
+ )
3012
3087
 
3013
3088
  # BUG-7: XML Spring Security detection for the canonical CIR pipeline.
3014
3089
  # _assemble only sees Java symbols — XML config is invisible to it.
@@ -3370,6 +3445,11 @@ def extract_java_endpoints(root: Path) -> "dict[str, Any]":
3370
3445
 
3371
3446
  _EXTENDS_FROM_SIG = _re.compile(r'\bextends\s+(\w+)')
3372
3447
 
3448
+ # Custom security annotations (BUG-3): recognized via sourcecode.config.json.
3449
+ _custom_security = _load_custom_security(root)
3450
+ _custom_sec_tuple = tuple(_custom_security)
3451
+ _extra_capture = _capture_markers(_custom_security)
3452
+
3373
3453
  # Exclude REST client proxy modules — they use JAX-RS annotations for client-side
3374
3454
  # proxy generation (RESTEasy, MicroProfile REST Client) and are NOT server resources.
3375
3455
  _CLIENT_PATH_FRAGMENTS = (
@@ -3394,7 +3474,7 @@ def extract_java_endpoints(root: Path) -> "dict[str, Any]":
3394
3474
  rel = str(jf.relative_to(root)).replace("\\", "/")
3395
3475
  except ValueError:
3396
3476
  rel = str(jf).replace("\\", "/")
3397
- _, symbols, _ = _extract_symbols(source, rel)
3477
+ _, symbols, _ = _extract_symbols(source, rel, extra_capture=_extra_capture)
3398
3478
  for sym in symbols:
3399
3479
  all_symbols.append(sym)
3400
3480
  if sym.type in ("class", "interface"):
@@ -3402,7 +3482,10 @@ def extract_java_endpoints(root: Path) -> "dict[str, Any]":
3402
3482
  if m:
3403
3483
  extends_map[sym.symbol] = m.group(1)
3404
3484
 
3405
- routes = _build_route_surface(all_symbols, route_diffs=None, extends_map=extends_map)
3485
+ routes = _build_route_surface(
3486
+ all_symbols, route_diffs=None, extends_map=extends_map,
3487
+ custom_security=_custom_sec_tuple,
3488
+ )
3406
3489
 
3407
3490
  # Security extraction: _build_route_surface already calls _route_security_from_sym
3408
3491
  # and stores the result as route["security_annotations"].
@@ -0,0 +1,99 @@
1
+ """Custom security annotation configuration (BUG-3).
2
+
3
+ Enterprise Spring projects routinely guard endpoints with bespoke authorization
4
+ annotations (e.g. ``@M3FiltroSeguridad(nombreRecurso=..., nivelRequerido=...)``)
5
+ instead of the standard ``@PreAuthorize`` / ``@Secured`` set. Without knowing
6
+ those names, the endpoint surface reports ``policy: none_detected`` for every
7
+ protected route, which makes ``endpoints`` / ``spring-audit`` blind in exactly
8
+ the repos that most need auditing.
9
+
10
+ This module loads ``sourcecode.config.json`` from a repo root and exposes the
11
+ custom annotation specs used by the canonical security extractor.
12
+
13
+ Best-effort by design: a missing file, malformed JSON, or unexpected shape all
14
+ yield an empty list, so repos without a config behave exactly as before.
15
+
16
+ Config shape::
17
+
18
+ {
19
+ "customSecurityAnnotations": [
20
+ {
21
+ "fullyQualifiedName": "com.example.security.M3FiltroSeguridad",
22
+ "shortName": "M3FiltroSeguridad",
23
+ "resourceParam": "nombreRecurso",
24
+ "levelParam": "nivelRequerido",
25
+ "riskLevel": "custom"
26
+ }
27
+ ]
28
+ }
29
+ """
30
+ from __future__ import annotations
31
+
32
+ import json
33
+ from dataclasses import dataclass
34
+ from pathlib import Path
35
+ from typing import Optional
36
+
37
+ CONFIG_FILENAME = "sourcecode.config.json"
38
+
39
+
40
+ @dataclass(frozen=True)
41
+ class CustomSecuritySpec:
42
+ """One custom security annotation the analyzer should recognize."""
43
+
44
+ short_name: str # e.g. "M3FiltroSeguridad" (no leading @)
45
+ fqn: str = "" # fully-qualified name (optional)
46
+ resource_param: str = "" # annotation attribute naming the protected resource
47
+ level_param: str = "" # annotation attribute naming the required level
48
+ risk_level: str = "custom"
49
+
50
+ @property
51
+ def marker(self) -> str:
52
+ """Annotation token as it appears in source and SymbolRecord.annotations."""
53
+ return f"@{self.short_name}"
54
+
55
+
56
+ def load_custom_security(root: Optional[Path]) -> list[CustomSecuritySpec]:
57
+ """Load custom security specs from ``<root>/sourcecode.config.json``.
58
+
59
+ Returns [] for any error or absent config — never raises.
60
+ """
61
+ if root is None:
62
+ return []
63
+ try:
64
+ cfg_path = root / CONFIG_FILENAME
65
+ if not cfg_path.is_file():
66
+ return []
67
+ data = json.loads(cfg_path.read_text(encoding="utf-8"))
68
+ except Exception:
69
+ return []
70
+
71
+ raw = data.get("customSecurityAnnotations") if isinstance(data, dict) else None
72
+ if not isinstance(raw, list):
73
+ return []
74
+
75
+ specs: list[CustomSecuritySpec] = []
76
+ for item in raw:
77
+ if not isinstance(item, dict):
78
+ continue
79
+ short = str(item.get("shortName") or "").strip().lstrip("@")
80
+ fqn = str(item.get("fullyQualifiedName") or "").strip()
81
+ if not short and fqn:
82
+ short = fqn.rsplit(".", 1)[-1]
83
+ if not short:
84
+ continue
85
+ specs.append(
86
+ CustomSecuritySpec(
87
+ short_name=short,
88
+ fqn=fqn,
89
+ resource_param=str(item.get("resourceParam") or "").strip(),
90
+ level_param=str(item.get("levelParam") or "").strip(),
91
+ risk_level=str(item.get("riskLevel") or "custom").strip() or "custom",
92
+ )
93
+ )
94
+ return specs
95
+
96
+
97
+ def capture_markers(specs: "list[CustomSecuritySpec]") -> "frozenset[str]":
98
+ """Annotation tokens whose argument lists must be captured during extraction."""
99
+ return frozenset(s.marker for s in specs)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes