sourcecode 4.0.0__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 +1 -1
- sourcecode/cli.py +63 -14
- sourcecode/envelope.py +10 -0
- sourcecode/non_coverage.py +8 -5
- sourcecode/posture.py +90 -0
- sourcecode/release_info.py +123 -0
- sourcecode/remedies.py +17 -0
- sourcecode/rule_catalog.py +108 -0
- sourcecode/schemas/envelope-v1.schema.json +8 -1
- sourcecode/security_config_scan.py +132 -1
- sourcecode/spring_security_audit.py +19 -2
- {sourcecode-4.0.0.dist-info → sourcecode-4.1.0.dist-info}/METADATA +64 -6
- {sourcecode-4.0.0.dist-info → sourcecode-4.1.0.dist-info}/RECORD +16 -14
- {sourcecode-4.0.0.dist-info → sourcecode-4.1.0.dist-info}/WHEEL +0 -0
- {sourcecode-4.0.0.dist-info → sourcecode-4.1.0.dist-info}/entry_points.txt +0 -0
- {sourcecode-4.0.0.dist-info → sourcecode-4.1.0.dist-info}/licenses/LICENSE +0 -0
sourcecode/__init__.py
CHANGED
sourcecode/cli.py
CHANGED
|
@@ -297,6 +297,27 @@ def _group_help_block() -> str:
|
|
|
297
297
|
return "\n".join(lines)
|
|
298
298
|
|
|
299
299
|
|
|
300
|
+
def _remedy_help_block() -> str:
|
|
301
|
+
"""The front page names the answers the field asked for as missing features.
|
|
302
|
+
|
|
303
|
+
C4-8 was closed in the payload: a limitation now carries the invocation that
|
|
304
|
+
resolves it. But the evaluator who wrote `ask posture --provenance` and `ask
|
|
305
|
+
contracts init` by hand was reading `--help` first, and neither shipped flag
|
|
306
|
+
was on it. The block is rendered from `remedies.REMEDIES`, so the pointer and
|
|
307
|
+
the capability cannot drift — `tests/test_remedies.py` already refuses a
|
|
308
|
+
remedy whose option the CLI does not accept.
|
|
309
|
+
"""
|
|
310
|
+
from sourcecode.remedies import headlines
|
|
311
|
+
|
|
312
|
+
lines = ["[bold]Questions the repository alone does not answer:[/bold]"]
|
|
313
|
+
for invocation, headline in headlines():
|
|
314
|
+
# Bare invocation, like every other block on this page: the header panels
|
|
315
|
+
# print without the `ask ` prefix (C3-16).
|
|
316
|
+
bare = invocation.removeprefix("ask ")
|
|
317
|
+
lines.append(f" {bare:<33}[dim]# {headline}[/dim]")
|
|
318
|
+
return "\n".join(lines)
|
|
319
|
+
|
|
320
|
+
|
|
300
321
|
def _build_help_text() -> str:
|
|
301
322
|
"""Build --help text dynamically based on current license state."""
|
|
302
323
|
try:
|
|
@@ -323,6 +344,9 @@ of files) in minutes. Semantic analysis itself is sub-second; repo indexing domi
|
|
|
323
344
|
endpoints . [dim]# endpoints + effective path + policy[/dim]
|
|
324
345
|
spring-audit . [dim]# TX anomalies + security surface[/dim]
|
|
325
346
|
migrate-check . --compact [dim]# Boot 2→3: located blockers + effort[/dim]
|
|
347
|
+
risk . [dim]# defects ranked by reach × access (exp.)[/dim]
|
|
348
|
+
|
|
349
|
+
{_remedy_help_block()}
|
|
326
350
|
|
|
327
351
|
[bold]Agent context:[/bold]
|
|
328
352
|
ask --compact [dim]# high-signal summary (~2,500–4,000 tokens)[/dim]
|
|
@@ -6438,19 +6462,11 @@ def spring_audit_cmd(
|
|
|
6438
6462
|
help=_NO_CACHE_SUBCOMMAND_HELP,
|
|
6439
6463
|
),
|
|
6440
6464
|
) -> None:
|
|
6441
|
-
"""Spring semantic audit:
|
|
6465
|
+
"""Spring semantic audit: {{RULE_SUMMARY}}.
|
|
6442
6466
|
|
|
6443
6467
|
\b
|
|
6444
6468
|
Detects:
|
|
6445
|
-
|
|
6446
|
-
TX-002 REQUIRES_NEW nested in REQUIRED call chain
|
|
6447
|
-
TX-003 readOnly=true boundary propagating to write operation
|
|
6448
|
-
TX-004 NOT_SUPPORTED/NEVER within active TX chain
|
|
6449
|
-
TX-005 Exception swallowing inside @Transactional
|
|
6450
|
-
TX-006 Self-invocation of @Transactional sibling (proxy bypass)
|
|
6451
|
-
SEC-001 Unsecured endpoint in annotation_based security model
|
|
6452
|
-
SEC-002 CVE-2025-41248: @PreAuthorize on inherited method from generic supertype
|
|
6453
|
-
SEC-003 @Transactional on @Controller/@RestController (TX in wrong layer)
|
|
6469
|
+
{{RULE_LIST}}
|
|
6454
6470
|
|
|
6455
6471
|
\b
|
|
6456
6472
|
Findings and defects are two counts, both published. A rule fires per
|
|
@@ -6482,10 +6498,7 @@ def spring_audit_cmd(
|
|
|
6482
6498
|
\b
|
|
6483
6499
|
What it does not look at (the whole list is in the payload's `non_coverage`,
|
|
6484
6500
|
and in the README):
|
|
6485
|
-
|
|
6486
|
-
NC-002 secrets outside Java and Spring configuration
|
|
6487
|
-
NC-003 filter-chain ORDER — a custom filter's presence is structural only
|
|
6488
|
-
NC-004 known vulnerabilities in dependencies — no CVE matching
|
|
6501
|
+
{{NON_COVERAGE}}
|
|
6489
6502
|
Silence about those is not evidence that there is nothing to find.
|
|
6490
6503
|
"""
|
|
6491
6504
|
import json as _json
|
|
@@ -6587,6 +6600,30 @@ def spring_audit_cmd(
|
|
|
6587
6600
|
raise typer.Exit(code=1)
|
|
6588
6601
|
|
|
6589
6602
|
|
|
6603
|
+
def _render_audit_help(doc: str) -> str:
|
|
6604
|
+
"""Fill the rule and non-coverage blocks from the tables that own them.
|
|
6605
|
+
|
|
6606
|
+
The rules this command detects were written out by hand in its own help, and
|
|
6607
|
+
stayed at `SEC-001..003` for two releases after CL-9 shipped three more — the
|
|
6608
|
+
C4-1 shape, where a curated list beside a generated one drifts and the
|
|
6609
|
+
curated one is what the reader gets. Both blocks are rendered now, so adding
|
|
6610
|
+
a rule updates the help by construction.
|
|
6611
|
+
"""
|
|
6612
|
+
from sourcecode import non_coverage, rule_catalog
|
|
6613
|
+
|
|
6614
|
+
nc_lines = [
|
|
6615
|
+
f" {line}" for line in non_coverage.render_help_lines("security_surface", width=68)
|
|
6616
|
+
]
|
|
6617
|
+
return (
|
|
6618
|
+
doc.replace("{{RULE_SUMMARY}}", rule_catalog.summary_line())
|
|
6619
|
+
.replace("{{RULE_LIST}}", rule_catalog.render_help_block())
|
|
6620
|
+
.replace("{{NON_COVERAGE}}", "\n".join(nc_lines))
|
|
6621
|
+
)
|
|
6622
|
+
|
|
6623
|
+
|
|
6624
|
+
spring_audit_cmd.__doc__ = _render_audit_help(spring_audit_cmd.__doc__ or "")
|
|
6625
|
+
|
|
6626
|
+
|
|
6590
6627
|
# ── verify-edit: in-loop semantic diff gate ───────────────────────────────────
|
|
6591
6628
|
|
|
6592
6629
|
|
|
@@ -8897,8 +8934,16 @@ def version_cmd() -> None:
|
|
|
8897
8934
|
{"cli_version": "1.33.11", "mcp_schema_version": "1.33.11",
|
|
8898
8935
|
"compatibility_schema_version": "1.0"}
|
|
8899
8936
|
"""
|
|
8937
|
+
from sourcecode.release_info import advisory_line, build_status
|
|
8938
|
+
|
|
8900
8939
|
if getattr(sys.stdout, "isatty", lambda: False)():
|
|
8901
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)
|
|
8902
8947
|
else:
|
|
8903
8948
|
import json as _json_ver
|
|
8904
8949
|
from sourcecode.envelope import ENVELOPE_VERSION, available_schemas
|
|
@@ -8906,6 +8951,10 @@ def version_cmd() -> None:
|
|
|
8906
8951
|
"cli_version": __version__,
|
|
8907
8952
|
"mcp_schema_version": __version__,
|
|
8908
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(),
|
|
8909
8958
|
# What a consumer should actually pin against: the release string
|
|
8910
8959
|
# changes every release, the envelope version only when the output
|
|
8911
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
|
|
sourcecode/non_coverage.py
CHANGED
|
@@ -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
|
|
78
|
-
"
|
|
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
|
|
82
|
-
"
|
|
83
|
-
"
|
|
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"]
|
sourcecode/remedies.py
CHANGED
|
@@ -38,6 +38,10 @@ class Remedy:
|
|
|
38
38
|
#: feature name — the evaluator's request was written in their own words, so
|
|
39
39
|
#: the pointer has to be recognisable from the question they were asking.
|
|
40
40
|
answers: str
|
|
41
|
+
#: The same thing in a help-screen column. A payload can afford the sentence;
|
|
42
|
+
#: a front page cannot, and rewriting it there by hand is how the pointer and
|
|
43
|
+
#: the capability drift apart.
|
|
44
|
+
headline: str = ""
|
|
41
45
|
|
|
42
46
|
@property
|
|
43
47
|
def invocation(self) -> str:
|
|
@@ -63,6 +67,7 @@ REMEDIES: "dict[str, Remedy]" = {
|
|
|
63
67
|
"this repository; to read every artefact that can set it with file:line "
|
|
64
68
|
"and rank the admissible sets by what they leave reachable"
|
|
65
69
|
),
|
|
70
|
+
headline="which profile set actually runs",
|
|
66
71
|
),
|
|
67
72
|
Remedy(
|
|
68
73
|
key="no_contracts_declared",
|
|
@@ -73,11 +78,23 @@ REMEDIES: "dict[str, Remedy]" = {
|
|
|
73
78
|
"and write `.ask/contracts.yml` from the current measurement instead of "
|
|
74
79
|
"by hand"
|
|
75
80
|
),
|
|
81
|
+
headline="derive contracts from measurement",
|
|
76
82
|
),
|
|
77
83
|
)
|
|
78
84
|
}
|
|
79
85
|
|
|
80
86
|
|
|
87
|
+
def headlines() -> "tuple[tuple[str, str], ...]":
|
|
88
|
+
"""`(invocation, headline)` for every remedy, for a help screen or a README.
|
|
89
|
+
|
|
90
|
+
Rendered rather than restated: the front page is where the evaluator looked
|
|
91
|
+
first and found neither of these commands.
|
|
92
|
+
"""
|
|
93
|
+
return tuple(
|
|
94
|
+
(r.invocation, r.headline or r.answers) for r in REMEDIES.values()
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
81
98
|
def remedy(key: str) -> Remedy:
|
|
82
99
|
"""The remedy for `key`. Raises rather than returning a silent blank, because a
|
|
83
100
|
missing pointer is exactly the defect this module exists to prevent."""
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""The rules `spring-audit` ships, in the one place every reader is rendered from.
|
|
2
|
+
|
|
3
|
+
The help screen used to name its own rules. It said `SEC-001..003` for two
|
|
4
|
+
releases after CL-9 shipped SEC-004, SEC-005 and SEC-006, so the product
|
|
5
|
+
advertised three security rules while holding six — the same shape as C4-1
|
|
6
|
+
(`endpoints` missing from every help list) and C4-8 (a shipped capability the
|
|
7
|
+
field asked for as a feature): a curated list beside a generated one drifts, and
|
|
8
|
+
the curated one is what a buyer reads.
|
|
9
|
+
|
|
10
|
+
Two seams keep this honest, and neither is a promise:
|
|
11
|
+
|
|
12
|
+
* The **summaries** live here, keyed by rule id, and `--help` renders from them
|
|
13
|
+
rather than restating them.
|
|
14
|
+
* The **id set** is not authored here at all. It belongs to the pattern
|
|
15
|
+
registries in `spring_tx_analyzer` and `spring_security_audit`, and
|
|
16
|
+
`tests/test_rule_catalog.py` derives it from them: a seventh SEC rule fails
|
|
17
|
+
the build until it has a line in this table. That import is deliberately not
|
|
18
|
+
done at module level — the analyzers are heavy and this module is imported to
|
|
19
|
+
render a help screen.
|
|
20
|
+
|
|
21
|
+
A pattern class that emits several ids declares them in `pattern_ids`; one that
|
|
22
|
+
emits its own declares nothing and is read through `pattern_id`.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
from dataclasses import dataclass
|
|
28
|
+
|
|
29
|
+
SCHEMA_VERSION = "rule-catalog-v1"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True)
|
|
33
|
+
class Rule:
|
|
34
|
+
"""One audit rule, as a reader meets it."""
|
|
35
|
+
|
|
36
|
+
id: str
|
|
37
|
+
family: str # "TX" or "SEC" — the scope flag that selects it
|
|
38
|
+
summary: str # one line, no wrapping games
|
|
39
|
+
|
|
40
|
+
def to_dict(self) -> dict:
|
|
41
|
+
return {"id": self.id, "family": self.family, "summary": self.summary}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
RULES: tuple[Rule, ...] = (
|
|
45
|
+
Rule("TX-001", "TX", "@Transactional on private/final method (CGLIB proxy bypass)"),
|
|
46
|
+
Rule("TX-002", "TX", "REQUIRES_NEW nested in REQUIRED call chain"),
|
|
47
|
+
Rule("TX-003", "TX", "readOnly=true boundary propagating to write operation"),
|
|
48
|
+
Rule("TX-004", "TX", "NOT_SUPPORTED/NEVER within active TX chain"),
|
|
49
|
+
Rule("TX-005", "TX", "Exception swallowing inside @Transactional"),
|
|
50
|
+
Rule("TX-006", "TX", "Self-invocation of @Transactional sibling (proxy bypass)"),
|
|
51
|
+
Rule("SEC-001", "SEC", "Unsecured endpoint in annotation_based security model"),
|
|
52
|
+
Rule("SEC-002", "SEC", "CVE-2025-41248: @PreAuthorize inherited from a generic supertype"),
|
|
53
|
+
Rule("SEC-003", "SEC", "@Transactional on @Controller/@RestController (TX in wrong layer)"),
|
|
54
|
+
Rule("SEC-004", "SEC", "Password stored under a fast unsalted digest (SHA-1, MD5)"),
|
|
55
|
+
Rule("SEC-005", "SEC", "CSRF disabled under session-bearing authentication"),
|
|
56
|
+
Rule("SEC-006", "SEC", "Cookie created without the Secure attribute"),
|
|
57
|
+
Rule("SEC-007", "SEC", "Credential stored in a deployment descriptor"),
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
FAMILY_ORDER: tuple[str, ...] = ("TX", "SEC")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def ids() -> tuple[str, ...]:
|
|
64
|
+
"""Every rule id this build can emit, in help order."""
|
|
65
|
+
return tuple(rule.id for rule in RULES)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def for_family(family: str) -> tuple[Rule, ...]:
|
|
69
|
+
return tuple(rule for rule in RULES if rule.family == family)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def range_label(family: str) -> str:
|
|
73
|
+
"""`TX-001..006` — the span a one-line summary can carry without lying."""
|
|
74
|
+
rules = for_family(family)
|
|
75
|
+
if not rules:
|
|
76
|
+
return ""
|
|
77
|
+
if len(rules) == 1:
|
|
78
|
+
return rules[0].id
|
|
79
|
+
first, last = rules[0].id, rules[-1].id
|
|
80
|
+
_, _, tail = last.partition("-")
|
|
81
|
+
return f"{first}..{tail}"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def summary_line() -> str:
|
|
85
|
+
"""The command's one-liner: the families and how far each one reaches."""
|
|
86
|
+
return " + ".join(
|
|
87
|
+
f"{'TX anomalies' if fam == 'TX' else 'security surface'} "
|
|
88
|
+
f"({range_label(fam)})"
|
|
89
|
+
for fam in FAMILY_ORDER
|
|
90
|
+
if for_family(fam)
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def help_lines(*, indent: str = " ") -> list[str]:
|
|
95
|
+
"""One line per rule, id column aligned, for a `--help` screen."""
|
|
96
|
+
width = max(len(rule.id) for rule in RULES)
|
|
97
|
+
return [f"{indent}{rule.id.ljust(width)} {rule.summary}" for rule in RULES]
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def render_help_block(*, indent: str = " ") -> str:
|
|
101
|
+
return "\n".join(help_lines(indent=indent))
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def render_markdown() -> str:
|
|
105
|
+
"""The docs table, generated from the same tuple."""
|
|
106
|
+
lines = ["| Rule | Detects |", "|---|---|"]
|
|
107
|
+
lines.extend(f"| `{rule.id}` | {rule.summary} |" for rule in RULES)
|
|
108
|
+
return "\n".join(lines)
|
|
@@ -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": [
|
|
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
|
-
|
|
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,12 +402,20 @@ 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 = {
|
|
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
|
}
|
|
416
|
+
#: A pattern that emits more than one rule id declares them all, so a catalogue
|
|
417
|
+
#: reader is never left with the group's identity standing for the group.
|
|
418
|
+
pattern_ids = tuple(_TITLE)
|
|
409
419
|
_FIX = {
|
|
410
420
|
"SEC-004": (
|
|
411
421
|
"Store passwords with an adaptive, salted function — BCryptPasswordEncoder, "
|
|
@@ -422,6 +432,13 @@ class _SEC00XConfigurationWeaknesses:
|
|
|
422
432
|
"Set the Secure attribute (and HttpOnly, and SameSite) on session cookies, "
|
|
423
433
|
"and configure it per environment rather than in code."
|
|
424
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
|
+
),
|
|
425
442
|
}
|
|
426
443
|
|
|
427
444
|
def analyze(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sourcecode
|
|
3
|
-
Version: 4.
|
|
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
|
|
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
|
|
@@ -129,6 +141,26 @@ ask spring-audit /path/to/repo
|
|
|
129
141
|
ask migrate-check /path/to/repo --compact
|
|
130
142
|
```
|
|
131
143
|
|
|
144
|
+
**Then the three that finish the sentence the four above start.** A profile-set answer stays
|
|
145
|
+
conditional until you know which set the deployment starts with, a gate nobody can populate is
|
|
146
|
+
a gate nobody runs, and a defect ranked without its reach is a linter note:
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
# Which profile set ACTUALLY runs? Reads every artefact that can set it — build,
|
|
150
|
+
# descriptors, Dockerfile, Compose, Kubernetes, launch scripts — with file:line, and
|
|
151
|
+
# says NOT DECIDED IN THIS REPOSITORY when nothing in the tree decides it. Then ranks
|
|
152
|
+
# every admissible set by what it leaves reachable without authentication.
|
|
153
|
+
ask posture /path/to/repo --resolve-environments
|
|
154
|
+
|
|
155
|
+
# Derive the contracts this repository already satisfies, execute each one before
|
|
156
|
+
# writing it, and populate .ask/contracts.yml from the measurement instead of by hand.
|
|
157
|
+
ask verify /path/to/repo --init
|
|
158
|
+
|
|
159
|
+
# What each defect actually costs once reach, access and write effect are in it:
|
|
160
|
+
# severity × reachability × auth_verdict × write_effect, ordered, every factor traceable.
|
|
161
|
+
ask risk /path/to/repo
|
|
162
|
+
```
|
|
163
|
+
|
|
132
164
|
Then the everyday loop:
|
|
133
165
|
|
|
134
166
|
```bash
|
|
@@ -145,13 +177,17 @@ ask onboard /path/to/repo
|
|
|
145
177
|
ask review-pr /path/to/repo --since main
|
|
146
178
|
|
|
147
179
|
# CI gate on NEW violations only, instead of on pre-existing debt
|
|
180
|
+
ask verify /path/to/repo --init # derive the contracts, don't hand-write them
|
|
148
181
|
ask verify /path/to/repo --capture-baseline # accept today's debt, once
|
|
149
182
|
ask verify /path/to/repo # then: only new violations block
|
|
150
183
|
```
|
|
151
184
|
|
|
152
|
-
> **Adopting a gate on a real codebase.**
|
|
153
|
-
>
|
|
154
|
-
>
|
|
185
|
+
> **Adopting a gate on a real codebase.** Nobody hand-writes contracts for a 3 000-file
|
|
186
|
+
> monolith, so `ask verify --init` derives the ones the repository satisfies **today** and
|
|
187
|
+
> executes each before writing it — init followed by verify passes by construction.
|
|
188
|
+
> A repository that starts declaring contracts already violates them somewhere; `ask verify`
|
|
189
|
+
> is **baseline-relative by default** (`--fail-on new`) so the gate survives contact with
|
|
190
|
+
> reality instead of being switched off on day one.
|
|
155
191
|
> `ask baseline capture|diff|trend` is a different thing: versioned architectural metrics over
|
|
156
192
|
> time, for trend reporting rather than blocking.
|
|
157
193
|
|
|
@@ -194,10 +230,32 @@ What a profile set **actually wires**: which conditional beans register, which d
|
|
|
194
230
|
`ask posture` · `ask posture --diff dev:prod` · `ask posture --property k=v`
|
|
195
231
|
→ [posture.md](docs/posture.md)
|
|
196
232
|
|
|
233
|
+
**`--resolve-environments` carries it past the edge of the repository.** Every answer above
|
|
234
|
+
is conditional on a profile set, and which set runs is decided by the deployment. This flag
|
|
235
|
+
reads every artefact in the tree that can set `spring.profiles.active` — Maven/Gradle,
|
|
236
|
+
`web.xml`, `Dockerfile`, Compose, Kubernetes, launch scripts — with `file:line`, and returns
|
|
237
|
+
`decided_in_repository`, `artefacts_disagree` (two deployments, never resolved by picking
|
|
238
|
+
one) or **`NOT DECIDED IN THIS REPOSITORY`**. Then it resolves every admissible set over one
|
|
239
|
+
parse and publishes the worst. A field audit closed those five hops by hand with grep before
|
|
240
|
+
the flag existed.
|
|
241
|
+
`ask posture . --resolve-environments`
|
|
242
|
+
|
|
197
243
|
Unresolved is a first-class outcome: a condition the resolver cannot decide is reported as a
|
|
198
244
|
hole with the condition named, never folded into active or inactive. **A posture answer that
|
|
199
245
|
guesses is a confident security falsehood — the worst failure mode this tool has.**
|
|
200
246
|
|
|
247
|
+
### 5c · Composed Risk *(experimental)*
|
|
248
|
+
Every command above answers one axis, and a reader composes them by hand. `ask risk` does the
|
|
249
|
+
join over the endpoint and symbol ids these commands already share with each other:
|
|
250
|
+
`severity_effective = defect_severity × reachability × auth_verdict × write_effect`. The
|
|
251
|
+
product of the published factors **is** the published score — decomposable to the four
|
|
252
|
+
figures and the authority behind each — and an axis this build cannot measure is `unknown`,
|
|
253
|
+
weighted 1.0 and named in `blind_axes` rather than silently treated as safe. Measured on a
|
|
254
|
+
field case: a defect both `spring-audit` and `impact-chain` called *medium* composes to
|
|
255
|
+
**high (8.06)** once *reachable unauthenticated* and *writes to the database* are in it.
|
|
256
|
+
`ask risk .` · `ask risk . --min-band high` · `ask risk . --limit 10`
|
|
257
|
+
→ [reference](docs/USER_GUIDE.md)
|
|
258
|
+
|
|
201
259
|
### 6 · Developer Workflows
|
|
202
260
|
The everyday loop: diff-based PR review, symptom-driven bug triage, and delta context for continuous agent runs.
|
|
203
261
|
`ask review-pr` · `ask fix-bug` · `ask prepare-context`
|
|
@@ -285,7 +343,7 @@ reading JSON sees the same boundary a buyer reads here.
|
|
|
285
343
|
| Not covered | Why | What answers it |
|
|
286
344
|
|---|---|---|
|
|
287
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. |
|
|
288
|
-
| **Secrets outside Java
|
|
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). |
|
|
289
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. |
|
|
290
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. |
|
|
291
349
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
sourcecode/__init__.py,sha256=
|
|
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=
|
|
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=
|
|
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=
|
|
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
|
|
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,18 +81,20 @@ 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
|
-
sourcecode/remedies.py,sha256=
|
|
86
|
+
sourcecode/remedies.py,sha256=oMLpxrup88yCOalrsHWM1-kc0penm68q4HV0H7kxia0,4279
|
|
86
87
|
sourcecode/rename_refactor.py,sha256=h6dNFlB9aZ_3q6heeHBkgXQeXaT03nvPSsYH6P8qxFg,12965
|
|
87
88
|
sourcecode/repo_classifier.py,sha256=FG1vaWKdWXsWdl-S8hjVMiTqcwgaRXkDyvK4rPcOGtQ,22681
|
|
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
|
|
92
|
+
sourcecode/rule_catalog.py,sha256=buU6qZI1j1EjqAWOIzeDhZ9kO_6Tp_Ma-dmexNWkMe0,4324
|
|
91
93
|
sourcecode/runtime_classifier.py,sha256=uTAD6BDCiBLUZEDRfqk718kM4RTT_vAbfkcOI2_Xx58,18432
|
|
92
94
|
sourcecode/scanner.py,sha256=z3CV0rcGunu0Y8mpNgp07wI7nxT0pxw1BkXRRtI0Rpo,9609
|
|
93
95
|
sourcecode/schema.py,sha256=aHNXDf8LGyUC8ZDE_VS9kiskC2-Oswhi_WnpdGy6HDw,24897
|
|
94
96
|
sourcecode/security_config.py,sha256=KblMEoRiEjrIE68YsPaUAFebxFp8UM7MS7lAk5CGD8U,3531
|
|
95
|
-
sourcecode/security_config_scan.py,sha256=
|
|
97
|
+
sourcecode/security_config_scan.py,sha256=TZj8qkg8XAMPPPDSSrJvWrvJn9hmgR-V-Sl9SRF4G2w,16148
|
|
96
98
|
sourcecode/security_posture.py,sha256=CjJ2Qm87HhKXepnaWx1ncJLW-SxI18udCmUDIkCyAsY,50270
|
|
97
99
|
sourcecode/semantic_analyzer.py,sha256=bpgdC6m0_ftVtRf3rSdwhbhWjnZnGxRXaZVcfe4BbcQ,95414
|
|
98
100
|
sourcecode/semantic_impact_engine.py,sha256=t09IirGC3JjQDy33JZd1_WKzQVKXkoNl3-XEUr5kjis,20563
|
|
@@ -105,7 +107,7 @@ sourcecode/spring_impact.py,sha256=ie1j6N8qIEvsJjFw3hVsG2BWRFIAXWAG0MZ7rzfJhfQ,7
|
|
|
105
107
|
sourcecode/spring_model.py,sha256=zOAgFmrRbG4a6KLm1TJl55aWMyPNsz3OS3FSczqPG6A,16594
|
|
106
108
|
sourcecode/spring_profiles.py,sha256=-kwrCK0O-MRjrCp6SA1d31t--o4Tg87iqPVrTwxoIUs,19605
|
|
107
109
|
sourcecode/spring_properties.py,sha256=kPTk5qAJxdbHc1hclhHAP0O-hCIzBn8PTh3lzl-I4a4,8400
|
|
108
|
-
sourcecode/spring_security_audit.py,sha256
|
|
110
|
+
sourcecode/spring_security_audit.py,sha256=KIjshNuS-cQSUBstqCuvDnp5i1J4_qAwXYKJArL2QpI,28720
|
|
109
111
|
sourcecode/spring_semantic.py,sha256=UrmLg_4gmBVmMqvSnUMgSReVEERVGv3X7LYDBEs6k_o,16598
|
|
110
112
|
sourcecode/spring_tx_analyzer.py,sha256=_wqjRktqdPS6PiXXxtTkCD1t6BaE2uhqx6ioYY-ULAw,44153
|
|
111
113
|
sourcecode/summarizer.py,sha256=0aD4x3vgPngqBCEBKGuES1J2Vk5f7mqCm_ZWErwm3js,27025
|
|
@@ -173,15 +175,15 @@ sourcecode/retrieval/steps_impact.py,sha256=vvU4MSEECkrEMTyKqivDuw-R_b32nghk-Na1
|
|
|
173
175
|
sourcecode/retrieval/steps_intf.py,sha256=vWyAn17O7G8-CdD9Qx0X-t60X7IpZxBZnmIlBkQUEDo,11657
|
|
174
176
|
sourcecode/retrieval/steps_struct.py,sha256=Y9043laxxPS9zeV509WRI4vPSHfnjci6WKxZX1Ryqhw,13843
|
|
175
177
|
sourcecode/retrieval/steps_txsec.py,sha256=5yB4uUrY7_tb4WP2Dh7SEaQwn9m1kS_z6HP0sryKuiM,17753
|
|
176
|
-
sourcecode/schemas/envelope-v1.schema.json,sha256=
|
|
178
|
+
sourcecode/schemas/envelope-v1.schema.json,sha256=Zu5D2cspa_biP5-JMWY0H6oYGyAyA4sII3x1X7rdAVs,3886
|
|
177
179
|
sourcecode/telemetry/__init__.py,sha256=ZxV5AJAK5_pk0RnW0B1ntpGPRapOX_XKFJdT_6RuYhE,3502
|
|
178
180
|
sourcecode/telemetry/config.py,sha256=HD5ccO_k91WxDsnMo1lmxaF906bK3nrLv8qzUR3HbTo,4212
|
|
179
181
|
sourcecode/telemetry/consent.py,sha256=pQdl-QeLl6Gcibn0eWHSKZrm-HYSsjpVqOnjrgFp8co,2472
|
|
180
182
|
sourcecode/telemetry/events.py,sha256=4_yeO58U-Cwc1Qb27VB0_EjhmroY0k91n3_VGxeALB8,2776
|
|
181
183
|
sourcecode/telemetry/filters.py,sha256=RzxauTz8HliO4BllQnXEXc7zTeqdCZi5MgqGEDuW7OQ,6570
|
|
182
184
|
sourcecode/telemetry/transport.py,sha256=4gGHsq0WeY9VywEZXA3vUxykfiYnw9uuqfjAAec7F8o,1681
|
|
183
|
-
sourcecode-4.
|
|
184
|
-
sourcecode-4.
|
|
185
|
-
sourcecode-4.
|
|
186
|
-
sourcecode-4.
|
|
187
|
-
sourcecode-4.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|