fix(contributor-scan): retain declared Gruntfile and report census gaps honestly

This commit is contained in:
Marius Ciepluch
2026-09-14 08:07:06 +02:00
parent 88216c5072
commit 7c1c2d4475
18 changed files with 1048 additions and 25 deletions
@@ -0,0 +1,76 @@
# Task 2.19 — measured input retention and census completeness
The maintainer accepted the proposed contributor-scan follow-up after the pre-push
NO-GO report. The Kiro root-cause-first workflow identified a concrete shared-boundary
repair; no new detector, ranking policy or rollout threshold is introduced.
## Debug Report
- ROOT_CAUSE: Joern 4.0.625's default filename filter excludes Gruntfile.js. A fresh read-verified NodeGoat input has 44 files/95,411 bytes but its raw graph census contains 43 files, 564 methods and 3,750 calls. The partition adapter substitutes zero counts for this incomplete census, incorrectly producing cpg_empty.
- CATEGORY: CONFIG_GAP
- FIX_PLAN: Retain the original Gruntfile under an explicit versioned frontend policy; preserve actual census counts and named omissions while keeping incomplete questions unresolved.
- VERIFICATION: Exact pinned Node input/census before and after; packaged native PHP/JavaScript partition smoke; malformed/missing census controls; full regression suite and canonical gates.
- NEXT_ACTION: RETRY_TASK
- CONFIDENCE: HIGH
- NOTES: Independent quality evidence, VAmPI context/transitive witness and runtime amortization remain separate blockers. The debug timing is not a representative hook latency profile.
## Implementation and review
`JoernBackend.include_build_configs` explicitly sets the frontend environment option,
overriding inherited ambient values. Runtime and graph identity include the option.
The pinned adaptation exempts only the `Gruntfile.js` basename from the known default
filter when enabled. Paths, bytes and graph connectivity are preserved; no configuration
is executed. Existing test retention and vendor exclusion remain intact.
Partition census rejection now carries actual counts plus a named completeness reason
and the missing filenames when available. Scan scope and outcomes consume that reason
in both evidence and arbitration passes. A nonempty graph is no longer reported empty.
Empty, absent or malformed census payloads produce an explicit unavailable marker,
with no invented observed counts. Review found this latter gap in the first correction;
three failing controls reproduced it before repair. A missing-file marker survives
aggregation with another valid partition; existing sharded/empty graph limits remain.
RED evidence: `/tmp/owner-retention-red.log` and `owner-retention-off.log` fail the new
explicit policy assertion. Default ON passes; the supported opt-out remains a real input
policy. Census diagnostics: `owner-census-diagnostic-red.log` and `-off.log` fail three
new expectations; ON passes and the temporary switch was removed. Additional empty,
malformed and absent census controls fail before repair (`owner-census-empty-red.log`).
The final focused suite passes 20 tests, including the combined push regression controls.
The raw Node check after retention finds all 44 files and 579 methods, with no missing
or extra source names. Both runs use exactly 95,411 readable source bytes. Diagnostic
build/census costs remain tens of seconds and cannot qualify the 30-second hook gate.
The before/after checks use packaged images; final partition smoke also exercises the
updated scan completeness path. Existing eligibility becomes stale after runtime/core
identity changes and admits nothing; no old measurement is relabeled as current PASS.
Review also checked regression fixtures that intentionally exercise complete graph evidence.
Those backends now provide a named census for the readable files they are given, matching
the production contract. Their ranking, pruning and comparison assertions are unchanged.
A genuinely empty graph retains `cpg_empty`; only unavailable/partial evidence uses the
new explicit census reasons. Counts and missing filenames are carried into the saved
scan degradations. The final focused scan/scope/partition/policy suite passes 74 tests.
The final offline image's mixed native PHP/JavaScript partition smoke passes, including
original Gruntfile method/file nodes, test directories/suffixes, first-party source byte
receipts and absence of vendor nodes. No language-specific ranking change was made.
## Status Report
- STATUS: READY_FOR_REVIEW
- TASK: 2.19
- RED_PHASE_OUTPUT: Explicit policy OFF and census preservation OFF failures recorded above; three additional missing/malformed census failures reproduced before repair. Temporary diagnostic switch removed.
- TESTS_RUN: Final full suite 1321 passed, 9 skipped in 120.96s, exit 0; 74 focused controls pass; ruff/format/mypy/compileall and detector/map/pair gates pass. Final image builds, native mixed partition smoke exits 0, installed builder identity and stale eligibility rejection verified.
## Review Verdict
- VERDICT: APPROVED
- TASK: 2.19
- MECHANICAL_RESULTS: Actual diff reviewed; full regression and packaged checks pass. Existing ordinary-scan test backends now supply named readable-file censuses while retaining their original assertions. No new placeholders, secrets, rank heuristics or vendor restoration.
- FINDINGS: Actual empty graphs retain cpg_empty. Nonempty partial graphs retain counts and missing names; absent/empty/malformed census cannot authorize completion or tier-zero pruning. Runtime/cache identity binds the retention option and installed adaptation. Main-context Kiro fallback; no independent review claimed.
- SUMMARY: Targeted shared input/census repair verified without a latency, transitive-detection or rollout claim.
## Verification Result
- STATUS: VERIFIED
- CLAIM_TYPE: TASK
- CLAIM: Task 2.19 repairs the measured Gruntfile omission and honest census-completeness handling.
- EVIDENCE: `benchmarks/measurements/2026-09-14-node-census-repair.json` preserves all source receipts, raw before/after census, reproducer, build/smoke logs, packaged identity and final test/gate results. Node census 43/44 → 44/44, methods 564 → 579. Final image `898789cb6cc88507a9fa67276fd442ee4ef9e9f1135b333bafda511fb0532b25` passes mixed native PHP/JavaScript smoke, including the original build/test methods and vendor absence.
- GAPS: Representative runtime/coverage gates, VAmPI change context/transitive evidence and independent qualification remain unresolved. Pre-push task 8.3 and rollout remain NO-GO. Old eligibility is stale and enables no capabilities.
@@ -64,3 +64,28 @@ boundaries explicit. Do not fabricate transitivity from a raw finding at the tar
Revalidate ordinary VAmPI scan, exact question accounting, push regressions and independent Revalidate ordinary VAmPI scan, exact question accounting, push regressions and independent
qualification after an approved owner fix. This note records the prerequisite; it does qualification after an approved owner fix. This note records the prerequisite; it does
not silently approve an upstream implementation or change detector semantics. not silently approve an upstream implementation or change detector semantics.
## 2026-09-14 census diagnosis and targeted retention repair
The maintainer accepted the proposed contributor-scan follow-up with "ok" after
group 8's NO-GO report. This authorizes the targeted shared-boundary fixes and their
validation; independent qualification and rollout still require their existing gates.
A fresh offline packaged probe copied and reopened all 44 exact NodeGoat first-party
JavaScript inputs (95,411 bytes), built a graph and inspected its raw named census.
Result: 43 files, 564 methods, 3,750 calls; only `Gruntfile.js` was missing. Build took
21.708s and census 14.990s. Tests were retained. The pinned frontend's
`IgnoredFilesRegex` explicitly drops `Gruntfile.js`; the partition adapter then replaced
the partial census with zero counts, producing the misleading `cpg_empty` diagnosis.
The first repair narrowly extends the existing versioned frontend adaptation to retain
`Gruntfile.js` under explicit `OUSAST_INCLUDE_BUILD_CONFIGS=1`. It changes neither its
path nor bytes and leaves all other defaults, vendor exclusion and test retention
intact. The backend owns the option and binds it to graph/runtime identity, so old
artifacts cannot silently qualify under the new retention policy. It does not execute
the build configuration. This format-level input repair is not NodeGoat-specific scope
selection or a claim that every possible JavaScript configuration file is now supported.
The partial-census correction now preserves actual counts and missing filenames,
with missing/empty/malformed census still preventing completed questions. Runtime amortization and VAmPI context/witness evidence remain open.
Source: [Joern 4.0.625 AstGenRunner](https://raw.githubusercontent.com/joernio/joern/v4.0.625/joern-cli/frontends/jssrc2cpg/src/main/scala/io/joern/jssrc2cpg/utils/AstGenRunner.scala).
+12 -1
View File
@@ -1,7 +1,7 @@
{ {
"feature_name": "contributor-scan", "feature_name": "contributor-scan",
"created_at": "2026-09-08T20:00:00Z", "created_at": "2026-09-08T20:00:00Z",
"updated_at": "2026-09-13T09:29:15.806475+00:00", "updated_at": "2026-09-14T06:06:32.633059+00:00",
"language": "en", "language": "en",
"phase": "implementation", "phase": "implementation",
"approvals": { "approvals": {
@@ -46,6 +46,11 @@
"at": "2026-09-13T09:17:01.788332+00:00", "at": "2026-09-13T09:17:01.788332+00:00",
"source": "approved", "source": "approved",
"action": "Approved frontend-retention prerequisite, task 2.18, including reproducible dependency adaptation; resume pre-push-safety-net 3.4 after verification." "action": "Approved frontend-retention prerequisite, task 2.18, including reproducible dependency adaptation; resume pre-push-safety-net 3.4 after verification."
},
{
"at": "2026-09-14T05:58:44.810694+00:00",
"source": "ok",
"action": "Accepted the proposed shared engine/census/context follow-up after group 8 NO-GO. Task 2.19 covers the confirmed missing Gruntfile and honest census-completeness repair; runtime and independent qualification gates remain unchanged."
} }
], ],
"implementation_verification": { "implementation_verification": {
@@ -53,6 +58,12 @@
"status": "VERIFIED", "status": "VERIFIED",
"review": "APPROVED", "review": "APPROVED",
"record": "frontend-retention-verification.md" "record": "frontend-retention-verification.md"
},
"2.19": {
"status": "VERIFIED",
"review": "APPROVED",
"independent": false,
"record": "census-repair-verification.md"
} }
} }
} }
+9
View File
@@ -342,6 +342,15 @@
- _Approval:_ maintainer "approved", 2026-09-13; prerequisite for pre-push-safety-net task 3.4. - _Approval:_ maintainer "approved", 2026-09-13; prerequisite for pre-push-safety-net task 3.4.
- _Verified:_ independent review APPROVED; 1,141 tests passed, nine skipped, static checks clean; real PHP/JavaScript test nodes retained and disabled-policy control reproduces omission. See `frontend-retention-verification.md`. - _Verified:_ independent review APPROVED; 1,141 tests passed, nine skipped, static checks clean; real PHP/JavaScript test nodes retained and disabled-policy control reproduces omission. See `frontend-retention-verification.md`.
- [x] 2.19 Repair the measured Node input census and preserve honest completeness diagnostics
- Retain the declared original `Gruntfile.js` through the versioned frontend adaptation; bind the explicit policy to runtime/cache identity.
- Preserve observed file/method counts and missing names when census is incomplete; missing, empty or malformed census must not complete questions or become a false clean result.
- Verify the exact 44-file NodeGoat input, native PHP/JavaScript partition smoke, and ordinary scan/hook regressions. Do not infer latency or capability admission from retention.
- _Boundary: existing frontend adaptation, partition census and scan completeness integration._
- _Requirements: contributor-scan 4.3, 5.6, 6.1; pre-push 2.3, 4.3, 8.2._
- _Approval: maintainer accepted contributor-scan remediation with "ok" on 2026-09-14; no rollout approval._
- _Verified: 1321 tests passed, 9 skipped; real 44/44 Node census and packaged native PHP/JavaScript smoke. See `census-repair-verification.md`._
## Group 3 — Ship it ## Group 3 — Ship it
- [ ] 3.1 Contributor output - [ ] 3.1 Contributor output
+7 -1
View File
@@ -1,7 +1,7 @@
{ {
"feature_name": "pre-push-safety-net", "feature_name": "pre-push-safety-net",
"created_at": "2026-09-12T15:35:00Z", "created_at": "2026-09-12T15:35:00Z",
"updated_at": "2026-09-13T17:38:57.553583+00:00", "updated_at": "2026-09-14T06:06:32.633405+00:00",
"language": "en", "language": "en",
"phase": "implementation", "phase": "implementation",
"approvals": { "approvals": {
@@ -143,6 +143,12 @@
"feature_complete": false, "feature_complete": false,
"record": "validation-group-8.md", "record": "validation-group-8.md",
"capabilities_enabled": 0 "capabilities_enabled": 0
},
"owner_remediation": {
"task": "contributor-scan 2.19",
"status": "VERIFIED",
"record": "../contributor-scan/census-repair-verification.md",
"effect": "Node 44/44 input census restored; partial/missing census remains explicitly unresolved. Existing eligibility stale; runtime/context/independent qualification remain NO-GO."
} }
} }
} }
+9
View File
@@ -111,3 +111,12 @@ context/transitive evidence remains unresolved. Independent reviewed Node/PHP/Py
populations are missing; libpng arithmetic/bounds remains unsupported. No real capability populations are missing; libpng arithmetic/bounds remains unsupported. No real capability
is enabled. Engine/census/context prerequisites route to contributor-scan before another is enabled. Engine/census/context prerequisites route to contributor-scan before another
frozen qualification attempt. See `pre-push-safety-net/validation-group-8.md`. frozen qualification attempt. See `pre-push-safety-net/validation-group-8.md`.
On 2026-09-14, contributor-scan task 2.19 repaired the confirmed Gruntfile frontend
omission (real Node census 43/44 → 44/44) and the misleading empty-graph diagnostic.
Incomplete or unavailable census retains explicit coverage gaps and cannot complete
questions. Native PHP/JavaScript partition smoke and 1321 tests pass. Runtime/core
identity changed, so prior eligibility is stale and remains disabled. This repairs one
shared prerequisite; pre-push 8.3, runtime amortization, VAmPI context/transitive
evidence and independent qualification remain open. No ranker retune or rollout GO.
File diff suppressed because one or more lines are too long
+9
View File
@@ -34,3 +34,12 @@ the upstream version string alone cannot distinguish these graphs.
After building the image, run `ops/smoke_partitions.py` as described in `ops/README.md`. After building the image, run `ops/smoke_partitions.py` as described in `ops/README.md`.
Its off control uses `OUSAST_SMOKE_INCLUDE_TESTS=0` and must fail the test-retention Its off control uses `OUSAST_SMOKE_INCLUDE_TESTS=0` and must fail the test-retention
assertion. First-party test retention does not imply security capability admission. assertion. First-party test retention does not imply security capability admission.
The 2026-09-14 retention correction adds `OUSAST_INCLUDE_BUILD_CONFIGS=1` for
`Gruntfile.js`, which Joern otherwise filters even after test retention is enabled.
`JoernBackend(include_build_configs=True)` sets this explicit default; false restores
that upstream exclusion. Other configuration/minification filters are unchanged.
The option is part of graph/runtime identity. Source paths and bytes are preserved,
and the configuration is parsed, never executed. Real NodeGoat census and the packaged
PHP/JavaScript partition smoke verify this input-retention correction separately from
hook latency, actionable detection and capability admission.
+9 -1
View File
@@ -97,6 +97,14 @@ const IGNORE_FILE_PATTERN = INCLUDE_TESTS
"lazy val isIgnoredTest = IgnoredTestsRegex.exists(_.matches(filePath))", "lazy val isIgnoredTest = IgnoredTestsRegex.exists(_.matches(filePath))",
'lazy val isIgnoredTest = !sys.env.get("OUSAST_INCLUDE_TESTS").contains("1") && IgnoredTestsRegex.exists(_.matches(filePath))', 'lazy val isIgnoredTest = !sys.env.get("OUSAST_INCLUDE_TESTS").contains("1") && IgnoredTestsRegex.exists(_.matches(filePath))',
) )
# Keep the original Gruntfile path and bytes. Other default filters remain intact.
replace_once(
scala,
"lazy val isIgnored = IgnoredFilesRegex.exists(_.matches(filePath))",
"lazy val isIgnored = IgnoredFilesRegex.exists(_.matches(filePath)) && "
'!(sys.env.get("OUSAST_INCLUDE_BUILD_CONFIGS").contains("1") && '
'Paths.get(filePath).getFileName.toString == "Gruntfile.js")',
)
# Upstream stages embedded TS standard-library assets; retain that build recipe, # Upstream stages embedded TS standard-library assets; retain that build recipe,
# selecting just the supported Linux x64 target instead of all release platforms. # selecting just the supported Linux x64 target instead of all release platforms.
script = astgen / "scripts/build-binaries.ts" script = astgen / "scripts/build-binaries.ts"
@@ -135,7 +143,7 @@ const IGNORE_FILE_PATTERN = INCLUDE_TESTS
manifest = { manifest = {
"revision": "ousast-frontend-retention-v1", "revision": "ousast-frontend-retention-v1",
"builder_sha256": digest(Path(__file__)), "builder_sha256": digest(Path(__file__)),
"policy": "OUSAST_INCLUDE_TESTS=1", "policy": "OUSAST_INCLUDE_TESTS=1; OUSAST_INCLUDE_BUILD_CONFIGS=1 retains Gruntfile.js",
"sources": PINS, "sources": PINS,
"original_jar_sha256": JAR_SHA256, "original_jar_sha256": JAR_SHA256,
"adapted_jar_sha256": digest(adapted), "adapted_jar_sha256": digest(adapted),
+3
View File
@@ -37,6 +37,7 @@ def main() -> None:
} }
""") """)
sources = { sources = {
"Gruntfile.js": "function build_config_marker() { return 42; }\n",
"src/api.php": "<?php\nfunction php_handler($req) { eval($req); }\n", "src/api.php": "<?php\nfunction php_handler($req) { eval($req); }\n",
"server/api.js": "function js_handler(req) { eval(req.query.code); }\n", "server/api.js": "function js_handler(req) { eval(req.query.code); }\n",
"browser/client.js": "function browser_marker() { return 42; }\n", "browser/client.js": "function browser_marker() { return 42; }\n",
@@ -88,6 +89,8 @@ def main() -> None:
assert any(str(path).endswith("server/api.js") for path in files), witness assert any(str(path).endswith("server/api.js") for path in files), witness
assert any(str(path).endswith("tests/probe.js") for path in files), witness assert any(str(path).endswith("tests/probe.js") for path in files), witness
assert "js_handler" in methods and "js_test_marker" in methods, witness assert "js_handler" in methods and "js_test_marker" in methods, witness
assert any(str(path).endswith("Gruntfile.js") for path in files), witness
assert "build_config_marker" in methods, witness
for path, method in suffix_tests.items(): for path, method in suffix_tests.items():
assert any(str(filename).endswith(path) for filename in files), witness assert any(str(filename).endswith(path) for filename in files), witness
assert method in methods, witness assert method in methods, witness
+13 -2
View File
@@ -267,7 +267,11 @@ class NullBackend:
def engine_runtime_identity( def engine_runtime_identity(
*, include_tests: bool = True, language: str | None = None, execution_budget: ExecutionBudget | None = None *,
include_tests: bool = True,
include_build_configs: bool = True,
language: str | None = None,
execution_budget: ExecutionBudget | None = None,
) -> str: ) -> str:
"""Installed frontend/adaptation bytes, also used by comparison eligibility provenance.""" """Installed frontend/adaptation bytes, also used by comparison eligibility provenance."""
from .artifact import digest_value, graph_bytes from .artifact import digest_value, graph_bytes
@@ -296,6 +300,7 @@ def engine_runtime_identity(
"version": version, "version": version,
"files": installation, "files": installation,
"include_tests": include_tests, "include_tests": include_tests,
"include_build_configs": include_build_configs,
"java_options": os.environ.get("JAVA_TOOL_OPTIONS", ""), "java_options": os.environ.get("JAVA_TOOL_OPTIONS", ""),
} }
) )
@@ -307,6 +312,7 @@ class JoernBackend:
runner: Runner | None = None runner: Runner | None = None
include_tests: bool = True # Explicit input policy for the versioned frontend adaptation. include_tests: bool = True # Explicit input policy for the versioned frontend adaptation.
include_build_configs: bool = True # Explicit retention of supported build configuration source.
build_timeout: int = BUILD_TIMEOUT_SECONDS build_timeout: int = BUILD_TIMEOUT_SECONDS
query_timeout: int = field(default_factory=lambda: _configured_timeout()) query_timeout: int = field(default_factory=lambda: _configured_timeout())
heap_mb: int = 0 # 0 means read the environment, then fall back to CPG_HEAP_MB heap_mb: int = 0 # 0 means read the environment, then fall back to CPG_HEAP_MB
@@ -380,10 +386,14 @@ class JoernBackend:
(("implementation", code),), (("implementation", code),),
( (
("include_tests", str(self.include_tests)), ("include_tests", str(self.include_tests)),
("include_build_configs", str(self.include_build_configs)),
( (
"installation", "installation",
engine_runtime_identity( engine_runtime_identity(
include_tests=self.include_tests, language=language, execution_budget=execution_budget or self.execution_budget include_tests=self.include_tests,
include_build_configs=self.include_build_configs,
language=language,
execution_budget=execution_budget or self.execution_budget,
), ),
), ),
("java_options", os.environ.get("JAVA_TOOL_OPTIONS", "default") or "default"), ("java_options", os.environ.get("JAVA_TOOL_OPTIONS", "default") or "default"),
@@ -1189,6 +1199,7 @@ class JoernBackend:
""" """
env = dict(os.environ) env = dict(os.environ)
env["OUSAST_INCLUDE_TESTS"] = "1" if self.include_tests else "0" env["OUSAST_INCLUDE_TESTS"] = "1" if self.include_tests else "0"
env["OUSAST_INCLUDE_BUILD_CONFIGS"] = "1" if self.include_build_configs else "0"
env["JAVA_TOOL_OPTIONS"] = f"{env.get('JAVA_TOOL_OPTIONS', '').strip()} -Xmx{self._heap_mb()}m".strip() env["JAVA_TOOL_OPTIONS"] = f"{env.get('JAVA_TOOL_OPTIONS', '').strip()} -Xmx{self._heap_mb()}m".strip()
return env return env
+11 -7
View File
@@ -106,6 +106,9 @@ class PartitionGraph:
raw = batch(kind, selected) if callable(batch) else {rid: graph.run(kind, params) for rid, params in selected.items()} raw = batch(kind, selected) if callable(batch) else {rid: graph.run(kind, params) for rid, params in selected.items()}
if not isinstance(raw, Mapping): if not isinstance(raw, Mapping):
continue continue
raw = dict(raw)
if not isinstance(raw.get("__census__"), list):
raw["__census__"] = []
for rid, rows in raw.items(): for rid, rows in raw.items():
if rid in selected and isinstance(rows, list): if rid in selected and isinstance(rows, list):
answers[rid] = self._normalize(rows) answers[rid] = self._normalize(rows)
@@ -117,17 +120,18 @@ class PartitionGraph:
observed = {p.removeprefix("./") for n in names if isinstance(n, list) for p in n if isinstance(p, str)} observed = {p.removeprefix("./") for n in names if isinstance(n, list) for p in n if isinstance(p, str)}
if not named or not expected <= observed: if not named or not expected <= observed:
self.diagnostics.append("partition_file_census_incomplete" if named else "partition_file_census_unavailable") self.diagnostics.append("partition_file_census_incomplete" if named else "partition_file_census_unavailable")
reason = "partition_file_census_incomplete" if named else "partition_file_census_unavailable"
rows = [ rows = [
{ dict(r, census_failure=reason, missing_file_names=sorted(expected - observed) if named else [])
"methods": 0, for r in normalized
"files": 0, if isinstance(r, Mapping)
"shards": max((int(str(r.get("shards", 1))) for r in rows if isinstance(r, Mapping)), default=1), ] or [{"census_failure": reason, "missing_file_names": []}]
}
]
# Preserve any individual empty or sharded graph's degradation. # Preserve any individual empty or sharded graph's degradation.
previous = answers.get(rid, []) previous = answers.get(rid, [])
if not previous or any( if not previous or any(
isinstance(r, Mapping) and (int(str(r.get("methods", 0))) == 0 or int(str(r.get("shards", 1))) > 1) for r in rows isinstance(r, Mapping)
and (r.get("census_failure") or int(str(r.get("methods", 0))) == 0 or int(str(r.get("shards", 1))) > 1)
for r in rows
): ):
answers[rid] = rows answers[rid] = rows
return answers return answers
+20 -4
View File
@@ -355,6 +355,8 @@ def _scan_repository_impl(
in { in {
"files_unparsed", "files_unparsed",
"cpg_empty", "cpg_empty",
"partition_file_census_incomplete",
"partition_file_census_unavailable",
"cpg_sharded", "cpg_sharded",
"cross_partition_semantics_unresolved", "cross_partition_semantics_unresolved",
"vendor_semantics_unresolved", "vendor_semantics_unresolved",
@@ -410,8 +412,14 @@ def _scan_repository_impl(
if census and not census_reported: if census and not census_reported:
entry = census[0] if isinstance(census[0], Mapping) else {} entry = census[0] if isinstance(census[0], Mapping) else {}
methods = int(str(entry.get("methods", "0")) or 0) methods = int(str(entry.get("methods", "0")) or 0)
if methods == 0: if methods == 0 or entry.get("census_failure"):
degradations.append({"stage": "model", "reason": "cpg_empty", "files": int(str(entry.get("files", "0")) or 0)}) degradations.append(
{
"stage": "model",
"reason": "cpg_empty" if "methods" in entry and methods == 0 else entry.get("census_failure", "cpg_empty"),
**{k: entry[k] for k in ("methods", "files", "missing_file_names") if k in entry},
}
)
# More than one graph means some files could only be built apart from the rest, and a # More than one graph means some files could only be built apart from the rest, and a
# flow whose source is in one shard and whose sink is in another does not exist for any # flow whose source is in one shard and whose sink is in another does not exist for any
# question we can ask. Reporting the split is the difference between a partial answer and # question we can ask. Reporting the split is the difference between a partial answer and
@@ -508,6 +516,8 @@ def _scan_repository_impl(
in { in {
"files_unparsed", "files_unparsed",
"cpg_empty", "cpg_empty",
"partition_file_census_incomplete",
"partition_file_census_unavailable",
"cpg_sharded", "cpg_sharded",
"cross_partition_semantics_unresolved", "cross_partition_semantics_unresolved",
"vendor_semantics_unresolved", "vendor_semantics_unresolved",
@@ -741,8 +751,14 @@ def _evidence_pass(
shards = int(str(entry.get("shards", "1"))) shards = int(str(entry.get("shards", "1")))
except ValueError: except ValueError:
methods, shards = 0, 1 methods, shards = 0, 1
if methods <= 0: if methods <= 0 or entry.get("census_failure"):
degradations.append({"stage": "model", "reason": "cpg_empty"}) degradations.append(
{
"stage": "model",
"reason": "cpg_empty" if "methods" in entry and methods <= 0 else entry.get("census_failure", "cpg_empty"),
**{k: entry[k] for k in ("methods", "files", "missing_file_names") if k in entry},
}
)
if shards > 1: if shards > 1:
degradations.append({"stage": "model", "reason": "cpg_sharded", "shards": shards}) degradations.append({"stage": "model", "reason": "cpg_sharded", "shards": shards})
by_rid = {rid: (region, spec) for rid, region, spec in work} by_rid = {rid: (region, spec) for rid, region, spec in work}
+6
View File
@@ -29,3 +29,9 @@ def test_builder_rejects_unrecognized_dependency_before_download(tmp_path):
builder.build(tmp_path) builder.build(tmp_path)
assert jar.read_bytes() == b"different dependency" assert jar.read_bytes() == b"different dependency"
assert not (tmp_path / "ousast-frontend-retention-v1").exists() assert not (tmp_path / "ousast-frontend-retention-v1").exists()
def test_build_config_retention_is_explicit_and_overrides_ambient_policy(monkeypatch):
monkeypatch.setenv("OUSAST_INCLUDE_BUILD_CONFIGS", "0")
assert JoernBackend()._jvm_env()["OUSAST_INCLUDE_BUILD_CONFIGS"] == "1"
assert JoernBackend(include_build_configs=False)._jvm_env()["OUSAST_INCLUDE_BUILD_CONFIGS"] == "0"
+14 -5
View File
@@ -1,5 +1,7 @@
from pathlib import Path from pathlib import Path
import pytest
from openultrasast.cpg.backend import CpgResult from openultrasast.cpg.backend import CpgResult
from openultrasast.model.regions import ScanRegion from openultrasast.model.regions import ScanRegion
from openultrasast.model.scan import ScanBudget, scan_repository from openultrasast.model.scan import ScanBudget, scan_repository
@@ -153,7 +155,8 @@ def test_unsupported_only_population_is_named(tmp_path):
assert "frontend_unsupported" in result.scope.unresolved_boundaries assert "frontend_unsupported" in result.scope.unresolved_boundaries
def test_partial_partition_census_never_completes_questions(tmp_path): @pytest.mark.parametrize("census", [[{"methods": 2, "files": 1}], [], ["invalid"], None])
def test_partial_partition_census_never_completes_questions(tmp_path, census):
for name in ("a.py", "b.py"): for name in ("a.py", "b.py"):
(tmp_path / name).write_text("print('readable')") (tmp_path / name).write_text("print('readable')")
@@ -162,13 +165,14 @@ def test_partial_partition_census_never_completes_questions(tmp_path):
return CpgResult( return CpgResult(
Path("graph"), Path("graph"),
lambda q, p: [], lambda q, p: [],
run_batch=lambda q, reqs: {**{rid: [] for rid in reqs}, "__census__": [{"methods": 2, "files": 1}]}, run_batch=lambda q, reqs: {**{rid: [] for rid in reqs}, **({"__census__": census} if census is not None else {})},
) )
region = ScanRegion(path="a.py", function="f", language="python", families=("injection",), rank=1.0, source="entry_point") region = ScanRegion(path="a.py", function="f", language="python", families=("injection",), rank=1.0, source="entry_point")
result = scan_repository(tmp_path, [region], backend=Backend(), budget=ScanBudget(tiering=False)) result = scan_repository(tmp_path, [region], backend=Backend(), budget=ScanBudget(tiering=False))
assert result.question_outcomes[0].status == "unresolved" assert result.question_outcomes[0].status == "unresolved"
assert "cpg_empty" in result.scope.unresolved_boundaries assert "partition_file_census_unavailable" in result.scope.unresolved_boundaries
assert "cpg_empty" not in result.scope.unresolved_boundaries
def test_projection_refuses_substituted_ancestor(tmp_path): def test_projection_refuses_substituted_ancestor(tmp_path):
@@ -286,7 +290,9 @@ def test_synthetic_census_node_cannot_mask_missing_source(tmp_path):
graph = build_partitions(tmp_path, build, None) graph = build_partitions(tmp_path, build, None)
try: try:
result = graph.run_batch("taint", {"q": {"file": "a.php"}}) result = graph.run_batch("taint", {"q": {"file": "a.php"}})
assert result["__census__"][0]["files"] == 0 assert result["__census__"][0]["files"] == 2
assert result["__census__"][0]["methods"] == 3
assert result["__census__"][0]["missing_file_names"] == ["b.php"]
assert "partition_file_census_incomplete" in graph.execution_diagnostics() assert "partition_file_census_incomplete" in graph.execution_diagnostics()
finally: finally:
graph.cleanup() graph.cleanup()
@@ -331,7 +337,10 @@ def test_unnamed_sharded_census_remains_explicitly_incomplete(tmp_path):
) )
try: try:
result = graph.run_batch("taint", {"q": {"file": "app.php"}}) result = graph.run_batch("taint", {"q": {"file": "app.php"}})
assert result["__census__"] == [{"methods": 0, "files": 0, "shards": 2}] assert result["__census__"][0]["methods"] == 20
assert result["__census__"][0]["files"] == 20
assert result["__census__"][0]["shards"] == 2
assert result["__census__"][0]["census_failure"] == "partition_file_census_unavailable"
assert "partition_file_census_unavailable" in graph.execution_diagnostics() assert "partition_file_census_unavailable" in graph.execution_diagnostics()
finally: finally:
graph.cleanup() graph.cleanup()
+18 -2
View File
@@ -463,7 +463,15 @@ def test_tier_zero_pairs_are_not_asked_and_nothing_else_is_skipped(tmp_path: Pat
issued.append({"kind": kind, "count": len(requests)}) issued.append({"kind": kind, "count": len(requests)})
return {rid: [] for rid in requests} return {rid: [] for rid in requests}
return CpgResult(cpg_path=Path("cpg.bin"), run=lambda q, p: [], run_batch=batch) names = [str(p) for p in root.rglob("*.py")]
return CpgResult(
cpg_path=Path("cpg.bin"),
run=lambda q, p: [],
run_batch=lambda kind, requests: {
"__census__": [{"methods": len(names), "files": len(names), "file_names": names}],
**batch(kind, requests),
},
)
region = ScanRegion( region = ScanRegion(
path="a.py", function="run", language="python", families=("injection", "path", "deserialization"), rank=0.9, source="entry_point" path="a.py", function="run", language="python", families=("injection", "path", "deserialization"), rank=0.9, source="entry_point"
@@ -521,7 +529,15 @@ def test_the_budget_is_spent_by_evidence_when_asked(tmp_path: Path) -> None:
self.taint_files.append(sorted({str(p.get("file", "")) for p in requests.values()})) self.taint_files.append(sorted({str(p.get("file", "")) for p in requests.values()}))
return {rid: [] for rid in requests} return {rid: [] for rid in requests}
return CpgResult(cpg_path=Path("cpg.bin"), run=lambda q, p: [], run_batch=batch) names = [str(p) for p in root.rglob("*.py")]
return CpgResult(
cpg_path=Path("cpg.bin"),
run=lambda q, p: [],
run_batch=lambda kind, requests: {
"__census__": [{"methods": len(names), "files": len(names), "file_names": names}],
**batch(kind, requests),
},
)
top = ScanRegion(path="top.py", function="handle", language="python", families=("injection",), rank=1.0, source="entry_point") top = ScanRegion(path="top.py", function="handle", language="python", families=("injection",), rank=1.0, source="entry_point")
low = ScanRegion(path="low.py", function="run", language="python", families=("injection",), rank=0.3, source="entry_point") low = ScanRegion(path="low.py", function="run", language="python", families=("injection",), rank=0.3, source="entry_point")
+6 -1
View File
@@ -26,7 +26,12 @@ class Backend:
assert any(p.read_bytes() for p in root.rglob("*") if p.is_file()) assert any(p.read_bytes() for p in root.rglob("*") if p.is_file())
if self.fail: if self.fail:
return None return None
return SimpleNamespace(cpg_path=Path("test.cpg"), run_batch=self.batch, cleanup=lambda: None) names = [str(p) for p in root.rglob("*") if p.is_file()]
def batch(kind, requests):
return {"__census__": [{"methods": len(names), "files": len(names), "file_names": names}], **self.batch(kind, requests)}
return SimpleNamespace(cpg_path=Path("test.cpg"), run_batch=batch, cleanup=lambda: None)
def batch(self, kind, requests): def batch(self, kind, requests):
if any(p.get("evidenceOnly") == "true" for p in requests.values()): if any(p.get("evidenceOnly") == "true" for p in requests.values()):
+8 -1
View File
@@ -117,7 +117,14 @@ def test_targeted_base_uses_real_driver_same_deadline(tmp_path):
assert (root / "a.js").read_bytes() assert (root / "a.js").read_bytes()
seen.append(execution_budget) seen.append(execution_budget)
rows = json.loads(scan(line=5, sanitized=True).question_outcomes[0].raw_rows_json) rows = json.loads(scan(line=5, sanitized=True).question_outcomes[0].raw_rows_json)
return CpgResult(Path("graph"), lambda kind, params: rows) return CpgResult(
Path("graph"),
lambda kind, params: rows,
run_batch=lambda kind, requests: {
**{rid: rows for rid in requests},
"__census__": [{"methods": 1, "files": 1, "file_names": [str(root / "a.js")]}],
},
)
region = ScanRegion("a.js", "handler", "javascript", ("injection",), 1.0, "entry_point") region = ScanRegion("a.js", "handler", "javascript", ("injection",), 1.0, "entry_point")
budget = ExecutionBudget(time.monotonic() + 30, 1.0) budget = ExecutionBudget(time.monotonic() + 30, 1.0)