@dailephd/my-dev-kit-lab 0.4.4 → 0.4.6
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.
- package/CHANGELOG.md +484 -0
- package/README.md +39 -3
- package/dist/scripts/cli.js +9 -0
- package/dist/src/audits/core/typescriptJavaScriptAnalyzer.js +30 -12
- package/dist/src/cli/help.js +300 -0
- package/dist/src/cli/index.js +2 -0
- package/dist/src/cli/runLabCli.js +229 -0
- package/dist/src/commands/runAuditCommand.js +132 -0
- package/dist/src/commands/runControlledExperimentCommand.js +7 -3
- package/dist/{scripts/experiments/describeExperiment.js → src/commands/runExperimentDescribeCommand.js} +14 -10
- package/dist/{scripts/experiments/listExperiments.js → src/commands/runExperimentListCommand.js} +3 -6
- package/dist/{scripts/experiments/runExperiment.js → src/commands/runExperimentRunCommand.js} +80 -22
- package/dist/src/commands/runFinalDemoCommand.js +2 -3
- package/dist/src/commands/runSecurityValidationCommand.js +271 -0
- package/dist/src/evaluation/ecosystemFixtures/index.js +3 -0
- package/dist/src/evaluation/ecosystemFixtures/loadContextIntegrityFixture.js +102 -0
- package/dist/src/evaluation/ecosystemFixtures/manifestTypes.js +1 -0
- package/dist/src/evaluation/ecosystemFixtures/validateManifest.js +82 -0
- package/dist/src/evaluation/ecosystemFixtures/verifyFixtureHashes.js +45 -0
- package/dist/src/evaluation/stageContextMetrics/calculateAllocationMetrics.js +169 -0
- package/dist/src/evaluation/stageContextMetrics/calculateConditionCoverageMetrics.js +74 -0
- package/dist/src/evaluation/stageContextMetrics/calculateProducerConditionAgreement.js +113 -0
- package/dist/src/evaluation/stageContextMetrics/calculateReadinessAgreement.js +60 -0
- package/dist/src/evaluation/stageContextMetrics/calculateRunIntegrityAgreement.js +309 -0
- package/dist/src/evaluation/stageContextMetrics/calculateSupplementalRawAgreement.js +12 -2
- package/dist/src/evaluation/stageContextMetrics/calculateTruncationClassification.js +100 -0
- package/dist/src/evaluation/stageContextMetrics/evaluateProducerReadinessBridge.js +76 -5
- package/dist/src/evaluation/stageContextMetrics/index.js +4 -0
- package/dist/src/evaluation/stageContextMetrics/runIntegrityAgreementTypes.js +1 -0
- package/dist/src/evaluation/stageContextSelectors/contextArtifactConsistency.js +7 -0
- package/dist/src/evaluation/stageContextSelectors/index.js +1 -0
- package/dist/src/evaluation/stageContextSelectors/orchestratorRunIntegritySelectors.js +64 -0
- package/dist/src/evaluation/upstreamArtifacts/index.js +2 -0
- package/dist/src/evaluation/upstreamArtifacts/orchestratorRunIntegrityV1.js +33 -0
- package/dist/src/evaluation/upstreamArtifacts/readOrchestratorRunIntegrityV1.js +9 -0
- package/dist/src/evaluation/upstreamArtifacts/validateMyDevKitContextCapsuleV1.js +59 -2
- package/dist/src/evaluation/upstreamArtifacts/validateMyDevKitRetrievalAuditRecordV1.js +6 -1
- package/dist/src/evaluation/upstreamArtifacts/validateOrchestratorRunIntegrityV1.js +177 -0
- package/dist/src/report/experiments/buildContextIntegrityReport.js +204 -0
- package/dist/src/report/experiments/contextIntegrityReportModel.js +1 -0
- package/dist/src/report/experiments/index.js +5 -0
- package/dist/src/report/experiments/renderContextIntegrityHtml.js +98 -0
- package/dist/src/report/experiments/renderContextIntegrityJsonReport.js +3 -0
- package/dist/src/report/experiments/renderContextIntegrityText.js +111 -0
- package/dist/src/runtime/index.js +3 -0
- package/dist/src/runtime/labExecutionContext.js +22 -0
- package/dist/src/runtime/packageResource.js +22 -0
- package/dist/src/runtime/packageRoot.js +37 -0
- package/dist/src/securityValidation/packageChecks/requiredPackageContents.js +46 -0
- package/dist/src/securityValidation/packageChecks/runPackageChecks.js +11 -5
- package/docs/METRICS.md +83 -0
- package/package.json +10 -8
- package/dist/scripts/audits/runAudit.js +0 -135
- package/dist/scripts/build-gallery.js +0 -3
- package/dist/scripts/capture-demo-report.js +0 -3
- package/dist/scripts/evaluate-token-savings.js +0 -2
- package/dist/scripts/generate-experiment-plots.js +0 -3
- package/dist/scripts/generate-prompt-variants.js +0 -2
- package/dist/scripts/render-experiment-report.js +0 -2
- package/dist/scripts/run-agent-prompt.js +0 -2
- package/dist/scripts/run-controlled-experiment.js +0 -2
- package/dist/scripts/run-lab-demo.js +0 -5
- package/dist/scripts/run-visualization-demos.js +0 -3
- package/dist/scripts/security/resolveToolRoot.js +0 -16
- package/dist/scripts/security/runCodeql.js +0 -58
- package/dist/scripts/security/runDependencyChecks.js +0 -58
- package/dist/scripts/security/runFuzzSmoke.js +0 -29
- package/dist/scripts/security/runPackageChecks.js +0 -57
- package/dist/scripts/security/runSemgrep.js +0 -64
- package/dist/scripts/security/validate.js +0 -266
- package/dist/scripts/verify-benchmarks.js +0 -202
- package/examples/demo-report-input.json +0 -78
- package/examples/lab-demo-cases.json +0 -35
- package/examples/real-agent-campaign-cases.json +0 -118
- package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-a/src/debug/res/xml/ambiguous_backup_rules.xml +0 -4
- package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-a/src/main/res/xml/ambiguous_backup_rules.xml +0 -4
- package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-a/src/main/res/xml/backup_rules.xml +0 -5
- package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-a/src/main/res/xml/broad_backup_rules.xml +0 -4
- package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-a/src/main/res/xml/data_extraction_rules.xml +0 -10
- package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-a/src/main/res/xml/unsupported_domain_rules.xml +0 -4
- package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-b/src/main/res/xml/malformed_backup_rules.xml +0 -4
- package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-b/src/main/res/xml/unsupported_root.xml +0 -4
- package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/debug/res/xml/ambiguous_config.xml +0 -4
- package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/main/res/xml/ambiguous_config.xml +0 -4
- package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/main/res/xml/base_cleartext_false.xml +0 -8
- package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/main/res/xml/broad_and_duplicate_domains.xml +0 -8
- package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/main/res/xml/empty_domain.xml +0 -6
- package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/main/res/xml/malformed_pin.xml +0 -11
- package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/main/res/xml/network_security_config.xml +0 -26
- package/tests/fixtures/android/advanced-security-fixtures/network-security/module-b/src/main/res/xml/malformed_network_security_config.xml +0 -6
- package/tests/fixtures/android/advanced-security-fixtures/network-security/module-b/src/main/res/xml/unsupported_root.xml +0 -4
- package/tests/fixtures/android/advanced-security-fixtures/release-configuration/groovy-release-debuggable-dynamic.gradle +0 -7
- package/tests/fixtures/android/advanced-security-fixtures/release-configuration/groovy-release-debuggable-false.gradle +0 -9
- package/tests/fixtures/android/advanced-security-fixtures/release-configuration/groovy-release-debuggable-true.gradle +0 -13
- package/tests/fixtures/android/advanced-security-fixtures/release-configuration/kotlin-release-debuggable-false.gradle.kts +0 -7
- package/tests/fixtures/android/advanced-security-fixtures/release-configuration/kotlin-release-debuggable-true.gradle.kts +0 -10
- package/tests/fixtures/android/advanced-security-fixtures/release-configuration/missing-release-build-type.gradle.kts +0 -7
- package/tests/fixtures/android/advanced-security-fixtures/resource-resolution/module-a/src/debug/res/xml/network_security_config.xml +0 -8
- package/tests/fixtures/android/advanced-security-fixtures/resource-resolution/module-a/src/main/res/xml/backup_rules.xml +0 -4
- package/tests/fixtures/android/advanced-security-fixtures/resource-resolution/module-a/src/main/res/xml/network_security_config.xml +0 -8
- package/tests/fixtures/android/advanced-security-fixtures/resource-resolution/module-b/src/main/res/xml/malformed_network_security_config.xml +0 -7
- package/tests/fixtures/android/advanced-security-fixtures/secret-candidates/SampleActivity.java +0 -8
- package/tests/fixtures/android/advanced-security-fixtures/secret-candidates/SampleActivity.kt +0 -9
- package/tests/fixtures/android/advanced-security-fixtures/secret-candidates/build.gradle +0 -11
- package/tests/fixtures/android/advanced-security-fixtures/secret-candidates/build.gradle.kts +0 -11
- package/tests/fixtures/android/advanced-security-fixtures/secret-candidates/google-services.json +0 -16
- package/tests/fixtures/android/advanced-security-fixtures/secret-candidates/local.properties +0 -3
- package/tests/fixtures/android/advanced-security-fixtures/secret-candidates-bounds/module/binary.properties +0 -0
- package/tests/fixtures/android/advanced-security-fixtures/secret-candidates-bounds/module/oversized.json +0 -1
- package/tests/fixtures/android/advanced-security-fixtures/signing-configuration/app/fake-release-keystore.jks +0 -1
- package/tests/fixtures/android/advanced-security-fixtures/signing-configuration/build/generated-fake.keystore +0 -1
- package/tests/fixtures/android/compose-app/app/build.gradle.kts +0 -21
- package/tests/fixtures/android/compose-app/app/src/main/AndroidManifest.xml +0 -22
- package/tests/fixtures/android/compose-app/app/src/main/java/com/example/composeapp/MainActivity.kt +0 -11
- package/tests/fixtures/android/compose-app/build.gradle.kts +0 -4
- package/tests/fixtures/android/compose-app/gradle/wrapper/gradle-wrapper.properties +0 -4
- package/tests/fixtures/android/compose-app/settings.gradle.kts +0 -2
- package/tests/fixtures/android/gradle-metadata-fixtures/catalog-only-plugin/build.gradle.kts +0 -3
- package/tests/fixtures/android/gradle-metadata-fixtures/catalog-only-plugin/gradle/libs.versions.toml +0 -5
- package/tests/fixtures/android/gradle-metadata-fixtures/comment-safety.gradle.kts +0 -21
- package/tests/fixtures/android/gradle-metadata-fixtures/dynamic-values.gradle.kts +0 -17
- package/tests/fixtures/android/gradle-metadata-fixtures/groovy-literal.gradle +0 -23
- package/tests/fixtures/android/gradle-metadata-fixtures/wrapper-malformed.properties +0 -4
- package/tests/fixtures/android/gradle-metadata-fixtures/wrapper-missing-url.properties +0 -3
- package/tests/fixtures/android/gradle-metadata-fixtures/wrapper-with-checksum.properties +0 -5
- package/tests/fixtures/android/java-app/app/build.gradle +0 -16
- package/tests/fixtures/android/java-app/app/src/main/AndroidManifest.xml +0 -19
- package/tests/fixtures/android/java-app/app/src/main/java/com/example/javaapp/MainActivity.java +0 -4
- package/tests/fixtures/android/java-app/build.gradle +0 -3
- package/tests/fixtures/android/java-app/gradle/wrapper/gradle-wrapper.properties +0 -4
- package/tests/fixtures/android/java-app/settings.gradle +0 -2
- package/tests/fixtures/android/library/build.gradle.kts +0 -3
- package/tests/fixtures/android/library/gradle/wrapper/gradle-wrapper.properties +0 -4
- package/tests/fixtures/android/library/mylibrary/build.gradle.kts +0 -13
- package/tests/fixtures/android/library/mylibrary/src/main/AndroidManifest.xml +0 -2
- package/tests/fixtures/android/library/settings.gradle.kts +0 -2
- package/tests/fixtures/android/manifest-audit-fixtures/incomplete-intent-filter.xml +0 -14
- package/tests/fixtures/android/manifest-audit-fixtures/malformed-exported.xml +0 -10
- package/tests/fixtures/android/manifest-audit-fixtures/malformed.xml +0 -5
- package/tests/fixtures/android/manifest-audit-fixtures/missing-application.xml +0 -7
- package/tests/fixtures/android/manifest-audit-fixtures/missing-component-name.xml +0 -11
- package/tests/fixtures/android/manifest-audit-fixtures/permission-coverage.xml +0 -29
- package/tests/fixtures/android/manifest-audit-fixtures/risky-components.xml +0 -62
- package/tests/fixtures/android/manifest-audit-fixtures/risky-deep-links.xml +0 -45
- package/tests/fixtures/android/manifest-audit-fixtures/unresolved-placeholder.xml +0 -14
- package/tests/fixtures/android/manifest-audit-fixtures/unusual-namespace-prefix.xml +0 -11
- package/tests/fixtures/android/multi-module/app/build.gradle.kts +0 -22
- package/tests/fixtures/android/multi-module/app/src/androidTest/java/com/example/multimodule/app/ExampleInstrumentedTest.kt +0 -3
- package/tests/fixtures/android/multi-module/app/src/main/AndroidManifest.xml +0 -11
- package/tests/fixtures/android/multi-module/app/src/test/java/com/example/multimodule/app/ExampleUnitTest.kt +0 -3
- package/tests/fixtures/android/multi-module/build.gradle.kts +0 -5
- package/tests/fixtures/android/multi-module/core/build.gradle.kts +0 -14
- package/tests/fixtures/android/multi-module/core/src/main/AndroidManifest.xml +0 -2
- package/tests/fixtures/android/multi-module/feature-login/build.gradle.kts +0 -18
- package/tests/fixtures/android/multi-module/feature-login/src/main/AndroidManifest.xml +0 -2
- package/tests/fixtures/android/multi-module/gradle/libs.versions.toml +0 -8
- package/tests/fixtures/android/multi-module/gradle/wrapper/gradle-wrapper.properties +0 -4
- package/tests/fixtures/android/multi-module/settings.gradle.kts +0 -4
- package/tests/fixtures/android/non-android-gradle/build.gradle.kts +0 -3
- package/tests/fixtures/android/non-android-gradle/core/build.gradle.kts +0 -3
- package/tests/fixtures/android/non-android-gradle/core/src/main/java/com/example/core/Core.java +0 -4
- package/tests/fixtures/android/non-android-gradle/gradle/wrapper/gradle-wrapper.properties +0 -4
- package/tests/fixtures/android/non-android-gradle/settings.gradle.kts +0 -2
- package/tests/fixtures/android/partial/app/build.gradle.kts +0 -7
- package/tests/fixtures/android/partial/settings.gradle.kts +0 -2
- package/tests/fixtures/android/xml-view-app/app/build.gradle.kts +0 -17
- package/tests/fixtures/android/xml-view-app/app/src/main/AndroidManifest.xml +0 -31
- package/tests/fixtures/android/xml-view-app/app/src/main/kotlin/com/example/xmlviewapp/MainActivity.kt +0 -7
- package/tests/fixtures/android/xml-view-app/app/src/main/res/layout/activity_main.xml +0 -4
- package/tests/fixtures/android/xml-view-app/build.gradle.kts +0 -4
- package/tests/fixtures/android/xml-view-app/gradle/wrapper/gradle-wrapper.properties +0 -4
- package/tests/fixtures/android/xml-view-app/settings.gradle.kts +0 -2
- package/tests/fixtures/audits/mixed-language/mixed-docs-claims/README.md +0 -16
- package/tests/fixtures/audits/mixed-language/mixed-docs-claims/build.gradle.kts +0 -4
- package/tests/fixtures/audits/mixed-language/mixed-docs-claims/src/main/java/com/example/Widget.java +0 -7
- package/tests/fixtures/audits/mixed-language/mixed-docs-claims/src/main/kotlin/com/example/Gadget.kt +0 -7
- package/tests/fixtures/audits/mixed-language/mixed-generated-vendor/README.md +0 -10
- package/tests/fixtures/audits/mixed-language/mixed-generated-vendor/build/Main.class +0 -1
- package/tests/fixtures/audits/mixed-language/mixed-generated-vendor/dist/index.js +0 -1
- package/tests/fixtures/audits/mixed-language/mixed-generated-vendor/generated/schema.ts +0 -1
- package/tests/fixtures/audits/mixed-language/mixed-generated-vendor/src/index.ts +0 -3
- package/tests/fixtures/audits/mixed-language/mixed-generated-vendor/vendor/lib.py +0 -2
- package/tests/fixtures/audits/mixed-language/mixed-java-kotlin/README.md +0 -10
- package/tests/fixtures/audits/mixed-language/mixed-java-kotlin/build.gradle.kts +0 -8
- package/tests/fixtures/audits/mixed-language/mixed-java-kotlin/settings.gradle.kts +0 -1
- package/tests/fixtures/audits/mixed-language/mixed-java-kotlin/src/main/java/com/example/FooService.java +0 -7
- package/tests/fixtures/audits/mixed-language/mixed-java-kotlin/src/main/kotlin/com/example/BarService.kt +0 -7
- package/tests/fixtures/audits/mixed-language/mixed-java-kotlin/src/test/java/com/example/FooServiceTest.java +0 -8
- package/tests/fixtures/audits/mixed-language/mixed-java-kotlin/src/test/kotlin/com/example/BarServiceTest.kt +0 -8
- package/tests/fixtures/audits/mixed-language/mixed-python-ts/README.md +0 -8
- package/tests/fixtures/audits/mixed-language/mixed-python-ts/src/app.py +0 -7
- package/tests/fixtures/audits/mixed-language/mixed-python-ts/src/index.ts +0 -3
- package/tests/fixtures/audits/mixed-language/mixed-python-ts/tests/test_app.py +0 -5
- package/tests/fixtures/audits/mixed-language/mixed-ts-js/README.md +0 -7
- package/tests/fixtures/audits/mixed-language/mixed-ts-js/src/helper.js +0 -5
- package/tests/fixtures/audits/mixed-language/mixed-ts-js/src/index.ts +0 -3
- package/tests/fixtures/audits/mixed-language/mixed-ts-js/tests/index.sample.ts +0 -5
- package/tests/fixtures/audits/mixed-language/mixed-ts-python-jvm/README.md +0 -10
- package/tests/fixtures/audits/mixed-language/mixed-ts-python-jvm/build.gradle.kts +0 -4
- package/tests/fixtures/audits/mixed-language/mixed-ts-python-jvm/pytests/test_app.py +0 -5
- package/tests/fixtures/audits/mixed-language/mixed-ts-python-jvm/src/app.py +0 -2
- package/tests/fixtures/audits/mixed-language/mixed-ts-python-jvm/src/helper.js +0 -5
- package/tests/fixtures/audits/mixed-language/mixed-ts-python-jvm/src/index.ts +0 -3
- package/tests/fixtures/audits/mixed-language/mixed-ts-python-jvm/src/main/java/com/example/Multiplier.java +0 -7
- package/tests/fixtures/audits/mixed-language/mixed-ts-python-jvm/src/main/kotlin/com/example/Divider.kt +0 -7
- package/tests/fixtures/fake-adversarial-cli.js +0 -152
- package/tests/fixtures/fake-my-dev-kit-cli.js +0 -83
- package/tests/fixtures/full-workflow-library/complete-v1.0.0.json +0 -24
- package/tests/fixtures/full-workflow-library/future-minor-v1.1.0-additive.json +0 -25
- package/tests/fixtures/stage-context-expectations/complete-v1.0.0.json +0 -230
- package/tests/fixtures/stage-context-expectations/future-minor-v1.1.0-additive.json +0 -232
- package/tests/fixtures/stage-context-expectations/invalid/malformed-json.txt +0 -1
- package/tests/fixtures/stage-context-expectations/invalid/non-object-root.json +0 -1
- package/tests/fixtures/stage-context-expectations/minimal-v1.0.0.json +0 -18
- package/tests/fixtures/upstream-artifacts/invalid/malformed-json.txt +0 -1
- package/tests/fixtures/upstream-artifacts/invalid/non-object-root.json +0 -1
- package/tests/fixtures/upstream-artifacts/my-dev-kit/1.10.2/context-capsule/complete-v1.0.0.json +0 -504
- package/tests/fixtures/upstream-artifacts/my-dev-kit/1.10.2/context-capsule/future-minor-v1.1.0-additive.json +0 -955
- package/tests/fixtures/upstream-artifacts/my-dev-kit/1.10.2/retrieval-audit-record/complete-v1.0.0.json +0 -190
- package/tests/fixtures/upstream-artifacts/my-dev-kit/1.10.2/retrieval-audit-record/future-minor-v1.1.0-additive.json +0 -327
- package/tests/fixtures/upstream-artifacts/my-dev-kit-1.10.2-identity/PROVENANCE.txt +0 -13
- package/tests/fixtures/upstream-artifacts/my-dev-kit-1.10.2-identity/context-capsule.json +0 -7874
- package/tests/fixtures/upstream-artifacts/my-dev-kit-1.10.2-identity/retrieval-audit-record.json +0 -1076
- package/tests/fixtures/upstream-artifacts/my-dev-kit-orchestrator/1.2.1/workflow-instruction-packet/complete-v1.0.0.json +0 -160
- package/tests/fixtures/upstream-artifacts/my-dev-kit-orchestrator/1.2.1/workflow-instruction-packet/future-minor-v1.1.0-additive.json +0 -186
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { runSecurityValidation } from "../securityValidation/validate/runSecurityValidation.js";
|
|
3
|
+
import { resolveValidationTarget, reportFilenamePrefix } from "../securityValidation/validate/resolveTarget.js";
|
|
4
|
+
import { buildSecurityReportFromSummary } from "../securityValidation/report/buildSecurityReport.js";
|
|
5
|
+
import { renderTextReport } from "../securityValidation/report/renderSecurityReport.js";
|
|
6
|
+
import { writeSecurityReportFiles } from "../securityValidation/report/writeSecurityReportFiles.js";
|
|
7
|
+
import { parseSecurityValidateArgs, normalizeSecurityValidateConfig, applyProfileDefaultChecksIfApplicable } from "../securityValidation/validate/cliOptions.js";
|
|
8
|
+
import { resolveAttackProfile } from "../securityValidation/attackScenarios/attackProfile.js";
|
|
9
|
+
import { findingsBreachFailOnThreshold } from "../securityValidation/validate/verdict.js";
|
|
10
|
+
import { validateAndroidTarget } from "../mobile/android/validate/validateAndroidTarget.js";
|
|
11
|
+
import { toAndroidReportModel } from "../mobile/android/report/model.js";
|
|
12
|
+
import { renderAndroidTextReport } from "../mobile/android/report/renderAndroidReport.js";
|
|
13
|
+
import { writeAndroidReportFiles } from "../mobile/android/report/writeAndroidReportFiles.js";
|
|
14
|
+
import { createLabExecutionContext } from "../runtime/index.js";
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// v0.4.6 Batch 4 -- reusable security-validation command owner.
|
|
17
|
+
//
|
|
18
|
+
// Extracted from scripts/security/validate.ts (formerly a top-level script
|
|
19
|
+
// with inline try/catch + process.exit()) so both the contributor npm script
|
|
20
|
+
// and the installed CLI router (src/cli/) call the same argument-parsing/
|
|
21
|
+
// target-resolution/report-writing/exit-mapping path. Mirrors the audit
|
|
22
|
+
// command extraction in src/commands/runAuditCommand.ts (Batch 3). Detector/
|
|
23
|
+
// verdict/severity policy, the Android validator, and the classic check
|
|
24
|
+
// pipeline are untouched -- this module only owns CLI wiring.
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
export const SECURITY_VALIDATE_USAGE = "Usage: my-dev-kit-lab security validate [--target <path>] [--out <dir>] [--report-prefix <name>] " +
|
|
27
|
+
"[--checks deps,package,static,cli-adversarial,fuzz,boundary,subprocess,secrets,network] " +
|
|
28
|
+
"[--profile node-cli-package|local-tool|npm-package|android] [--format text,json] [--fail-on blocker|high|medium|low] " +
|
|
29
|
+
"[--android-gradle-operations wrapper-version,tasks,assemble-debug,unit-test-debug,lint-debug] " +
|
|
30
|
+
"[--android-external-tools semgrep,osv,android-lint,dependency-check] " +
|
|
31
|
+
"[--android-external-network deny|allow-requested]\n" +
|
|
32
|
+
"\n" +
|
|
33
|
+
"Android profile:\n" +
|
|
34
|
+
" --profile android performs static, read-only Android project detection, manifest parsing,\n" +
|
|
35
|
+
" permission/exported-component/intent-filter/deep-link audits, static Gradle metadata\n" +
|
|
36
|
+
" extraction, and (as of v0.4.1 Batch 8) eleven internal advanced security checks (network\n" +
|
|
37
|
+
" security config, backup/release configuration, hardcoded secrets, signing configuration,\n" +
|
|
38
|
+
" WebView, FileProvider, sensitive storage/logging, clipboard, and Firebase/Google services).\n" +
|
|
39
|
+
" Gradle and external tools are never executed by default.\n" +
|
|
40
|
+
" --android-gradle-operations explicitly opts into running ONLY the listed allowlisted Gradle\n" +
|
|
41
|
+
" operations (wrapper-version, tasks, assemble-debug, unit-test-debug, lint-debug) via the\n" +
|
|
42
|
+
" project's own Gradle wrapper. Arbitrary Gradle tasks cannot be passed. This option is only\n" +
|
|
43
|
+
" valid together with --profile android.\n" +
|
|
44
|
+
" --android-external-tools explicitly opts into running ONLY the listed allowlisted optional\n" +
|
|
45
|
+
" external security tools (semgrep, osv, android-lint, dependency-check). No arbitrary tool\n" +
|
|
46
|
+
" names, commands, or arguments can be passed. This option is only valid together with\n" +
|
|
47
|
+
" --profile android.\n" +
|
|
48
|
+
" --android-external-network controls whether a requested tool may use network access.\n" +
|
|
49
|
+
" Defaults to deny (osv is skipped unless supported offline data is available; semgrep,\n" +
|
|
50
|
+
" android-lint, and dependency-check always run offline/no-update regardless of this flag).\n" +
|
|
51
|
+
" allow-requested authorizes network only for an explicitly requested tool whose Batch 7\n" +
|
|
52
|
+
" contract supports it (currently only osv). This option is only valid together with\n" +
|
|
53
|
+
" --profile android.\n" +
|
|
54
|
+
" Example: my-dev-kit-lab security validate --target \"<android-project-path>\" --profile android\n" +
|
|
55
|
+
" Example: my-dev-kit-lab security validate --target \"<android-project-path>\" --profile android --android-gradle-operations wrapper-version,tasks\n" +
|
|
56
|
+
" Example: my-dev-kit-lab security validate --target \"<android-project-path>\" --profile android --android-external-tools semgrep,android-lint\n" +
|
|
57
|
+
" Example: my-dev-kit-lab security validate --target \"<android-project-path>\" --profile android --android-external-tools osv --android-external-network allow-requested";
|
|
58
|
+
export async function runSecurityValidationCommandFromArgs(argv, options = {}) {
|
|
59
|
+
const context = options.context ?? createLabExecutionContext();
|
|
60
|
+
const toolRoot = context.packageRoot;
|
|
61
|
+
const defaultOutRoot = options.defaultOutRoot ?? toolRoot;
|
|
62
|
+
// Help deliberately wins over every other argument. It must remain a
|
|
63
|
+
// no-work path: no target/default resolution, option normalization, report
|
|
64
|
+
// directory creation, validation orchestration, or subprocess execution.
|
|
65
|
+
if (argv.includes("--help") || argv.includes("-h")) {
|
|
66
|
+
console.log(SECURITY_VALIDATE_USAGE);
|
|
67
|
+
return 0;
|
|
68
|
+
}
|
|
69
|
+
let args;
|
|
70
|
+
let config;
|
|
71
|
+
try {
|
|
72
|
+
args = parseSecurityValidateArgs(argv);
|
|
73
|
+
config = normalizeSecurityValidateConfig(args, defaultOutRoot);
|
|
74
|
+
// v0.2.2 Batch 5: --profile with no --checks uses that profile's declared
|
|
75
|
+
// default checks. No-op unless the user passed --profile without --checks
|
|
76
|
+
// (see applyProfileDefaultChecksIfApplicable's guard) -- the true no-flag
|
|
77
|
+
// case and any explicit --checks case are always unaffected. Skipped for
|
|
78
|
+
// "android": that profile does not use the classic --checks pipeline.
|
|
79
|
+
if (config.profile !== "android") {
|
|
80
|
+
config = applyProfileDefaultChecksIfApplicable(config, resolveAttackProfile(config.profile).defaultCheckIds);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
85
|
+
console.error(`\nERROR: ${msg}`);
|
|
86
|
+
console.error(SECURITY_VALIDATE_USAGE);
|
|
87
|
+
return 2;
|
|
88
|
+
}
|
|
89
|
+
// Resolve and validate target early so we can fail fast with a clean error.
|
|
90
|
+
let target;
|
|
91
|
+
try {
|
|
92
|
+
target = resolveValidationTarget(args.target, toolRoot);
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
96
|
+
console.error(`\nERROR: ${msg}`);
|
|
97
|
+
console.error(SECURITY_VALIDATE_USAGE);
|
|
98
|
+
return 1;
|
|
99
|
+
}
|
|
100
|
+
console.log("=".repeat(60));
|
|
101
|
+
console.log("my-dev-kit-lab security:validate");
|
|
102
|
+
console.log("=".repeat(60));
|
|
103
|
+
console.log(`Tool root : ${toolRoot}`);
|
|
104
|
+
if (!target.isSelf) {
|
|
105
|
+
console.log(`Target : ${target.targetRoot}`);
|
|
106
|
+
if (target.packageName)
|
|
107
|
+
console.log(`Package : ${target.packageName}${target.packageVersion ? `@${target.packageVersion}` : ""}`);
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
console.log(`Mode : self-validation`);
|
|
111
|
+
}
|
|
112
|
+
console.log(`Profile : ${config.profile}`);
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
// v0.4.0 Batch 5 -- Android profile path.
|
|
115
|
+
//
|
|
116
|
+
// A separate orchestrator (not the classic check-group/attack-scenario
|
|
117
|
+
// pipeline below) because the Android check/finding/status vocabulary
|
|
118
|
+
// established in Batches 1-4 does not fit SecurityCheckId/SecurityCheckResult.
|
|
119
|
+
// Same command, same --profile flag, same reports/security/ output root.
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
if (config.profile === "android") {
|
|
122
|
+
if (config.androidGradleOperationIds.length > 0) {
|
|
123
|
+
console.log(`Gradle ops : ${config.androidGradleOperationIds.join(", ")} (explicitly requested — will execute via the target's Gradle wrapper)`);
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
console.log(`Gradle ops : (none requested — static-only validation, zero Gradle process execution)`);
|
|
127
|
+
}
|
|
128
|
+
// v0.4.1 Batch 8 -- external tools remain explicit opt-in, mirroring Gradle
|
|
129
|
+
// operations exactly. "allow-requested" (the CLI-facing value) maps to the
|
|
130
|
+
// Batch 7 dispatcher's "allow-for-requested-tool" network policy value.
|
|
131
|
+
const externalNetworkPolicy = config.androidExternalNetworkPolicy === "allow-requested" ? "allow-for-requested-tool" : "deny";
|
|
132
|
+
if (config.androidExternalToolIds.length > 0) {
|
|
133
|
+
console.log(`External : ${config.androidExternalToolIds.join(", ")} (explicitly requested — network policy: ${config.androidExternalNetworkPolicy})`);
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
console.log(`External : (none requested — zero external-tool process execution)`);
|
|
137
|
+
}
|
|
138
|
+
console.log("");
|
|
139
|
+
const reportsDir = config.out;
|
|
140
|
+
let result;
|
|
141
|
+
try {
|
|
142
|
+
result = await validateAndroidTarget({
|
|
143
|
+
toolRoot,
|
|
144
|
+
targetPath: args.target,
|
|
145
|
+
requestedGradleOperationIds: config.androidGradleOperationIds,
|
|
146
|
+
requestedExternalToolIds: config.androidExternalToolIds,
|
|
147
|
+
externalNetworkPolicy: externalNetworkPolicy,
|
|
148
|
+
externalToolArtifactRoot: path.join(reportsDir, "external-tools")
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
catch (err) {
|
|
152
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
153
|
+
console.error(`\nERROR: Android validation failed to run: ${msg}`);
|
|
154
|
+
return 3;
|
|
155
|
+
}
|
|
156
|
+
const reportModel = toAndroidReportModel(result, {
|
|
157
|
+
profile: config.profile,
|
|
158
|
+
requestedGradleOperations: config.androidGradleOperationIds,
|
|
159
|
+
requestedExternalTools: config.androidExternalToolIds,
|
|
160
|
+
externalNetworkPolicy: config.androidExternalNetworkPolicy
|
|
161
|
+
});
|
|
162
|
+
const textReport = renderAndroidTextReport(reportModel);
|
|
163
|
+
const prefix = args.reportPrefix ??
|
|
164
|
+
reportFilenamePrefix({
|
|
165
|
+
isSelf: result.target.local.isSelf,
|
|
166
|
+
packageName: result.target.local.packageName,
|
|
167
|
+
packageVersion: result.target.local.packageVersion,
|
|
168
|
+
targetRoot: result.target.local.targetRoot
|
|
169
|
+
});
|
|
170
|
+
let writtenPaths;
|
|
171
|
+
try {
|
|
172
|
+
({ writtenPaths } = writeAndroidReportFiles({ outDir: reportsDir, prefix, report: reportModel, formats: config.formats }));
|
|
173
|
+
}
|
|
174
|
+
catch (err) {
|
|
175
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
176
|
+
console.error(`\nERROR: Failed to write Android report files: ${msg}`);
|
|
177
|
+
console.error("Validation completed but the report could not be written. The in-memory result was not persisted.");
|
|
178
|
+
return 4;
|
|
179
|
+
}
|
|
180
|
+
console.log(textReport);
|
|
181
|
+
console.log(`\nReports written:`);
|
|
182
|
+
for (const p of writtenPaths) {
|
|
183
|
+
console.log(` ${p}`);
|
|
184
|
+
}
|
|
185
|
+
const blockerExists = result.verdict === "not-ready-security-blocker-remains";
|
|
186
|
+
const inconclusive = result.verdict === "inconclusive-audit-environment-incomplete";
|
|
187
|
+
if (blockerExists) {
|
|
188
|
+
console.error("\nExit 1 — security blocker remains.");
|
|
189
|
+
return 1;
|
|
190
|
+
}
|
|
191
|
+
else if (inconclusive) {
|
|
192
|
+
console.warn("\nExit 2 — android environment incomplete.");
|
|
193
|
+
return 2;
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
console.log("\nExit 0 — validation completed.");
|
|
197
|
+
return 0;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
// ---------------------------------------------------------------------------
|
|
201
|
+
// Classic check-group/attack-scenario pipeline (unchanged from prior versions).
|
|
202
|
+
// ---------------------------------------------------------------------------
|
|
203
|
+
if (!config.checksWereDefault) {
|
|
204
|
+
console.log(`Checks : ${config.checks.join(", ")}`);
|
|
205
|
+
}
|
|
206
|
+
if (config.plannedChecksRequested.length > 0) {
|
|
207
|
+
console.log(`Note : ${config.plannedChecksRequested.join(", ")} are routed through the attack-scenario framework rather than the classic check-group runners. Review the attack-scenario section in the report for their concrete results.`);
|
|
208
|
+
}
|
|
209
|
+
console.log("");
|
|
210
|
+
const summary = await runSecurityValidation({
|
|
211
|
+
cwd: toolRoot,
|
|
212
|
+
targetPath: args.target,
|
|
213
|
+
fuzzIterations: parseInt(process.env["FUZZ_ITERATIONS"] ?? "50", 10),
|
|
214
|
+
fuzzSeed: parseInt(process.env["FUZZ_SEED"] ?? "0xDEADBEEF", 16),
|
|
215
|
+
// Full selection (implemented + planned/attack-scenario ids) -- the runner
|
|
216
|
+
// gates both implemented check groups and attack-scenario checks off the
|
|
217
|
+
// same selectedChecks set.
|
|
218
|
+
selectedChecks: config.checks,
|
|
219
|
+
profile: config.profile
|
|
220
|
+
});
|
|
221
|
+
// v0.2.2 Batch 5: --fail-on breach is additive to (never a substitute for)
|
|
222
|
+
// the existing verdict-based exit decision below -- it can only escalate
|
|
223
|
+
// exit 0 to exit 1 (e.g. --fail-on medium/low catching minor/informational
|
|
224
|
+
// findings the verdict policy doesn't already treat as blocking), never
|
|
225
|
+
// downgrade an existing blocker.
|
|
226
|
+
const failOnBreached = findingsBreachFailOnThreshold(summary.findings, config.failOnThreshold);
|
|
227
|
+
// Build report object
|
|
228
|
+
const report = buildSecurityReportFromSummary(summary, {
|
|
229
|
+
profile: config.profile,
|
|
230
|
+
selectedChecks: config.checks,
|
|
231
|
+
failOnThreshold: config.failOnThreshold,
|
|
232
|
+
formats: config.formats,
|
|
233
|
+
failOnBreached
|
|
234
|
+
});
|
|
235
|
+
const textReport = renderTextReport(report);
|
|
236
|
+
// Determine output directory
|
|
237
|
+
const reportsDir = config.out;
|
|
238
|
+
// Determine report filename prefix
|
|
239
|
+
const prefix = args.reportPrefix ?? reportFilenamePrefix(target);
|
|
240
|
+
const { writtenPaths } = writeSecurityReportFiles({
|
|
241
|
+
outDir: reportsDir,
|
|
242
|
+
prefix,
|
|
243
|
+
report,
|
|
244
|
+
formats: config.formats
|
|
245
|
+
});
|
|
246
|
+
// Print report to stdout
|
|
247
|
+
console.log(textReport);
|
|
248
|
+
console.log(`\nReports written:`);
|
|
249
|
+
for (const p of writtenPaths) {
|
|
250
|
+
console.log(` ${p}`);
|
|
251
|
+
}
|
|
252
|
+
// Exit code based on verdict, escalated (never downgraded) by --fail-on.
|
|
253
|
+
const blockerExists = summary.verdict === "not-ready-security-blocker-remains";
|
|
254
|
+
const inconclusive = summary.verdict === "inconclusive-audit-environment-incomplete";
|
|
255
|
+
if (blockerExists) {
|
|
256
|
+
console.error("\nExit 1 — security blocker remains.");
|
|
257
|
+
return 1;
|
|
258
|
+
}
|
|
259
|
+
else if (inconclusive) {
|
|
260
|
+
console.warn("\nExit 2 — audit environment incomplete.");
|
|
261
|
+
return 2;
|
|
262
|
+
}
|
|
263
|
+
else if (failOnBreached) {
|
|
264
|
+
console.error(`\nExit 1 — --fail-on ${config.failOnThreshold} threshold breached.`);
|
|
265
|
+
return 1;
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
console.log("\nExit 0 — validation completed.");
|
|
269
|
+
return 0;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// v0.4.5 Batch 4: bounded end-to-end fixture loader (section 22). Loads a frozen
|
|
2
|
+
// context-integrity ecosystem fixture (failed-run or corrected-replay) through the existing
|
|
3
|
+
// Batch 1 producer readers and Batch 3 run-integrity reader, verifies hashes, and rejects
|
|
4
|
+
// path traversal. It never recalculates metrics itself -- evaluation happens through the
|
|
5
|
+
// existing evaluateProducerReadinessBridge, called by the test/consumer, not by this loader.
|
|
6
|
+
import { readFile } from "node:fs/promises";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { readMyDevKitContextCapsuleV1 } from "../upstreamArtifacts/readMyDevKitContextCapsuleV1.js";
|
|
9
|
+
import { readMyDevKitRetrievalAuditRecordV1 } from "../upstreamArtifacts/readMyDevKitRetrievalAuditRecordV1.js";
|
|
10
|
+
import { readOrchestratorRunIntegrityEvidenceV1 } from "../upstreamArtifacts/readOrchestratorRunIntegrityV1.js";
|
|
11
|
+
import { isSafeFixtureRelativePath, validateEcosystemFixtureManifest } from "./validateManifest.js";
|
|
12
|
+
import { verifyFixtureHashes } from "./verifyFixtureHashes.js";
|
|
13
|
+
function findEntry(manifest, role) {
|
|
14
|
+
return manifest.artifacts.find((a) => a.role === role && a.evaluationUse);
|
|
15
|
+
}
|
|
16
|
+
export async function loadContextIntegrityFixture(fixtureRoot, manifestPath) {
|
|
17
|
+
const issues = [];
|
|
18
|
+
let manifestRaw;
|
|
19
|
+
try {
|
|
20
|
+
manifestRaw = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
return {
|
|
24
|
+
ok: false,
|
|
25
|
+
manifest: null,
|
|
26
|
+
hashVerification: null,
|
|
27
|
+
implementationCapsule: null,
|
|
28
|
+
implementationAudit: null,
|
|
29
|
+
runIntegrityEvidence: null,
|
|
30
|
+
issues: [`Manifest at "${manifestPath}" could not be read or parsed: ${error.message}`]
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
const manifestResult = validateEcosystemFixtureManifest(manifestRaw);
|
|
34
|
+
if (!manifestResult.ok) {
|
|
35
|
+
return {
|
|
36
|
+
ok: false,
|
|
37
|
+
manifest: null,
|
|
38
|
+
hashVerification: null,
|
|
39
|
+
implementationCapsule: null,
|
|
40
|
+
implementationAudit: null,
|
|
41
|
+
runIntegrityEvidence: null,
|
|
42
|
+
issues: manifestResult.issues.map((i) => `[${i.code}] ${i.fieldPath}: ${i.message}`)
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
const manifest = manifestResult.manifest;
|
|
46
|
+
const hashVerification = await verifyFixtureHashes(manifest, fixtureRoot);
|
|
47
|
+
if (!hashVerification.ok) {
|
|
48
|
+
issues.push(...hashVerification.issues.map((i) => `[${i.code}] ${i.fixtureRelativePath}: ${i.message}`));
|
|
49
|
+
}
|
|
50
|
+
function resolveSafe(relativePath) {
|
|
51
|
+
if (!isSafeFixtureRelativePath(relativePath)) {
|
|
52
|
+
issues.push(`Refused to resolve unsafe fixture-relative path "${relativePath}".`);
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
return path.resolve(fixtureRoot, relativePath);
|
|
56
|
+
}
|
|
57
|
+
let implementationCapsule = null;
|
|
58
|
+
const capsuleEntry = findEntry(manifest, "context-capsule");
|
|
59
|
+
if (capsuleEntry) {
|
|
60
|
+
const resolved = resolveSafe(capsuleEntry.fixtureRelativePath);
|
|
61
|
+
if (resolved) {
|
|
62
|
+
const result = await readMyDevKitContextCapsuleV1(resolved);
|
|
63
|
+
if (result.ok)
|
|
64
|
+
implementationCapsule = result.artifact;
|
|
65
|
+
else
|
|
66
|
+
issues.push(`context-capsule reader failed for "${capsuleEntry.fixtureRelativePath}": [${result.code}] ${result.message}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
let implementationAudit = null;
|
|
70
|
+
const auditEntry = findEntry(manifest, "retrieval-audit-record");
|
|
71
|
+
if (auditEntry) {
|
|
72
|
+
const resolved = resolveSafe(auditEntry.fixtureRelativePath);
|
|
73
|
+
if (resolved) {
|
|
74
|
+
const result = await readMyDevKitRetrievalAuditRecordV1(resolved);
|
|
75
|
+
if (result.ok)
|
|
76
|
+
implementationAudit = result.artifact;
|
|
77
|
+
else
|
|
78
|
+
issues.push(`retrieval-audit-record reader failed for "${auditEntry.fixtureRelativePath}": [${result.code}] ${result.message}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
let runIntegrityEvidence = null;
|
|
82
|
+
const runIntegrityEntry = findEntry(manifest, "run-integrity-evidence");
|
|
83
|
+
if (runIntegrityEntry) {
|
|
84
|
+
const resolved = resolveSafe(runIntegrityEntry.fixtureRelativePath);
|
|
85
|
+
if (resolved) {
|
|
86
|
+
const result = await readOrchestratorRunIntegrityEvidenceV1(resolved);
|
|
87
|
+
if (result.ok)
|
|
88
|
+
runIntegrityEvidence = result.artifact;
|
|
89
|
+
else
|
|
90
|
+
issues.push(`run-integrity-evidence reader failed for "${runIntegrityEntry.fixtureRelativePath}": [${result.code}] ${result.message}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
ok: issues.length === 0,
|
|
95
|
+
manifest,
|
|
96
|
+
hashVerification,
|
|
97
|
+
implementationCapsule,
|
|
98
|
+
implementationAudit,
|
|
99
|
+
runIntegrityEvidence,
|
|
100
|
+
issues
|
|
101
|
+
};
|
|
102
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
function isPlainObject(value) {
|
|
2
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3
|
+
}
|
|
4
|
+
// Rejects any fixture-relative path that is absolute, empty, or escapes the fixture root
|
|
5
|
+
// via ".." segments (section 22 "rejects path traversal"). Windows and POSIX separators are
|
|
6
|
+
// both checked since fixture manifests are authored on Windows but tests may run on either.
|
|
7
|
+
export function isSafeFixtureRelativePath(candidate) {
|
|
8
|
+
if (candidate.length === 0)
|
|
9
|
+
return false;
|
|
10
|
+
if (candidate.startsWith("/") || candidate.startsWith("\\"))
|
|
11
|
+
return false;
|
|
12
|
+
if (/^[a-zA-Z]:[\\/]/.test(candidate))
|
|
13
|
+
return false;
|
|
14
|
+
const segments = candidate.split(/[\\/]/);
|
|
15
|
+
return segments.every((segment) => segment !== "..") && segments.length > 0;
|
|
16
|
+
}
|
|
17
|
+
export function validateEcosystemFixtureManifest(value) {
|
|
18
|
+
const issues = [];
|
|
19
|
+
if (!isPlainObject(value)) {
|
|
20
|
+
return { ok: false, issues: [{ code: "NON_OBJECT_MANIFEST", fieldPath: "", message: "Manifest must be a plain JSON object." }] };
|
|
21
|
+
}
|
|
22
|
+
const schemaVersion = value["manifestSchemaVersion"];
|
|
23
|
+
if (typeof schemaVersion !== "string" || !/^1\./.test(schemaVersion)) {
|
|
24
|
+
return {
|
|
25
|
+
ok: false,
|
|
26
|
+
issues: [
|
|
27
|
+
{
|
|
28
|
+
code: "UNSUPPORTED_MANIFEST_SCHEMA_MAJOR",
|
|
29
|
+
fieldPath: "manifestSchemaVersion",
|
|
30
|
+
message: `Manifest schema version "${String(schemaVersion)}" is unsupported; expected major version 1.`
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
const requiredStringFields = ["fixtureId", "description", "sourceEvidenceRoot", "generatedAt"];
|
|
36
|
+
for (const field of requiredStringFields) {
|
|
37
|
+
if (typeof value[field] !== "string" || value[field].length === 0) {
|
|
38
|
+
issues.push({ code: "MISSING_REQUIRED_FIELD", fieldPath: field, message: `Manifest is missing required non-empty string field "${field}".` });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const artifacts = value["artifacts"];
|
|
42
|
+
if (!Array.isArray(artifacts)) {
|
|
43
|
+
issues.push({ code: "INVALID_FIELD_TYPE", fieldPath: "artifacts", message: 'Manifest field "artifacts" must be an array.' });
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
const seenPaths = new Set();
|
|
47
|
+
artifacts.forEach((entry, index) => {
|
|
48
|
+
const fieldPath = `artifacts[${index}]`;
|
|
49
|
+
if (!isPlainObject(entry)) {
|
|
50
|
+
issues.push({ code: "INVALID_FIELD_TYPE", fieldPath, message: "Each artifact entry must be an object." });
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const relPath = entry["fixtureRelativePath"];
|
|
54
|
+
if (typeof relPath !== "string" || relPath.length === 0) {
|
|
55
|
+
issues.push({ code: "MISSING_REQUIRED_FIELD", fieldPath: `${fieldPath}.fixtureRelativePath`, message: "Missing fixtureRelativePath." });
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (!isSafeFixtureRelativePath(relPath)) {
|
|
59
|
+
issues.push({
|
|
60
|
+
code: "PATH_ESCAPES_FIXTURE_ROOT",
|
|
61
|
+
fieldPath: `${fieldPath}.fixtureRelativePath`,
|
|
62
|
+
message: `fixtureRelativePath "${relPath}" is absolute or escapes the fixture root.`
|
|
63
|
+
});
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (seenPaths.has(relPath)) {
|
|
67
|
+
issues.push({ code: "DUPLICATE_ARTIFACT_PATH", fieldPath: `${fieldPath}.fixtureRelativePath`, message: `Duplicate fixtureRelativePath "${relPath}".` });
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
seenPaths.add(relPath);
|
|
71
|
+
for (const hashField of ["originalSha256", "copiedSha256"]) {
|
|
72
|
+
const v = entry[hashField];
|
|
73
|
+
if (typeof v !== "string" || !/^[0-9a-f]{64}$/i.test(v)) {
|
|
74
|
+
issues.push({ code: "INVALID_FIELD_TYPE", fieldPath: `${fieldPath}.${hashField}`, message: `${hashField} must be a 64-hex-character SHA-256 digest.` });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
if (issues.length > 0)
|
|
80
|
+
return { ok: false, issues };
|
|
81
|
+
return { ok: true, manifest: value };
|
|
82
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { isSafeFixtureRelativePath } from "./validateManifest.js";
|
|
5
|
+
async function sha256OfFile(absolutePath) {
|
|
6
|
+
const contents = await readFile(absolutePath);
|
|
7
|
+
return createHash("sha256").update(contents).digest("hex");
|
|
8
|
+
}
|
|
9
|
+
// Section 21: reads the manifest, hashes each tracked fixture file, and reports missing
|
|
10
|
+
// files, hash mismatches, and path-traversal attempts with deterministic (manifest) order.
|
|
11
|
+
// Never rewrites the manifest or the fixture; a mismatch is reported, not repaired.
|
|
12
|
+
export async function verifyFixtureHashes(manifest, fixtureRoot) {
|
|
13
|
+
const issues = [];
|
|
14
|
+
let checkedCount = 0;
|
|
15
|
+
for (const artifact of manifest.artifacts) {
|
|
16
|
+
const relPath = artifact.fixtureRelativePath;
|
|
17
|
+
if (!isSafeFixtureRelativePath(relPath)) {
|
|
18
|
+
issues.push({ code: "PATH_ESCAPES_FIXTURE_ROOT", fixtureRelativePath: relPath, message: `Manifest entry references an unsafe path: "${relPath}".` });
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
const absolutePath = path.resolve(fixtureRoot, relPath);
|
|
22
|
+
let actualSha256;
|
|
23
|
+
try {
|
|
24
|
+
actualSha256 = await sha256OfFile(absolutePath);
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
const code = error.code;
|
|
28
|
+
issues.push({
|
|
29
|
+
code: code === "ENOENT" ? "MISSING_FILE" : "UNREADABLE_FILE",
|
|
30
|
+
fixtureRelativePath: relPath,
|
|
31
|
+
message: code === "ENOENT" ? `Fixture file is missing: "${relPath}".` : `Fixture file could not be read: "${relPath}" (${error.message}).`
|
|
32
|
+
});
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
checkedCount += 1;
|
|
36
|
+
if (actualSha256 !== artifact.copiedSha256) {
|
|
37
|
+
issues.push({
|
|
38
|
+
code: "HASH_MISMATCH",
|
|
39
|
+
fixtureRelativePath: relPath,
|
|
40
|
+
message: `Fixture file "${relPath}" hash does not match the manifest's recorded copiedSha256 (expected ${artifact.copiedSha256}, got ${actualSha256}).`
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return { ok: issues.length === 0, checkedCount, issues };
|
|
45
|
+
}
|