sourcecode 4.0.1__py3-none-any.whl → 4.1.0__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.

Potentially problematic release.


This version of sourcecode might be problematic. Click here for more details.

sourcecode/__init__.py CHANGED
@@ -4,4 +4,4 @@ ASK Engine is the product. ``ask`` is the canonical CLI command; ``sourcecode``
4
4
  the legacy compatibility alias and the Python/PyPI package name. See
5
5
  docs/PRODUCT_IDENTITY.md (normative)."""
6
6
 
7
- __version__ = "4.0.1"
7
+ __version__ = "4.1.0"
sourcecode/cli.py CHANGED
@@ -8934,8 +8934,16 @@ def version_cmd() -> None:
8934
8934
  {"cli_version": "1.33.11", "mcp_schema_version": "1.33.11",
8935
8935
  "compatibility_schema_version": "1.0"}
8936
8936
  """
8937
+ from sourcecode.release_info import advisory_line, build_status
8938
+
8937
8939
  if getattr(sys.stdout, "isatty", lambda: False)():
8938
8940
  typer.echo(f"ask {__version__}")
8941
+ # C4-12: the field audits builds that are days old and many releases
8942
+ # behind, and nothing said so. Printed only when it applies — an advisory
8943
+ # on every run is one nobody reads.
8944
+ _advisory = advisory_line()
8945
+ if _advisory:
8946
+ typer.echo(_advisory)
8939
8947
  else:
8940
8948
  import json as _json_ver
8941
8949
  from sourcecode.envelope import ENVELOPE_VERSION, available_schemas
@@ -8943,6 +8951,10 @@ def version_cmd() -> None:
8943
8951
  "cli_version": __version__,
8944
8952
  "mcp_schema_version": __version__,
8945
8953
  "compatibility_schema_version": "1.0",
8954
+ # Which release this is, when it shipped, and how far behind it
8955
+ # probably is. The estimate carries its basis and never claims a
8956
+ # newer release exists (C4-12).
8957
+ "build": build_status(),
8946
8958
  # What a consumer should actually pin against: the release string
8947
8959
  # changes every release, the envelope version only when the output
8948
8960
  # contract does.
sourcecode/envelope.py CHANGED
@@ -123,6 +123,16 @@ def build_meta(
123
123
  "tool_version": __version__,
124
124
  "command": command,
125
125
  }
126
+ # C4-12: a consumer holding a payload could not tell how old the build that
127
+ # produced it was. The DATE is a constant of the build, so it goes here; the
128
+ # AGE changes daily and would break the byte-identical property above, so it
129
+ # is derived where it is asked for (`ask version`), never in the envelope.
130
+ try:
131
+ from sourcecode.release_info import RELEASE_DATE
132
+
133
+ meta["tool_release_date"] = RELEASE_DATE
134
+ except Exception:
135
+ pass
126
136
  if schema_version:
127
137
  meta["schema_version"] = schema_version
128
138
 
@@ -74,13 +74,16 @@ NON_COVERAGE: tuple[NonCoverage, ...] = (
74
74
  id="NC-002",
75
75
  surface="security_surface",
76
76
  not_covered=(
77
- "Secrets outside Java and Spring configuration — a credential in a "
78
- "deployment descriptor, a Helm value, a CI variable file."
77
+ "Secrets outside Java, Spring configuration and the deployment "
78
+ "descriptors — a Helm value, a CI variable file, a shell script."
79
79
  ),
80
80
  why=(
81
- "The file population this analyzer reads is the Java source and the "
82
- "Spring configuration convention. A secret elsewhere is not missed by "
83
- "a weak rule; it is outside the set of files anything here opens."
81
+ "The file population this analyzer reads is the Java source, the Spring "
82
+ "configuration convention and the descriptors it already parses to "
83
+ "resolve an environment. A secret elsewhere is not missed by a weak "
84
+ "rule; it is outside the set of files anything here opens. Narrowed by "
85
+ "CL-10: a credential stored in `web.xml` or a Spring XML context IS "
86
+ "reported (SEC-007), because those files were already being read."
84
87
  ),
85
88
  instead="A dedicated secret scanner over the whole tree (gitleaks, trufflehog).",
86
89
  ),
sourcecode/posture.py CHANGED
@@ -381,6 +381,87 @@ def resolve_beans(
381
381
  )
382
382
 
383
383
 
