sourcecode 1.36.2__py3-none-any.whl → 1.36.4__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/canonical_ir.py +12 -0
- sourcecode/cli.py +12 -0
- sourcecode/repository_ir.py +96 -13
- sourcecode/security_config.py +99 -0
- {sourcecode-1.36.2.dist-info → sourcecode-1.36.4.dist-info}/METADATA +50 -5
- {sourcecode-1.36.2.dist-info → sourcecode-1.36.4.dist-info}/RECORD +10 -9
- {sourcecode-1.36.2.dist-info → sourcecode-1.36.4.dist-info}/WHEEL +0 -0
- {sourcecode-1.36.2.dist-info → sourcecode-1.36.4.dist-info}/entry_points.txt +0 -0
- {sourcecode-1.36.2.dist-info → sourcecode-1.36.4.dist-info}/licenses/LICENSE +0 -0
sourcecode/__init__.py
CHANGED
sourcecode/canonical_ir.py
CHANGED
|
@@ -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
|
|
sourcecode/cli.py
CHANGED
|
@@ -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 .
|
sourcecode/repository_ir.py
CHANGED
|
@@ -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(
|
|
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(
|
|
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:
|
|
2865
|
-
root:
|
|
2866
|
-
since:
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sourcecode
|
|
3
|
-
Version: 1.36.
|
|
3
|
+
Version: 1.36.4
|
|
4
4
|
Summary: Persistent structural context and ultra-fast repeated analysis for AI coding agents
|
|
5
5
|
License-File: LICENSE
|
|
6
6
|
Keywords: agents,ai,codebase,context,developer-tools,llm
|
|
@@ -40,8 +40,8 @@ Description-Content-Type: text/markdown
|
|
|
40
40
|
|
|
41
41
|
**Persistent structural context and ultra-fast repeated analysis for AI coding agents.**
|
|
42
42
|
|
|
43
|
-

|
|
44
|
+

|
|
45
45
|
|
|
46
46
|
---
|
|
47
47
|
|
|
@@ -114,7 +114,7 @@ pipx install sourcecode
|
|
|
114
114
|
|
|
115
115
|
```bash
|
|
116
116
|
sourcecode version
|
|
117
|
-
# sourcecode 1.36.
|
|
117
|
+
# sourcecode 1.36.4
|
|
118
118
|
```
|
|
119
119
|
|
|
120
120
|
---
|
|
@@ -283,7 +283,7 @@ Specifically:
|
|
|
283
283
|
- Architecture pattern detection best for Spring MVC layered apps; SPI/plugin architectures (e.g. Quarkus extension model) may be misclassified
|
|
284
284
|
- Endpoint recall for JAX-RS subresource locator pattern is ~65%
|
|
285
285
|
- `impact` on implementation classes (e.g. `OrderServiceImpl`) returns 0 callers in Spring Boot — callers inject the interface via `@Autowired`. Always target the interface. When `direct_callers: []` with `confidence_level: high` for a `@Service` class, re-query the interface.
|
|
286
|
-
- `no_security_signal` on endpoints means no method-level
|
|
286
|
+
- `no_security_signal` on endpoints means no recognized method-level annotation found — does **not** mean the endpoint is unsecured. Projects using Spring Security filter chains show 100% `no_security_signal` even when fully secured. Projects using a custom authorization annotation can teach the scanner via [`sourcecode.config.json`](#sourcecodeconfigjson-repo-root).
|
|
287
287
|
- `spring-audit` and `impact-chain` are **Java/Spring only** — non-Java repos return `spring_detected: false`
|
|
288
288
|
- Event topology via `--type events` does not resolve Kafka/RabbitMQ/Redis message routes — only Spring ApplicationEvent and `@EventListener` chains
|
|
289
289
|
- Self-invocation TX bypass (calling `@Transactional` method from the same class without going through the proxy) is not detected
|
|
@@ -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
|
|
@@ -394,6 +411,8 @@ Detects structural Spring anomalies that survive code review and tests, but caus
|
|
|
394
411
|
|
|
395
412
|
Returns structured findings with `severity`, `confidence`, `symbol`, `source_file`, `evidence`, `explanation`, and `fix_hint`. JAVA/SPRING ONLY.
|
|
396
413
|
|
|
414
|
+
Endpoints guarded by a project-specific authorization annotation are treated as secured (not flagged `SEC-001`) once declared in [`sourcecode.config.json`](#sourcecodeconfigjson-repo-root).
|
|
415
|
+
|
|
397
416
|
### `impact-chain` — systemic blast radius with TX/SEC enrichment [free]
|
|
398
417
|
|
|
399
418
|
```bash
|
|
@@ -699,3 +718,29 @@ Or: `export SOURCECODE_TELEMETRY=0`
|
|
|
699
718
|
```bash
|
|
700
719
|
sourcecode config # show version, config file path, telemetry status
|
|
701
720
|
```
|
|
721
|
+
|
|
722
|
+
### `sourcecode.config.json` (repo root)
|
|
723
|
+
|
|
724
|
+
Optional, per-repo. Loaded from the root of the repo being analyzed. Absent or
|
|
725
|
+
malformed config is ignored — the tool behaves exactly as without it.
|
|
726
|
+
|
|
727
|
+
**Custom security annotations.** Teach `endpoints`, `spring-audit`, and `explain`
|
|
728
|
+
about project-specific authorization annotations (otherwise reported as
|
|
729
|
+
`policy: "none_detected"`):
|
|
730
|
+
|
|
731
|
+
```json
|
|
732
|
+
{
|
|
733
|
+
"customSecurityAnnotations": [
|
|
734
|
+
{
|
|
735
|
+
"fullyQualifiedName": "com.example.security.M3FiltroSeguridad",
|
|
736
|
+
"shortName": "M3FiltroSeguridad",
|
|
737
|
+
"resourceParam": "nombreRecurso",
|
|
738
|
+
"levelParam": "nivelRequerido"
|
|
739
|
+
}
|
|
740
|
+
]
|
|
741
|
+
}
|
|
742
|
+
```
|
|
743
|
+
|
|
744
|
+
`resourceParam` / `levelParam` are optional and name the annotation attributes to
|
|
745
|
+
surface as `resourceName` / `requiredLevel`. Matching endpoints report
|
|
746
|
+
`policy: "custom"` and drop out of the `no_security_signal` count.
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
sourcecode/__init__.py,sha256=
|
|
1
|
+
sourcecode/__init__.py,sha256=VaLp6STbtbdgz6lEh8T1SQ9wlrYaZOhGX5EsxDkin2c,103
|
|
2
2
|
sourcecode/adaptive_scanner.py,sha256=XffluXKzJUXrMtjEiAOnSNPZnztdIcts17T9ouHeID0,10521
|
|
3
3
|
sourcecode/architecture_analyzer.py,sha256=liCwQmLgb5vplohy8arjYxs_HOIv5C9MjLh_gY6bc5Q,44115
|
|
4
4
|
sourcecode/architecture_summary.py,sha256=z34_6v7cSwy98cof2UVciGho7SCrZ93tiqMmq5WNzRQ,20405
|
|
5
5
|
sourcecode/ast_extractor.py,sha256=sa6CmLpn-k5G3_Hzxn8hAlZ5-TS-EVzXDD0Gvxd2jzs,50613
|
|
6
6
|
sourcecode/cache.py,sha256=1V3vsaODAa2UBJAC0xpvxpmRdriCezQx5Q8JCcfgziE,31892
|
|
7
|
-
sourcecode/canonical_ir.py,sha256=
|
|
7
|
+
sourcecode/canonical_ir.py,sha256=DEwucOPJguLsVtg5cV8mWXNi112l5jmBhv73KGGebVk,24849
|
|
8
8
|
sourcecode/cir_graphs.py,sha256=rZi8JV4ZrAa2WSCeyNa4JIEKQ_yZzDZTsrvVz2KfuKA,8919
|
|
9
9
|
sourcecode/classifier.py,sha256=hKzg-nQ47htqqIUzSGvYxv15cXrA3KgICTwJmdqal0o,8095
|
|
10
|
-
sourcecode/cli.py,sha256
|
|
10
|
+
sourcecode/cli.py,sha256=-E7iKh47hQyZGbhy1lM1bxCPUa21XW6zLHurByc7KGc,253149
|
|
11
11
|
sourcecode/code_notes_analyzer.py,sha256=EJemNCNc9Dn-1RZYu-aNbK0ELzmsyC4s6FdHi3XyNEI,9392
|
|
12
12
|
sourcecode/confidence_analyzer.py,sha256=_jckZSxksV-OU38vbkxfVNBnWCtlCq8Vwfg23x1uspA,19054
|
|
13
13
|
sourcecode/context_scorer.py,sha256=QpChSpsmaAYz91rXA4Ue5xzQmNz_ZboZN09YOHScq1U,14679
|
|
@@ -42,11 +42,12 @@ sourcecode/redactor.py,sha256=SB4hwIvg8h-hvcqKcDWaZvA-aSyn-at-BIRwa0tUv5E,3227
|
|
|
42
42
|
sourcecode/relevance_scorer.py,sha256=0AgEt4KrV73nioMqBgjhGjtY7L2C7L7cSyKtj3IKcrw,9408
|
|
43
43
|
sourcecode/rename_refactor.py,sha256=h6dNFlB9aZ_3q6heeHBkgXQeXaT03nvPSsYH6P8qxFg,12965
|
|
44
44
|
sourcecode/repo_classifier.py,sha256=FG1vaWKdWXsWdl-S8hjVMiTqcwgaRXkDyvK4rPcOGtQ,22681
|
|
45
|
-
sourcecode/repository_ir.py,sha256=
|
|
45
|
+
sourcecode/repository_ir.py,sha256=XtkzRTl3Ze3G6D2sXtXxlyxjkjSL2BuiKb8GQehkbdE,188320
|
|
46
46
|
sourcecode/ris.py,sha256=RcqLVwC-doFcKKViYDkCjZLBqf_wzLES7-F6vHEeWzE,20419
|
|
47
47
|
sourcecode/runtime_classifier.py,sha256=uTAD6BDCiBLUZEDRfqk718kM4RTT_vAbfkcOI2_Xx58,18432
|
|
48
48
|
sourcecode/scanner.py,sha256=WdOQ78mMzjR1NjmKTlbxdgwinnCTfAhxCVLBEFQiFHU,8899
|
|
49
49
|
sourcecode/schema.py,sha256=aHNXDf8LGyUC8ZDE_VS9kiskC2-Oswhi_WnpdGy6HDw,24897
|
|
50
|
+
sourcecode/security_config.py,sha256=_m8oQAy2gP7Ho2I-IIMTFFjFVWbJIGlLEOiAWK2TDjs,3503
|
|
50
51
|
sourcecode/semantic_analyzer.py,sha256=4OdG6tTSnTvq3_dSWMbQu8Ad1ndSCKeG-b9qM4hIxkw,89176
|
|
51
52
|
sourcecode/serializer.py,sha256=TGzftrSKitZrtl6Hh-R05s4KdTOxwTmph_lGDbo2Wzg,125015
|
|
52
53
|
sourcecode/spring_event_topology.py,sha256=5_ON_21Le5zbG-1GRc5GLIi5HJfy_QjcXLVPC5WeUGQ,18055
|
|
@@ -97,8 +98,8 @@ sourcecode/telemetry/consent.py,sha256=wLMvGNJeSSyZoNkQXpoUioY6mMv4Qdvuw7S9jAEWn
|
|
|
97
98
|
sourcecode/telemetry/events.py,sha256=LtzYfaX9Ilckj5PTvAcTpDa9mLqDsYPDUiDkRa58piY,2580
|
|
98
99
|
sourcecode/telemetry/filters.py,sha256=NHa5T-6DaZduQPFuC34jOqHWQgSizM-Ygq8aZ4j19ng,5834
|
|
99
100
|
sourcecode/telemetry/transport.py,sha256=4gGHsq0WeY9VywEZXA3vUxykfiYnw9uuqfjAAec7F8o,1681
|
|
100
|
-
sourcecode-1.36.
|
|
101
|
-
sourcecode-1.36.
|
|
102
|
-
sourcecode-1.36.
|
|
103
|
-
sourcecode-1.36.
|
|
104
|
-
sourcecode-1.36.
|
|
101
|
+
sourcecode-1.36.4.dist-info/METADATA,sha256=cvBsZ2SCRYW3ObJswm0cwyqc54S36u_Y3V6VoVljSQ4,32243
|
|
102
|
+
sourcecode-1.36.4.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
103
|
+
sourcecode-1.36.4.dist-info/entry_points.txt,sha256=ex3F9rmbXeyDIoFQHtkEqTsKSaJow8F0LrVu8XfIktQ,57
|
|
104
|
+
sourcecode-1.36.4.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
|
|
105
|
+
sourcecode-1.36.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|