mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-16 03:49:26 +00:00
fix(contributor-scan): retain declared Gruntfile and report census gaps honestly
This commit is contained in:
@@ -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).
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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.
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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()):
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user