384
+ #: CL-11 — container settings that make the resolution below a DEFAULT rather
385
+ #: than a description of what runs. Each says what the container will no longer
386
+ #: report, in the terms of the verdict it qualifies. Published Spring property
387
+ #: names, so this is framework vocabulary, never a client's own configuration.
388
+ _OBSERVABILITY_AMPLIFIERS: "tuple[tuple[str, str], ...]" = (
389
+ (
390
+ "spring.main.allow-bean-definition-overriding",
391
+ "a second bean definition for a name resolved here REPLACES it instead of "
392
+ "failing at startup, so a security configuration can be overridden by one "
393
+ "this resolution never saw and nothing in the deployment reports the "
394
+ "disagreement",
395
+ ),
396
+ (
397
+ "spring.main.allow-circular-references",
398
+ "a wiring cycle through the beans resolved here is repaired at runtime by "
399
+ "injecting a proxy rather than refused, so the instance that ends up in the "
400
+ "chain need not be the one this resolution describes",
401
+ ),
402
+ )
403
+
404
+ #: Values Spring reads as "on". Compared case-insensitively, as Spring does.
405
+ _TRUTHY = frozenset({"true", "yes", "on", "1"})
406
+
407
+
408
+ def container_observability(properties: "EffectiveProperties") -> dict:
409
+ """Whether the container would report a disagreement with this resolution (CL-11).
410
+
411
+ Audit #8 found `allow-bean-definition-overriding: true` and
412
+ `allow-circular-references: true` in the repository whose entire security
413
+ verdict turns on which `SecurityConfig` bean wins. On its own each is a
414
+ preference; beside a conditional security verdict each is the reason that
415
+ verdict is a **default and not a guarantee** — with overriding enabled, a
416
+ silent override of the filter chain does not fail at startup, so the one
417
+ check that would have caught it never runs.
418
+
419
+ This block only ever LOWERS confidence. It is emitted beside the verdict it
420
+ qualifies rather than as a finding underneath it, because the C1-20 lesson
421
+ runs the other way here: a caveat a reader has to go and find is a caveat
422
+ that does not reach the conclusion it was written for.
423
+ """
424
+ amplifiers: "list[dict]" = []
425
+ for key, effect in _OBSERVABILITY_AMPLIFIERS:
426
+ blocked = properties.blocked(key)
427
+ if blocked:
428
+ # The property exists and nothing decides it. That is not "off": an
429
+ # undecidable safety setting is exactly the case where claiming the
430
+ # safe default would be the confident falsehood.
431
+ amplifiers.append({
432
+ "property": key, "value": None, "state": "undecidable",
433
+ "reason": blocked, "source": None, "profile": None, "effect": effect,
434
+ })
435
+ continue
436
+ found = properties.lookup(key)
437
+ if found is None or found.value.strip().lower() not in _TRUTHY:
438
+ continue
439
+ amplifiers.append({
440
+ "property": key, "value": found.value, "state": "enabled",
441
+ "reason": None, "source": found.source, "profile": found.profile,
442
+ "effect": effect,
443
+ })
444
+
445
+ block: dict = {
446
+ "weakens_this_resolution": bool(amplifiers),
447
+ "amplifiers": amplifiers,
448
+ }
449
+ if amplifiers:
450
+ block["reading"] = (
451
+ "The bean resolution in this payload describes the configuration this "
452
+ "repository declares. " + str(len(amplifiers)) + " container setting(s) "
453
+ "mean the deployment would not report a disagreement with it, so read "
454
+ "every verdict below as a default rather than as what runs."
455
+ )
456
+ else:
457
+ block["reading"] = (
458
+ "No container setting was found that suppresses a startup failure over "
459
+ "the beans resolved here; a disagreement with this resolution would "
460
+ "surface as a failure to start."
461
+ )
462
+ return block
463
+
464
+
384
465
  #: Published Spring Security vocabulary that makes a bean part of the request
385
466
  #: chain's authorization configuration. Recorded as evidence; never branched on
386
467
  #: a client's own class name.
@@ -1017,11 +1098,20 @@ def _posture(
1017
1098
  "supplied": sorted(properties or {}),
1018
1099
  },
1019
1100
  **resolution.to_dict(),
1101
+ # CL-11. Whether the container would even report a disagreement with the
1102
+ # resolution above. Beside it, not underneath it: this block only lowers
1103
+ # confidence in the verdicts that follow, and a caveat a reader has to go
1104
+ # looking for does not reach the conclusion it was written for.
1105
+ "container": container_observability(resolved_properties),
1020
1106
  # Called out separately because a profile that switches the security
1021
1107
  # configuration is not one bean among many — it is the difference
1022
1108
  # between an authenticated surface and an open one.
1023
1109
  "security": security_projection(cir, resolution),
1024
1110
  }
1111
+ # The verdict a reader acts on is the security slice, so the qualifier
1112
+ # travels with it rather than only at the top of the document.
1113
+ if payload["container"]["weakens_this_resolution"]:
1114
+ payload["security"]["resolution_is_a_default"] = payload["container"]["reading"]
1025
1115
  payload["endpoints"] = endpoint_projection(cir, payload["security"])
1026
1116
  # Which chain is wired is a fact about a bean; what that chain permits is the
1027
1117
  # fact about the request, and it is the one a reviewer asked for.
@@ -0,0 +1,123 @@
1
+ """What this build is, and how far behind it is likely to be (C4-12).
2
+
3
+ Field audit #8 ran a build self-reporting 3.5.0 while 4.0.1 was shipping and
4
+ re-derived, by hand, twelve product complaints from rows already closed — one of
5
+ them the answer to its own critical finding. Nothing in the output told the
6
+ operator their build was eight releases behind, so remediation speed, the
7
+ property `docs/DEFECT-LEDGER.md` exists to make visible, was invisible to the
8
+ person it was for.
9
+
10
+ Age in days is the wrong unit on its own, and measuring it is what shows why:
11
+ that build was **three days old**. On this project's cadence three days is six
12
+ releases. So the fact published here is the release date (a constant, in the
13
+ build), and the derived figure is an **estimate** of how many releases have
14
+ shipped since — labelled as an estimate, because a CLI cannot see an index it
15
+ has no network to read. It never claims a newer release exists.
16
+
17
+ `_meta.tool_release_date` carries the constant half; the envelope is
18
+ byte-identical across runs by contract, so the age — which changes every day —
19
+ is computed only where it is asked for, in `ask version`.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import datetime
25
+ from typing import Any, Optional
26
+
27
+ #: The date `__version__` was released, from that version's CHANGELOG entry.
28
+ #: `tests/test_release_info.py` fails the build if the two disagree, so this is
29
+ #: a copy of a fact rather than a second authority for it.
30
+ RELEASE_DATE = "2026-08-03"
31
+
32
+ #: Releases per day, measured over the 30 releases before this one (the same
33
+ #: computation the test re-runs against the CHANGELOG). Stable across window
34
+ #: sizes when it was taken — 2.25 over 10, 2.11 over 20, 2.07 over 30 — which is
35
+ #: why a single figure is publishable at all.
36
+ RELEASE_RATE_PER_DAY = 2.07
37
+
38
+ #: How many releases behind is worth telling the operator about. Four, which on
39
+ #: the measured cadence is about two days: one release is half a day and warning
40
+ #: about it would put a line on almost every run, which is how an advisory stops
41
+ #: being read. The field case — three days, an estimated six releases, eight in
42
+ #: fact — is well past it. An assumption, not a measurement.
43
+ STALE_AFTER_RELEASES = 4
44
+
45
+ #: What the estimate is, in the words the payload uses. The rate is a
46
+ #: measurement of the past, not a promise about the future, and the distinction
47
+ #: is the whole reason this string is published beside the number.
48
+ ESTIMATE_BASIS = (
49
+ f"{RELEASE_RATE_PER_DAY} releases/day, measured over the 30 releases before "
50
+ f"this one. An estimate of what has shipped since, not an observation: this "
51
+ f"build has no index to read and never claims a newer release exists."
52
+ )
53
+
54
+ #: Where this version's known-wrong answers are written down.
55
+ DEFECT_LEDGER = "docs/DEFECT-LEDGER.md"
56
+
57
+ #: How to move off this build. Named here so the advisory cannot drift from the
58
+ #: distribution name in `pyproject.toml`.
59
+ UPGRADE_COMMAND = "pip install -U sourcecode"
60
+
61
+
62
+ def _today() -> datetime.date:
63
+ return datetime.date.today()
64
+
65
+
66
+ def release_age_days(today: "Optional[datetime.date]" = None) -> int:
67
+ """Days between this build's release and *today*, never negative.
68
+
69
+ A build produced from a working tree ahead of the last CHANGELOG entry, or a
70
+ machine whose clock is behind, reads as age 0 rather than as a negative
71
+ number that would flow into the estimate as a negative release count.
72
+ """
73
+ try:
74
+ released = datetime.date.fromisoformat(RELEASE_DATE)
75
+ except ValueError:
76
+ return 0
77
+ return max(0, ((today or _today()) - released).days)
78
+
79
+
80
+ def estimated_releases_since(today: "Optional[datetime.date]" = None) -> int:
81
+ """Releases likely shipped since this build. Rounded down: a floor, not a guess up."""
82
+ return int(release_age_days(today) * RELEASE_RATE_PER_DAY)
83
+
84
+
85
+ def build_status(today: "Optional[datetime.date]" = None) -> "dict[str, Any]":
86
+ """What this build is and how far behind it probably is.
87
+
88
+ `advisory` is `None` on a fresh build — a line that appears on every run is a
89
+ line nobody reads, and this one exists to be read once.
90
+ """
91
+ from sourcecode import __version__
92
+
93
+ age = release_age_days(today)
94
+ behind = estimated_releases_since(today)
95
+ stale = behind >= STALE_AFTER_RELEASES
96
+
97
+ status: dict[str, Any] = {
98
+ "version": __version__,
99
+ "released": RELEASE_DATE,
100
+ "age_days": age,
101
+ "estimated_releases_since": behind,
102
+ "estimate_basis": ESTIMATE_BASIS,
103
+ "stale": stale,
104
+ "known_defects": (
105
+ f"{DEFECT_LEDGER} records what this version is known to get wrong. "
106
+ f"Rows closed after it shipped are not in this copy."
107
+ ),
108
+ "advisory": None,
109
+ }
110
+ if stale:
111
+ status["advisory"] = (
112
+ f"This build is {age} day{'s' if age != 1 else ''} old and this project "
113
+ f"released {RELEASE_RATE_PER_DAY} times a day over its last 30 releases "
114
+ f"(~{behind} since). Upgrade before reading an absent finding as a "
115
+ f"clean result: `{UPGRADE_COMMAND}`. What this version is known to get "
116
+ f"wrong is in {DEFECT_LEDGER}."
117
+ )
118
+ return status
119
+
120
+
121
+ def advisory_line(today: "Optional[datetime.date]" = None) -> "Optional[str]":
122
+ """The one-line human form of `build_status()['advisory']`, or None when fresh."""
123
+ return build_status(today)["advisory"]
@@ -54,6 +54,7 @@ RULES: tuple[Rule, ...] = (
54
54
  Rule("SEC-004", "SEC", "Password stored under a fast unsalted digest (SHA-1, MD5)"),
55
55
  Rule("SEC-005", "SEC", "CSRF disabled under session-bearing authentication"),
56
56
  Rule("SEC-006", "SEC", "Cookie created without the Secure attribute"),
57
+ Rule("SEC-007", "SEC", "Credential stored in a deployment descriptor"),
57
58
  )
58
59
 
59
60
  FAMILY_ORDER: tuple[str, ...] = ("TX", "SEC")
@@ -19,6 +19,11 @@
19
19
  "description": "Release that produced the payload.",
20
20
  "type": "string"
21
21
  },
22
+ "tool_release_date": {
23
+ "description": "Date the release that produced this payload shipped (ISO 8601). A constant of the build, so the envelope stays byte-identical across runs; the AGE is derived by the consumer, or printed by `ask version`, which also estimates how many releases have shipped since.",
24
+ "type": "string",
25
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
26
+ },
22
27
  "command": {
23
28
  "description": "Full path through the command tree, e.g. \"retrieve endpoint-inventory\".",
24
29
  "type": "string",
@@ -27,7 +32,9 @@
27
32
  "scope": {
28
33
  "description": "What was analysed. Absent when the command addresses no path.",
29
34
  "type": "object",
30
- "required": ["path"],
35
+ "required": [
36
+ "path"
37
+ ],
31
38
  "properties": {
32
39
  "path": {
33
40
  "description": "Absolute path the command was pointed at.",
@@ -83,6 +83,44 @@ _INSECURE_COOKIE_PROPERTY = re.compile(
83
83
  re.IGNORECASE,
84
84
  )
85
85
 
86
+ # ── SEC-007: a stored credential in a deployment descriptor ───────────────────
87
+ # CL-10. NC-002 declares "no secrets outside Java/YAML", which is honest and
88
+ # wider than it needs to be: audit #8's credential sits in the `web.xml` files
89
+ # `environment_resolution`, `filter_surface` and `path_filters` already read —
90
+ # `monitor:{SHA-256}dc20b820…`, unsalted, the SAME hash in all three
91
+ # per-environment overlays. The gap was admission surface, not detection.
92
+ #
93
+ # The rule fires on the SHAPE of the value, not on the name of the parameter
94
+ # carrying it (VAI): a digest tag followed by a hex literal is a stored
95
+ # credential wherever it appears, and needs no vendor vocabulary to recognise.
96
+
97
+ #: `{SHA-256}dc20…` — the platform-neutral form for a digested credential written
98
+ #: into a configuration value. The tag is spec vocabulary (RFC 2307 / LDAP, and
99
+ #: every container that copied it); the hex run is what makes it a stored value
100
+ #: rather than a reference to one.
101
+ _DIGEST_LITERAL = re.compile(
102
+ r"\{(?P<algo>SHA-?(?:1|256|384|512)|SSHA|MD5|CRYPT)\}(?P<digest>[A-Za-z0-9+/=]{16,})",
103
+ re.IGNORECASE,
104
+ )
105
+
106
+ #: A descriptor value that is a reference, not a credential: resolved at deploy
107
+ #: time from somewhere this repository does not contain.
108
+ _PLACEHOLDER = re.compile(r"^\s*[$@%]?[{(]?[^{}()]*[}\)]?\s*$")
109
+ _IS_REFERENCE = re.compile(r"[$@]\{[^}]+\}|^\s*$|^\s*ENC\(|^\s*\*+\s*$")
110
+
111
+ #: XML elements whose text is a configured VALUE. Servlet/JEE spec vocabulary.
112
+ _DESCRIPTOR_VALUE = re.compile(
113
+ r"<(?P<tag>param-value|env-entry-value|value|property|password)\b[^>]*>"
114
+ r"(?P<value>[^<]*)</(?P=tag)>",
115
+ re.IGNORECASE,
116
+ )
117
+
118
+ #: Files a deployment descriptor scan admits. `web.xml` and the Spring XML
119
+ #: contexts are the two `environment_resolution` already classifies, so this adds
120
+ #: no new reader — it narrows what NC-002 excludes to what we genuinely cannot see.
121
+ _DESCRIPTOR_NAMES = ("web.xml",)
122
+ _DESCRIPTOR_SUFFIXES = ("-context.xml",)
123
+
86
124
 
87
125
  @dataclass(frozen=True)
88
126
  class SecurityConfigObservation:
@@ -219,6 +257,78 @@ def _scan_config(text: str, rel: str) -> "list[SecurityConfigObservation]":
219
257
  return out
220
258
 
221
259
 
260
+ def _scan_descriptor(text: str, rel: str) -> "list[SecurityConfigObservation]":
261
+ """SEC-007 — a credential written into a deployment descriptor (CL-10).
262
+
263
+ Matched on the value's shape. A descriptor that carries `${admin.password}`
264
+ holds a reference and is not reported; one that carries a digest literal
265
+ holds the credential itself, in a file under version control.
266
+ """
267
+ out: "list[SecurityConfigObservation]" = []
268
+ for match in _DESCRIPTOR_VALUE.finditer(text):
269
+ value = match.group("value").strip()
270
+ if not value or _IS_REFERENCE.search(value):
271
+ continue
272
+ digest = _DIGEST_LITERAL.search(value)
273
+ if digest is None:
274
+ continue
275
+ out.append(SecurityConfigObservation(
276
+ rule="SEC-007",
277
+ file=rel,
278
+ line=_line_of(text, match.start()),
279
+ symbol=rel,
280
+ # The digest is truncated on purpose: enough to correlate two files,
281
+ # never enough to make this payload a place to read a credential from.
282
+ evidence=f"<{match.group('tag')}>…{{{digest.group('algo')}}}"
283
+ f"{digest.group('digest')[:8]}…",
284
+ detail=(
285
+ f"A credential is stored in this deployment descriptor as a "
286
+ f"{digest.group('algo').upper()} digest. It is under version control, so "
287
+ f"every reader of the repository holds it, and the digest is a fast "
288
+ f"unsalted hash rather than a password hash — recovering the input is "
289
+ f"a dictionary run, not an attack."
290
+ ),
291
+ ))
292
+ return out
293
+
294
+
295
+ def _shared_across_environments(
296
+ observations: "list[SecurityConfigObservation]",
297
+ ) -> "list[SecurityConfigObservation]":
298
+ """Say when the same stored credential appears in more than one descriptor.
299
+
300
+ Audit #8's finding was not one credential in one file: it was the SAME hash
301
+ in the DPA, M3 and M3DEV overlays, which is a fact about the deployment —
302
+ three environments share one credential, so compromising the least protected
303
+ one is compromising all three. Composed here because both halves are already
304
+ in this list and neither file knows about the other.
305
+ """
306
+ by_digest: "dict[str, list[SecurityConfigObservation]]" = {}
307
+ for obs in observations:
308
+ if obs.rule != "SEC-007":
309
+ continue
310
+ by_digest.setdefault(obs.evidence, []).append(obs)
311
+
312
+ out: "list[SecurityConfigObservation]" = []
313
+ for obs in observations:
314
+ siblings = by_digest.get(obs.evidence, []) if obs.rule == "SEC-007" else []
315
+ if len(siblings) > 1:
316
+ others = sorted({s.file for s in siblings if s.file != obs.file})
317
+ if others:
318
+ obs = SecurityConfigObservation(
319
+ rule=obs.rule, file=obs.file, line=obs.line, symbol=obs.symbol,
320
+ evidence=obs.evidence,
321
+ detail=(
322
+ obs.detail + " The same digest is declared in "
323
+ + ", ".join(others)
324
+ + ": these deployments share one credential, so the least "
325
+ "protected of them decides the strength of all of them."
326
+ ),
327
+ )
328
+ out.append(obs)
329
+ return out
330
+
331
+
222
332
  #: Configuration extensions worth reading for the property form of these decisions.
223
333
  _CONFIG_SUFFIXES = (".properties", ".yml", ".yaml")
224
334
 
@@ -259,4 +369,25 @@ def scan_security_configuration(
259
369
  continue
260
370
  out.extend(_scan_config(text, rel))
261
371
 
262
- return sorted(out, key=lambda o: (o.rule, o.file, o.line))
372
+ # CL-10 — the descriptors. Discovered the same way `environment_resolution`
373
+ # classifies them, so the two surfaces cannot disagree about which files are
374
+ # deployment descriptors, and a per-environment overlay tree is covered
375
+ # because the walk finds the file wherever the build put it.
376
+ descriptors: "list[str]" = []
377
+ try:
378
+ for path in root.rglob("*.xml"):
379
+ name = path.name.lower()
380
+ if name in _DESCRIPTOR_NAMES or name.endswith(_DESCRIPTOR_SUFFIXES):
381
+ descriptors.append(path.relative_to(root).as_posix())
382
+ except OSError:
383
+ pass
384
+ for rel in sorted(set(descriptors)):
385
+ try:
386
+ text = (root / rel).read_text(encoding="utf-8", errors="replace")
387
+ except OSError:
388
+ continue
389
+ out.extend(_scan_descriptor(text, rel))
390
+
391
+ return sorted(
392
+ _shared_across_environments(out), key=lambda o: (o.rule, o.file, o.line)
393
+ )
@@ -7,6 +7,7 @@ Patterns implemented (Phase 3):
7
7
  SEC-004 Password stored under a fast unsalted digest (CL-9)
8
8
  SEC-005 CSRF disabled under session-bearing authentication (CL-9)
9
9
  SEC-006 Cookie created without the Secure attribute (CL-9)
10
+ SEC-007 Credential stored in a deployment descriptor (CL-10)
10
11
 
11
12
  All patterns are deterministic and never raise.
12
13
  """
@@ -386,7 +387,8 @@ def _controller_source_file(cir: "CanonicalRepositoryIR", controller_fqn: str) -
386
387
 
387
388
  class _SEC00XConfigurationWeaknesses:
388
389
  """The textbook rules (CL-9): SEC-004 password storage, SEC-005 CSRF under
389
- session authentication, SEC-006 a cookie without the Secure attribute.
390
+ session authentication, SEC-006 a cookie without the Secure attribute — and
391
+ SEC-007 (CL-10), a credential stored in a deployment descriptor.
390
392
 
391
393
  One pattern for three rules because they share a scan of the same files, and
392
394
  because the alternative — three passes over the source per audit — is how a
@@ -400,11 +402,16 @@ class _SEC00XConfigurationWeaknesses:
400
402
 
401
403
  pattern_id = "SEC-004" # the group's identity for the Protocol; each finding carries its own
402
404
 
403
- _SEVERITY = {"SEC-004": "high", "SEC-005": "medium", "SEC-006": "medium"}
405
+ _SEVERITY = {
406
+ "SEC-004": "high", "SEC-005": "medium", "SEC-006": "medium",
407
+ # A credential every reader of the repository already holds.
408
+ "SEC-007": "high",
409
+ }
404
410
  _TITLE = {
405
411
  "SEC-004": "Password stored under a fast unsalted digest",
406
412
  "SEC-005": "CSRF protection disabled under session-bearing authentication",
407
413
  "SEC-006": "Cookie created without the Secure attribute",
414
+ "SEC-007": "Credential stored in a deployment descriptor",
408
415
  }
409
416
  #: A pattern that emits more than one rule id declares them all, so a catalogue
410
417
  #: reader is never left with the group's identity standing for the group.
@@ -425,6 +432,13 @@ class _SEC00XConfigurationWeaknesses:
425
432
  "Set the Secure attribute (and HttpOnly, and SameSite) on session cookies, "
426
433
  "and configure it per environment rather than in code."
427
434
  ),
435
+ "SEC-007": (
436
+ "Move the credential out of the descriptor: read it from an environment "
437
+ "variable or a secret store and leave a ${placeholder} in the file. Rotate "
438
+ "it — it is in the history of every clone, so removing the line does not "
439
+ "retire it — and give each environment its own, so the least protected "
440
+ "deployment stops deciding the strength of the others."
441
+ ),
428
442
  }
429
443
 
430
444
  def analyze(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 4.0.1
3
+ Version: 4.1.0
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
@@ -97,13 +97,25 @@ brew tap haroundominique/sourcecode && brew install sourcecode
97
97
  # pip / pipx
98
98
  pipx install sourcecode # or: pip install sourcecode
99
99
 
100
- ask version # ask 2.5.1
100
+ ask version # ask 4.1.0 — and, on a build that has aged,
101
+ # how many releases have probably shipped since
101
102
  ```
102
103
 
103
104
  > **Package vs. command.** The install package is named `sourcecode` this release
104
105
  > (renaming the distribution is a separate, breaking change). Installing it gives you the
105
106
  > canonical **`ask`** command plus the deprecated **`sourcecode`** alias.
106
107
 
108
+ > **Platforms.** Developed and tested on macOS and Linux; every release runs its full
109
+ > battery there. **Windows/PowerShell is a target we cannot verify:** the CLI is written
110
+ > for it — UTF-8 streams are forced at both console entry points, a successful run writes
111
+ > nothing to stderr (PowerShell 5.1 raises `NativeCommandError` on any stderr write), and
112
+ > the MCP server strips the BOM PowerShell puts on stdin — but no Windows runner exists in
113
+ > the battery. Three separate field reports of console mojibake (`—` rendering as `â€"`)
114
+ > have never reproduced under measurement, and the only variable those reports share is the
115
+ > platform we cannot exercise (C3-3, C3-12, C3-38 in the
116
+ > [defect ledger](docs/DEFECT-LEDGER.md)). On Windows, prefer `--output <file>` over shell
117
+ > redirection, and report a rendering problem rather than assuming it is known.
118
+
107
119
  ---
108
120
 
109
121
  ## Quickstart
@@ -331,7 +343,7 @@ reading JSON sees the same boundary a buyer reads here.
331
343
  | Not covered | Why | What answers it |
332
344
  |---|---|---|
333
345
  | **Whether request input reaches a sink — no dataflow or taint analysis.** | This engine resolves structure and wiring without compiling. Taint needs value flow through a program, which is a different analysis with a different failure mode: an unsound one produces confident findings that are wrong, and every claim here is meant to be checkable against the line that produced it. | A dataflow scanner (Semgrep, CodeQL). What this product adds on top is reachability: which of that tool's findings sit behind an endpoint that is reachable unauthenticated. |
334
- | **Secrets outside Java and Spring configuration — a credential in a deployment descriptor, a Helm value, a CI variable file.** | The file population this analyzer reads is the Java source and the Spring configuration convention. A secret elsewhere is not missed by a weak rule; it is outside the set of files anything here opens. | A dedicated secret scanner over the whole tree (gitleaks, trufflehog). |
346
+ | **Secrets outside Java, Spring configuration and the deployment descriptors — a Helm value, a CI variable file, a shell script.** | The file population this analyzer reads is the Java source, the Spring configuration convention and the descriptors it already parses to resolve an environment. A secret elsewhere is not missed by a weak rule; it is outside the set of files anything here opens. Narrowed by CL-10: a credential stored in `web.xml` or a Spring XML context IS reported (SEC-007), because those files were already being read. | A dedicated secret scanner over the whole tree (gitleaks, trufflehog). |
335
347
  | **Filter-chain order and per-filter URL patterns — the presence of a custom filter is structural only.** | Which filter runs first is decided by bean ordering this analyzer does not resolve. Where two active configurations both match a request, the answer published is `undecided` rather than a guess. | `ask posture --profile <set>` states per endpoint what the readable rules decide and what they leave undecided. |
336
348
  | **Known vulnerabilities in dependencies — no CVE database, no version advisory matching.** | Enriching a vulnerability feed is a different product with a different update cadence; a stale embedded database is worse than no database, because it reads as a clean bill of health. | A dependency scanner (Trivy, OWASP Dependency-Check). `impact-chain` then answers which of its findings anything actually reaches. |
337
349
 
@@ -1,4 +1,4 @@
1
- sourcecode/__init__.py,sha256=ad5-mKQ3VzgQIiOIpQZ3AKVLYX_-QJijCtIl87erKuU,308
1
+ sourcecode/__init__.py,sha256=apfUI_GpozMGQBqfAZGbrSQGmV6iibhGJAm1wFVTQvc,308
2
2
  sourcecode/adaptive_scanner.py,sha256=yJBKjNpkY6bpueYJ2YnRezen3sYZDecEt7WaaNWdqug,9466
3
3
  sourcecode/archetype.py,sha256=HBGTTaS-bVHS6pdacPUMKcMxklkDEKnZMgz48Kc3yec,37630
4
4
  sourcecode/architectural_baseline.py,sha256=7QzJri4pbL3nzAn9gNutZW6mZR8HHD6H2C2H1EEwYPE,17904
@@ -16,7 +16,7 @@ sourcecode/chain_rules.py,sha256=Bi6UHfgd-GxWswmnHRcPz5jdbAuqka3Zkz_P-MTvqhw,127
16
16
  sourcecode/change_plan.py,sha256=kFjjp16XYbupgkv1CPkfqo39_SiZPMRQ8OOfC-Vy9eg,7929
17
17
  sourcecode/cir_graphs.py,sha256=9G0HHj1kw2325IDyzo2OpX73BNswEckecf4MZUXB4JM,12078
18
18
  sourcecode/classifier.py,sha256=JBzPwSSrDG-tUHAbcKB678HRbjLpD-ohzbzzO62mgpo,20114
19
- sourcecode/cli.py,sha256=hGyDps9KReu_vB9wDUHdvPMKfmQ6Yq_Kufh4TTfrOVM,449390
19
+ sourcecode/cli.py,sha256=bPG84FLZTsYZits8DFjyl6tYLBAlWhAeS8-pCU2CvhQ,449990
20
20
  sourcecode/code_notes_analyzer.py,sha256=EJemNCNc9Dn-1RZYu-aNbK0ELzmsyC4s6FdHi3XyNEI,9392
21
21
  sourcecode/compare.py,sha256=xq3zsqAOAw4AWkoD9khb9xDP_O3KvwqO9k-pf8sbi3g,10951
22
22
  sourcecode/confidence_analyzer.py,sha256=5li0NyOdS3Ie-f2ZY1gBmnjtkuRvI50l4OnGaKkXAOU,22358
@@ -40,7 +40,7 @@ sourcecode/endpoint_literals.py,sha256=Qf4gTZzvNSFDGDuOvF0YRL9NvaYKKj24klhR5LIOa
40
40
  sourcecode/endpoint_metrics.py,sha256=sLSLUIgiIyvNdOynaxVgHd9SjRF_DbN3QxevaquykaU,2840
41
41
  sourcecode/entrypoint_classifier.py,sha256=jhTYlyqDJH2AtdEcLVaRU3lYRTJuF8DkxVzl4-W3zWE,5322
42
42
  sourcecode/env_analyzer.py,sha256=oLz4gDUE3BHlRRn6Qj4rnbjwyYjIZ0nlqO_SBQwL_H8,21999
43
- sourcecode/envelope.py,sha256=OBYOfhcwXf6W9sq7iO3oQU0-f_EFstquupVAMJtfdUY,6900
43
+ sourcecode/envelope.py,sha256=IIUY6q_VPGsDfmlZri1GORXcYiAKvQepGoI-B0N0orE,7372
44
44
  sourcecode/environment_resolution.py,sha256=FajZhyLFJblYMb4VORm1fdP4RsQ8TJKySG9uh5IE7bU,13838
45
45
  sourcecode/error_schema.py,sha256=uwosfNaSujtYm11_732Hu92z5ITV040fQDaIyefSvR4,1683
46
46
  sourcecode/evidence_provider.py,sha256=GSSL44JEaouO5AHks2sB3d1YvC9xIKIld1yBYxZpXxo,4277
@@ -63,7 +63,7 @@ sourcecode/mcp_nudge.py,sha256=lKemOqK_wny2u7Ymcr2Idi5Kx8pXY02jCi-_nJYLGMg,2992
63
63
  sourcecode/metrics_analyzer.py,sha256=gLoRWKygF18jLgwsqmGXSWopw-f5iO1VbM7Jjdif6ag,22809
64
64
  sourcecode/migrate_check.py,sha256=jMv1GMhMthpHCnoEtkKWHiIC2Not2kdbU4UaA9sWX7I,134643
65
65
  sourcecode/migration_blast.py,sha256=OPryWkM6-PbvcG3eJFdcWUnlpXBje41nu2BkrZnE2hg,9359
66
- sourcecode/non_coverage.py,sha256=YoCIOlLYoBDv94rxQ5r7L755WeEz05OKo2zU_jt9_a8,7985
66
+ sourcecode/non_coverage.py,sha256=u6-09dt21B2IltIWPC53KAo1ok7PU-u1rM38N16RhRY,8238
67
67
  sourcecode/openapi_surface.py,sha256=BTt0K-woZbkbWTN77IkqeBm_Okag9owR0848fmot8sk,16207
68
68
  sourcecode/output_budget.py,sha256=__DQrIg7MGsYrd0_S3lyd3AG0a0jWNGr55v5Gg9kK0U,12347
69
69
  sourcecode/parse_cache.py,sha256=SnHOhNTvAqHm_PXImPVBuj7NEoOQRn8dfIZHEqOFENs,7565
@@ -71,7 +71,7 @@ sourcecode/path_admission.py,sha256=OGNSoluhVh8YYOBZBnACUrkmH4Tp_yxjGLifkcsxQpo,
71
71
  sourcecode/path_filters.py,sha256=qPKO7kRmVp2y9zjLNSuAVCbcpZrIInHE4QulLUlzPFI,10412
72
72
  sourcecode/perf.py,sha256=GAcEoouPIlPMCQIcHNToxK6K3WdIR-lj9aFg4prOYJI,9743
73
73
  sourcecode/pipe_contract.py,sha256=PML0Er5d8uDyec4OrdUXuyqHbGPUYndjeaKUjkFz2u4,8369
74
- sourcecode/posture.py,sha256=-r4LxE1-GISz8hvKmVMQRipEWlAPbmtVbi6Z6D0DFTI,49479
74
+ sourcecode/posture.py,sha256=obE9lB3dxan5MDOm3eFQ-aQX3DrJQxAU_D2OyYZrYwk,53981
75
75
  sourcecode/pr_comment_renderer.py,sha256=239PmJdf95av_ZW236C7_tvq_ahECeuF9AZxrheJ4OQ,15573
76
76
  sourcecode/pr_impact.py,sha256=eaFgcPMDsjaF8VZvcitFWCPCabMTi-khtGdxpK0wXTQ,22461
77
77
  sourcecode/prepare_context.py,sha256=B8TiBQbazxUOz6D37Dt9zqbOtD5b2S_fR3MB15P9DrI,237206
@@ -81,6 +81,7 @@ sourcecode/readiness_timeline.py,sha256=qqc14vL9nj9E4qw4bVZpGhfMLaVaYr9s6Pak_VUI
81
81
  sourcecode/reconciliation.py,sha256=GU-1PTcVr8zcbtC7BASfpHcZndP9AdboXPNQiBc0fzo,34251
82
82
  sourcecode/redactor.py,sha256=SB4hwIvg8h-hvcqKcDWaZvA-aSyn-at-BIRwa0tUv5E,3227
83
83
  sourcecode/reference_facts.py,sha256=Ns495c6eTmq2SrqPkcUrJUJru4_wj_yRWm4YDeJwves,13440
84
+ sourcecode/release_info.py,sha256=r8GlRnraTQH_olGkdpbCAzBb0B_Js1cRBPaRe7IUoyU,5378
84
85
  sourcecode/relevance_scorer.py,sha256=0AgEt4KrV73nioMqBgjhGjtY7L2C7L7cSyKtj3IKcrw,9408
85
86
  sourcecode/remedies.py,sha256=oMLpxrup88yCOalrsHWM1-kc0penm68q4HV0H7kxia0,4279
86
87
  sourcecode/rename_refactor.py,sha256=h6dNFlB9aZ_3q6heeHBkgXQeXaT03nvPSsYH6P8qxFg,12965
@@ -88,12 +89,12 @@ sourcecode/repo_classifier.py,sha256=FG1vaWKdWXsWdl-S8hjVMiTqcwgaRXkDyvK4rPcOGtQ
88
89
  sourcecode/repository_ir.py,sha256=t4RSLn00mkru5p54KXsT3tmG78FjXKNFDMl2YR-jLiM,355314
89
90
  sourcecode/ris.py,sha256=Xin3d3U43-VthgRA6ttJnyIow8iyUEsQCBENvZc4eEc,23782
90
91
  sourcecode/risk.py,sha256=SRTJfFHCD8enHXVJapEff6lgwdicfeHOHkEcHp2o6iw,14262
91
- sourcecode/rule_catalog.py,sha256=foq2hypJ3J9yD1lTCTzGAi1NN-t_X47eC-DQwy6147M,4248
92
+ sourcecode/rule_catalog.py,sha256=buU6qZI1j1EjqAWOIzeDhZ9kO_6Tp_Ma-dmexNWkMe0,4324
92
93
  sourcecode/runtime_classifier.py,sha256=uTAD6BDCiBLUZEDRfqk718kM4RTT_vAbfkcOI2_Xx58,18432
93
94
  sourcecode/scanner.py,sha256=z3CV0rcGunu0Y8mpNgp07wI7nxT0pxw1BkXRRtI0Rpo,9609
94
95
  sourcecode/schema.py,sha256=aHNXDf8LGyUC8ZDE_VS9kiskC2-Oswhi_WnpdGy6HDw,24897
95
96
  sourcecode/security_config.py,sha256=KblMEoRiEjrIE68YsPaUAFebxFp8UM7MS7lAk5CGD8U,3531
96
- sourcecode/security_config_scan.py,sha256=tG9KgmOdVOPz8PGJ6FPz3IvbR2irfkXxvTImlA7qrOM,9979
97
+ sourcecode/security_config_scan.py,sha256=TZj8qkg8XAMPPPDSSrJvWrvJn9hmgR-V-Sl9SRF4G2w,16148
97
98
  sourcecode/security_posture.py,sha256=CjJ2Qm87HhKXepnaWx1ncJLW-SxI18udCmUDIkCyAsY,50270
98
99
  sourcecode/semantic_analyzer.py,sha256=bpgdC6m0_ftVtRf3rSdwhbhWjnZnGxRXaZVcfe4BbcQ,95414
99
100
  sourcecode/semantic_impact_engine.py,sha256=t09IirGC3JjQDy33JZd1_WKzQVKXkoNl3-XEUr5kjis,20563
@@ -106,7 +107,7 @@ sourcecode/spring_impact.py,sha256=ie1j6N8qIEvsJjFw3hVsG2BWRFIAXWAG0MZ7rzfJhfQ,7
106
107
  sourcecode/spring_model.py,sha256=zOAgFmrRbG4a6KLm1TJl55aWMyPNsz3OS3FSczqPG6A,16594
107
108
  sourcecode/spring_profiles.py,sha256=-kwrCK0O-MRjrCp6SA1d31t--o4Tg87iqPVrTwxoIUs,19605
108
109
  sourcecode/spring_properties.py,sha256=kPTk5qAJxdbHc1hclhHAP0O-hCIzBn8PTh3lzl-I4a4,8400
109
- sourcecode/spring_security_audit.py,sha256=g2kiO59fFSsUv51a7_0NDY02YovaTuQpGpZ2sPJr0w8,27951
110
+ sourcecode/spring_security_audit.py,sha256=KIjshNuS-cQSUBstqCuvDnp5i1J4_qAwXYKJArL2QpI,28720
110
111
  sourcecode/spring_semantic.py,sha256=UrmLg_4gmBVmMqvSnUMgSReVEERVGv3X7LYDBEs6k_o,16598
111
112
  sourcecode/spring_tx_analyzer.py,sha256=_wqjRktqdPS6PiXXxtTkCD1t6BaE2uhqx6ioYY-ULAw,44153
112
113
  sourcecode/summarizer.py,sha256=0aD4x3vgPngqBCEBKGuES1J2Vk5f7mqCm_ZWErwm3js,27025
@@ -174,15 +175,15 @@ sourcecode/retrieval/steps_impact.py,sha256=vvU4MSEECkrEMTyKqivDuw-R_b32nghk-Na1
174
175
  sourcecode/retrieval/steps_intf.py,sha256=vWyAn17O7G8-CdD9Qx0X-t60X7IpZxBZnmIlBkQUEDo,11657
175
176
  sourcecode/retrieval/steps_struct.py,sha256=Y9043laxxPS9zeV509WRI4vPSHfnjci6WKxZX1Ryqhw,13843
176
177
  sourcecode/retrieval/steps_txsec.py,sha256=5yB4uUrY7_tb4WP2Dh7SEaQwn9m1kS_z6HP0sryKuiM,17753
177
- sourcecode/schemas/envelope-v1.schema.json,sha256=lABkyKqXND039JUY2R6KK47OCDco5mq5PLgkcCXFhOQ,3477
178
+ sourcecode/schemas/envelope-v1.schema.json,sha256=Zu5D2cspa_biP5-JMWY0H6oYGyAyA4sII3x1X7rdAVs,3886
178
179
  sourcecode/telemetry/__init__.py,sha256=ZxV5AJAK5_pk0RnW0B1ntpGPRapOX_XKFJdT_6RuYhE,3502
179
180
  sourcecode/telemetry/config.py,sha256=HD5ccO_k91WxDsnMo1lmxaF906bK3nrLv8qzUR3HbTo,4212
180
181
  sourcecode/telemetry/consent.py,sha256=pQdl-QeLl6Gcibn0eWHSKZrm-HYSsjpVqOnjrgFp8co,2472
181
182
  sourcecode/telemetry/events.py,sha256=4_yeO58U-Cwc1Qb27VB0_EjhmroY0k91n3_VGxeALB8,2776
182
183
  sourcecode/telemetry/filters.py,sha256=RzxauTz8HliO4BllQnXEXc7zTeqdCZi5MgqGEDuW7OQ,6570
183
184
  sourcecode/telemetry/transport.py,sha256=4gGHsq0WeY9VywEZXA3vUxykfiYnw9uuqfjAAec7F8o,1681
184
- sourcecode-4.0.1.dist-info/METADATA,sha256=JWZkVeDBlorjqt5DrZs3HRZUwS98YiMIK4sdxbdjL-c,26915
185
- sourcecode-4.0.1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
186
- sourcecode-4.0.1.dist-info/entry_points.txt,sha256=-JEAdChrK5We51kZcb7OaDcyil-dHBjBPL-NhuO-QY8,89
187
- sourcecode-4.0.1.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
188
- sourcecode-4.0.1.dist-info/RECORD,,
185
+ sourcecode-4.1.0.dist-info/METADATA,sha256=O0Dj7DN2b55Cvv2qxRRbSQJbEbEB9c-jL4ySO1Xd98s,28102
186
+ sourcecode-4.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
187
+ sourcecode-4.1.0.dist-info/entry_points.txt,sha256=-JEAdChrK5We51kZcb7OaDcyil-dHBjBPL-NhuO-QY8,89
188
+ sourcecode-4.1.0.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
189
+ sourcecode-4.1.0.dist-info/RECORD,,