Add scripts for resolving and verifying Chromium binary paths
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
- Implemented `chrome-path.js` to define functions for locating Chromium binaries across different platforms and nested directories. - Added `verify-chromium.js` to check for the presence of the Chromium binary and log the results, including candidate paths checked. - The scripts support Linux, Windows, and macOS environments, enhancing the flexibility of Chromium binary detection.
This commit is contained in:
16
.gitignore
vendored
16
.gitignore
vendored
@@ -18,10 +18,12 @@ obj/
|
|||||||
*.log
|
*.log
|
||||||
TestResults/
|
TestResults/
|
||||||
|
|
||||||
.dotnet
|
.dotnet
|
||||||
.DS_Store
|
.DS_Store
|
||||||
seed-data/ics-cisa/*.csv
|
seed-data/ics-cisa/*.csv
|
||||||
seed-data/ics-cisa/*.xlsx
|
seed-data/ics-cisa/*.xlsx
|
||||||
seed-data/ics-cisa/*.sha256
|
seed-data/ics-cisa/*.sha256
|
||||||
seed-data/cert-bund/**/*.json
|
seed-data/cert-bund/**/*.json
|
||||||
seed-data/cert-bund/**/*.sha256
|
seed-data/cert-bund/**/*.sha256
|
||||||
|
|
||||||
|
out/offline-kit/web/**/*
|
||||||
101
EXECPLAN.md
101
EXECPLAN.md
@@ -13,7 +13,7 @@ Generated from SPRINTS.md and module TASKS.md files on 2025-10-19. Waves cluster
|
|||||||
- Team Docs/CLI: read EXECPLAN.md Wave 0 and SPRINTS.md rows for `src/StellaOps.Cli/TASKS.md`. Focus on EXCITITOR-CLI-01-003 (TODO). Confirm prerequisites (external: EXCITITOR-CLI-01-001) before starting and report status in module TASKS.md.
|
- Team Docs/CLI: read EXECPLAN.md Wave 0 and SPRINTS.md rows for `src/StellaOps.Cli/TASKS.md`. Focus on EXCITITOR-CLI-01-003 (TODO). Confirm prerequisites (external: EXCITITOR-CLI-01-001) before starting and report status in module TASKS.md.
|
||||||
- Team Emit Guild: read EXECPLAN.md Wave 0 and SPRINTS.md rows for `src/StellaOps.Scanner.Emit/TASKS.md`. Focus on SCANNER-EMIT-10-601 (TODO), SCANNER-EMIT-10-602 (TODO), SCANNER-EMIT-10-603 (TODO), SCANNER-EMIT-10-604 (TODO), SCANNER-EMIT-10-605 (TODO), SCANNER-EMIT-10-606 (TODO). Confirm prerequisites (none) before starting and report status in module TASKS.md.
|
- Team Emit Guild: read EXECPLAN.md Wave 0 and SPRINTS.md rows for `src/StellaOps.Scanner.Emit/TASKS.md`. Focus on SCANNER-EMIT-10-601 (TODO), SCANNER-EMIT-10-602 (TODO), SCANNER-EMIT-10-603 (TODO), SCANNER-EMIT-10-604 (TODO), SCANNER-EMIT-10-605 (TODO), SCANNER-EMIT-10-606 (TODO). Confirm prerequisites (none) before starting and report status in module TASKS.md.
|
||||||
- Team EntryTrace Guild: read EXECPLAN.md Wave 0 and SPRINTS.md rows for `src/StellaOps.Scanner.EntryTrace/TASKS.md`. Focus on SCANNER-ENTRYTRACE-10-401 (TODO), SCANNER-ENTRYTRACE-10-402 (TODO), SCANNER-ENTRYTRACE-10-403 (TODO), SCANNER-ENTRYTRACE-10-404 (TODO), SCANNER-ENTRYTRACE-10-405 (TODO), SCANNER-ENTRYTRACE-10-406 (TODO), SCANNER-ENTRYTRACE-10-407 (TODO). Confirm prerequisites (none) before starting and report status in module TASKS.md.
|
- Team EntryTrace Guild: read EXECPLAN.md Wave 0 and SPRINTS.md rows for `src/StellaOps.Scanner.EntryTrace/TASKS.md`. Focus on SCANNER-ENTRYTRACE-10-401 (TODO), SCANNER-ENTRYTRACE-10-402 (TODO), SCANNER-ENTRYTRACE-10-403 (TODO), SCANNER-ENTRYTRACE-10-404 (TODO), SCANNER-ENTRYTRACE-10-405 (TODO), SCANNER-ENTRYTRACE-10-406 (TODO), SCANNER-ENTRYTRACE-10-407 (TODO). Confirm prerequisites (none) before starting and report status in module TASKS.md.
|
||||||
- Team Language Analyzer Guild: read EXECPLAN.md Wave 0 and SPRINTS.md rows for `src/StellaOps.Scanner.Analyzers.Lang/SPRINTS_LANG_IMPLEMENTATION_PLAN.md`, `src/StellaOps.Scanner.Analyzers.Lang/TASKS.md`. Focus on SCANNER-ANALYZERS-LANG-10-301 (TODO), SCANNER-ANALYZERS-LANG-10-307 (TODO), SCANNER-ANALYZERS-LANG-10-308 (TODO), SCANNER-ANALYZERS-LANG-10-302..309 (TODO). Confirm prerequisites (none) before starting and report status in module TASKS.md.
|
- Team Language Analyzer Guild: read EXECPLAN.md Wave 0 and SPRINTS.md rows for `src/StellaOps.Scanner.Analyzers.Lang/SPRINTS_LANG_IMPLEMENTATION_PLAN.md`, `src/StellaOps.Scanner.Analyzers.Lang/TASKS.md`. Focus on SCANNER-ANALYZERS-LANG-10-301 (TODO) and the upcoming Python/Go/.NET/Rust analyzers (10-303..306). Node sprint items 10-302/307/308/309 are DONE (latest 2025-10-21); shift coordination to remaining ecosystem analyzers and track follow-up work via module TASKS.md.
|
||||||
- Team Notify Models Guild: read EXECPLAN.md Wave 0 and SPRINTS.md rows for `src/StellaOps.Notify.Models/TASKS.md`. Focus on NOTIFY-MODELS-15-101 (TODO), NOTIFY-MODELS-15-102 (TODO), NOTIFY-MODELS-15-103 (TODO). Confirm prerequisites (none) before starting and report status in module TASKS.md.
|
- Team Notify Models Guild: read EXECPLAN.md Wave 0 and SPRINTS.md rows for `src/StellaOps.Notify.Models/TASKS.md`. Focus on NOTIFY-MODELS-15-101 (TODO), NOTIFY-MODELS-15-102 (TODO), NOTIFY-MODELS-15-103 (TODO). Confirm prerequisites (none) before starting and report status in module TASKS.md.
|
||||||
- Team Notify Storage Guild: read EXECPLAN.md Wave 0 and SPRINTS.md rows for `src/StellaOps.Notify.Storage.Mongo/TASKS.md`. Focus on NOTIFY-STORAGE-15-201 (TODO), NOTIFY-STORAGE-15-202 (TODO), NOTIFY-STORAGE-15-203 (TODO). Confirm prerequisites (none) before starting and report status in module TASKS.md.
|
- Team Notify Storage Guild: read EXECPLAN.md Wave 0 and SPRINTS.md rows for `src/StellaOps.Notify.Storage.Mongo/TASKS.md`. Focus on NOTIFY-STORAGE-15-201 (TODO), NOTIFY-STORAGE-15-202 (TODO), NOTIFY-STORAGE-15-203 (TODO). Confirm prerequisites (none) before starting and report status in module TASKS.md.
|
||||||
- Team Notify WebService Guild: read EXECPLAN.md Wave 0 and SPRINTS.md rows for `src/StellaOps.Notify.WebService/TASKS.md`. Focus on NOTIFY-WEB-15-101 (TODO), NOTIFY-WEB-15-102 (TODO). Confirm prerequisites (none) before starting and report status in module TASKS.md.
|
- Team Notify WebService Guild: read EXECPLAN.md Wave 0 and SPRINTS.md rows for `src/StellaOps.Notify.WebService/TASKS.md`. Focus on NOTIFY-WEB-15-101 (TODO), NOTIFY-WEB-15-102 (TODO). Confirm prerequisites (none) before starting and report status in module TASKS.md.
|
||||||
@@ -47,7 +47,7 @@ Generated from SPRINTS.md and module TASKS.md files on 2025-10-19. Waves cluster
|
|||||||
- Team Team Normalization & Storage Backbone: read EXECPLAN.md Wave 0 and SPRINTS.md rows for `src/StellaOps.Concelier.Storage.Mongo/TASKS.md`. Focus on FEEDSTORAGE-MONGO-08-001 (DONE 2025-10-19). Confirm prerequisites (none) before starting and report status in module TASKS.md.
|
- Team Team Normalization & Storage Backbone: read EXECPLAN.md Wave 0 and SPRINTS.md rows for `src/StellaOps.Concelier.Storage.Mongo/TASKS.md`. Focus on FEEDSTORAGE-MONGO-08-001 (DONE 2025-10-19). Confirm prerequisites (none) before starting and report status in module TASKS.md.
|
||||||
- Team Team WebService & Authority: read EXECPLAN.md Wave 0 and SPRINTS.md rows for `src/StellaOps.Authority/StellaOps.Authority.Plugin.Standard/TASKS.md`, `src/StellaOps.Concelier.WebService/TASKS.md`. Focus on SEC2.PLG (DOING), SEC3.PLG (DOING), SEC5.PLG (DOING), PLG4-6.CAPABILITIES (BLOCKED), PLG6.DIAGRAM (TODO), PLG7.RFC (REVIEW), FEEDWEB-DOCS-01-001 (DOING), FEEDWEB-OPS-01-006 (TODO), FEEDWEB-OPS-01-007 (BLOCKED). Confirm prerequisites (none) before starting and report status in module TASKS.md.
|
- Team Team WebService & Authority: read EXECPLAN.md Wave 0 and SPRINTS.md rows for `src/StellaOps.Authority/StellaOps.Authority.Plugin.Standard/TASKS.md`, `src/StellaOps.Concelier.WebService/TASKS.md`. Focus on SEC2.PLG (DOING), SEC3.PLG (DOING), SEC5.PLG (DOING), PLG4-6.CAPABILITIES (BLOCKED), PLG6.DIAGRAM (TODO), PLG7.RFC (REVIEW), FEEDWEB-DOCS-01-001 (DOING), FEEDWEB-OPS-01-006 (TODO), FEEDWEB-OPS-01-007 (BLOCKED). Confirm prerequisites (none) before starting and report status in module TASKS.md.
|
||||||
- Team Tools Guild, BE-Conn-MSRC: read EXECPLAN.md Wave 0 and SPRINTS.md rows for `src/StellaOps.Concelier.Connector.Common/TASKS.md`. Focus on FEEDCONN-SHARED-STATE-003 (**TODO). Confirm prerequisites (none) before starting and report status in module TASKS.md.
|
- Team Tools Guild, BE-Conn-MSRC: read EXECPLAN.md Wave 0 and SPRINTS.md rows for `src/StellaOps.Concelier.Connector.Common/TASKS.md`. Focus on FEEDCONN-SHARED-STATE-003 (**TODO). Confirm prerequisites (none) before starting and report status in module TASKS.md.
|
||||||
- Team UX Specialist, Angular Eng: read EXECPLAN.md Wave 0 and SPRINTS.md rows for `src/StellaOps.Web/TASKS.md`. Focus on WEB1.TRIVY-SETTINGS (DONE 2025-10-21), WEB1.TRIVY-SETTINGS-TESTS (DONE 2025-10-21), and WEB1.DEPS-13-001 (TODO). Confirm prerequisites (none) before starting and report status in module TASKS.md.
|
- Team UX Specialist, Angular Eng: read EXECPLAN.md Wave 0 and SPRINTS.md rows for `src/StellaOps.Web/TASKS.md`. Focus on WEB1.TRIVY-SETTINGS (DONE 2025-10-21), WEB1.TRIVY-SETTINGS-TESTS (DONE 2025-10-21), and WEB1.DEPS-13-001 (DONE 2025-10-21). Confirm prerequisites (none) before starting and report status in module TASKS.md.
|
||||||
- Team Zastava Core Guild: read EXECPLAN.md Wave 0 and SPRINTS.md rows for `src/StellaOps.Zastava.Core/TASKS.md`. Focus on ZASTAVA-CORE-12-201 (TODO), ZASTAVA-CORE-12-202 (TODO), ZASTAVA-CORE-12-203 (TODO), ZASTAVA-OPS-12-204 (TODO). Confirm prerequisites (none) before starting and report status in module TASKS.md.
|
- Team Zastava Core Guild: read EXECPLAN.md Wave 0 and SPRINTS.md rows for `src/StellaOps.Zastava.Core/TASKS.md`. Focus on ZASTAVA-CORE-12-201 (TODO), ZASTAVA-CORE-12-202 (TODO), ZASTAVA-CORE-12-203 (TODO), ZASTAVA-OPS-12-204 (TODO). Confirm prerequisites (none) before starting and report status in module TASKS.md.
|
||||||
- Team Zastava Webhook Guild: read EXECPLAN.md Wave 0 and SPRINTS.md rows for `src/StellaOps.Zastava.Webhook/TASKS.md`. Focus on ZASTAVA-WEBHOOK-12-101 (TODO), ZASTAVA-WEBHOOK-12-102 (TODO), ZASTAVA-WEBHOOK-12-103 (TODO). Confirm prerequisites (none) before starting and report status in module TASKS.md.
|
- Team Zastava Webhook Guild: read EXECPLAN.md Wave 0 and SPRINTS.md rows for `src/StellaOps.Zastava.Webhook/TASKS.md`. Focus on ZASTAVA-WEBHOOK-12-101 (TODO), ZASTAVA-WEBHOOK-12-102 (TODO), ZASTAVA-WEBHOOK-12-103 (TODO). Confirm prerequisites (none) before starting and report status in module TASKS.md.
|
||||||
|
|
||||||
@@ -57,7 +57,7 @@ Generated from SPRINTS.md and module TASKS.md files on 2025-10-19. Waves cluster
|
|||||||
- Team DevOps Guild: read EXECPLAN.md Wave 1 and SPRINTS.md rows for `ops/devops/TASKS.md`. Focus on DEVOPS-REL-14-001 (TODO). Confirm prerequisites (internal: ATTESTOR-API-11-201 (Wave 0), SIGNER-API-11-101 (Wave 0)) before starting and report status in module TASKS.md.
|
- Team DevOps Guild: read EXECPLAN.md Wave 1 and SPRINTS.md rows for `ops/devops/TASKS.md`. Focus on DEVOPS-REL-14-001 (TODO). Confirm prerequisites (internal: ATTESTOR-API-11-201 (Wave 0), SIGNER-API-11-101 (Wave 0)) before starting and report status in module TASKS.md.
|
||||||
- Team DevOps Guild, Scanner WebService Guild: read EXECPLAN.md Wave 1 and SPRINTS.md rows for `ops/devops/TASKS.md`. Focus on DEVOPS-SCANNER-09-204 (TODO). Confirm prerequisites (internal: SCANNER-EVENTS-15-201 (Wave 0)) before starting and report status in module TASKS.md.
|
- Team DevOps Guild, Scanner WebService Guild: read EXECPLAN.md Wave 1 and SPRINTS.md rows for `ops/devops/TASKS.md`. Focus on DEVOPS-SCANNER-09-204 (TODO). Confirm prerequisites (internal: SCANNER-EVENTS-15-201 (Wave 0)) before starting and report status in module TASKS.md.
|
||||||
- Team Emit Guild: read EXECPLAN.md Wave 1 and SPRINTS.md rows for `src/StellaOps.Scanner.Emit/TASKS.md`. Focus on SCANNER-EMIT-10-607 (TODO), SCANNER-EMIT-17-701 (TODO). Confirm prerequisites (internal: POLICY-CORE-09-005 (Wave 0), SCANNER-EMIT-10-602 (Wave 0), SCANNER-EMIT-10-604 (Wave 0)) before starting and report status in module TASKS.md.
|
- Team Emit Guild: read EXECPLAN.md Wave 1 and SPRINTS.md rows for `src/StellaOps.Scanner.Emit/TASKS.md`. Focus on SCANNER-EMIT-10-607 (TODO), SCANNER-EMIT-17-701 (TODO). Confirm prerequisites (internal: POLICY-CORE-09-005 (Wave 0), SCANNER-EMIT-10-602 (Wave 0), SCANNER-EMIT-10-604 (Wave 0)) before starting and report status in module TASKS.md.
|
||||||
- Team Language Analyzer Guild: read EXECPLAN.md Wave 1 and SPRINTS.md rows for `src/StellaOps.Scanner.Analyzers.Lang/TASKS.md`. Focus on SCANNER-ANALYZERS-LANG-10-309 (DOING), SCANNER-ANALYZERS-LANG-10-306 (TODO), SCANNER-ANALYZERS-LANG-10-302 (DOING), SCANNER-ANALYZERS-LANG-10-304 (TODO), SCANNER-ANALYZERS-LANG-10-305 (TODO), SCANNER-ANALYZERS-LANG-10-303 (TODO). Confirm prerequisites (internal: SCANNER-ANALYZERS-LANG-10-301 (Wave 0), SCANNER-ANALYZERS-LANG-10-307 (Wave 0)) before starting and report status in module TASKS.md.
|
- Team Language Analyzer Guild: read EXECPLAN.md Wave 1 and SPRINTS.md rows for `src/StellaOps.Scanner.Analyzers.Lang/TASKS.md`. Focus on SCANNER-ANALYZERS-LANG-10-303 (DONE 2025-10-21), SCANNER-ANALYZERS-LANG-10-304 (DOING 2025-10-22), SCANNER-ANALYZERS-LANG-10-305 (DOING 2025-10-22), SCANNER-ANALYZERS-LANG-10-306 (TODO). Node stream (tasks 10-302/309) closed on 2025-10-21; verify prereqs SCANNER-ANALYZERS-LANG-10-301/307 remain satisfied before pivoting to the remaining language analyzers.
|
||||||
- Team Licensing Guild: read EXECPLAN.md Wave 1 and SPRINTS.md rows for `ops/licensing/TASKS.md`. Focus on DEVOPS-LIC-14-004 (TODO). Confirm prerequisites (internal: AUTH-MTLS-11-002 (Wave 0)) before starting and report status in module TASKS.md.
|
- Team Licensing Guild: read EXECPLAN.md Wave 1 and SPRINTS.md rows for `ops/licensing/TASKS.md`. Focus on DEVOPS-LIC-14-004 (TODO). Confirm prerequisites (internal: AUTH-MTLS-11-002 (Wave 0)) before starting and report status in module TASKS.md.
|
||||||
- Team Notify Engine Guild: read EXECPLAN.md Wave 1 and SPRINTS.md rows for `src/StellaOps.Notify.Engine/TASKS.md`. Focus on NOTIFY-ENGINE-15-301 (TODO). Confirm prerequisites (internal: NOTIFY-MODELS-15-101 (Wave 0)) before starting and report status in module TASKS.md.
|
- Team Notify Engine Guild: read EXECPLAN.md Wave 1 and SPRINTS.md rows for `src/StellaOps.Notify.Engine/TASKS.md`. Focus on NOTIFY-ENGINE-15-301 (TODO). Confirm prerequisites (internal: NOTIFY-MODELS-15-101 (Wave 0)) before starting and report status in module TASKS.md.
|
||||||
- Team Notify Queue Guild: read EXECPLAN.md Wave 1 and SPRINTS.md rows for `src/StellaOps.Notify.Queue/TASKS.md`. Focus on NOTIFY-QUEUE-15-401 (TODO). Confirm prerequisites (internal: NOTIFY-MODELS-15-101 (Wave 0)) before starting and report status in module TASKS.md.
|
- Team Notify Queue Guild: read EXECPLAN.md Wave 1 and SPRINTS.md rows for `src/StellaOps.Notify.Queue/TASKS.md`. Focus on NOTIFY-QUEUE-15-401 (TODO). Confirm prerequisites (internal: NOTIFY-MODELS-15-101 (Wave 0)) before starting and report status in module TASKS.md.
|
||||||
@@ -68,7 +68,7 @@ Generated from SPRINTS.md and module TASKS.md files on 2025-10-19. Waves cluster
|
|||||||
- Team Scheduler Storage Guild: read EXECPLAN.md Wave 1 and SPRINTS.md rows for `src/StellaOps.Scheduler.Storage.Mongo/TASKS.md`. Focus on SCHED-STORAGE-16-203 (TODO), SCHED-STORAGE-16-202 (TODO). Confirm prerequisites (internal: SCHED-STORAGE-16-201 (Wave 0)) before starting and report status in module TASKS.md.
|
- Team Scheduler Storage Guild: read EXECPLAN.md Wave 1 and SPRINTS.md rows for `src/StellaOps.Scheduler.Storage.Mongo/TASKS.md`. Focus on SCHED-STORAGE-16-203 (TODO), SCHED-STORAGE-16-202 (TODO). Confirm prerequisites (internal: SCHED-STORAGE-16-201 (Wave 0)) before starting and report status in module TASKS.md.
|
||||||
- Team Scheduler WebService Guild: read EXECPLAN.md Wave 1 and SPRINTS.md rows for `src/StellaOps.Scheduler.WebService/TASKS.md`. Focus on SCHED-WEB-16-104 (TODO), SCHED-WEB-16-102 (TODO). Confirm prerequisites (internal: SCHED-QUEUE-16-401 (Wave 0), SCHED-STORAGE-16-201 (Wave 0), SCHED-WEB-16-101 (Wave 0)) before starting and report status in module TASKS.md.
|
- Team Scheduler WebService Guild: read EXECPLAN.md Wave 1 and SPRINTS.md rows for `src/StellaOps.Scheduler.WebService/TASKS.md`. Focus on SCHED-WEB-16-104 (TODO), SCHED-WEB-16-102 (TODO). Confirm prerequisites (internal: SCHED-QUEUE-16-401 (Wave 0), SCHED-STORAGE-16-201 (Wave 0), SCHED-WEB-16-101 (Wave 0)) before starting and report status in module TASKS.md.
|
||||||
- Team Scheduler Worker Guild: read EXECPLAN.md Wave 1 and SPRINTS.md rows for `src/StellaOps.Scheduler.Worker/TASKS.md`. Focus on SCHED-WORKER-16-201 (TODO). Confirm prerequisites (internal: SCHED-QUEUE-16-401 (Wave 0)) before starting and report status in module TASKS.md.
|
- Team Scheduler Worker Guild: read EXECPLAN.md Wave 1 and SPRINTS.md rows for `src/StellaOps.Scheduler.Worker/TASKS.md`. Focus on SCHED-WORKER-16-201 (TODO). Confirm prerequisites (internal: SCHED-QUEUE-16-401 (Wave 0)) before starting and report status in module TASKS.md.
|
||||||
- Team TBD: read EXECPLAN.md Wave 1 and SPRINTS.md rows for `src/StellaOps.Scanner.Analyzers.Lang.DotNet/TASKS.md`, `src/StellaOps.Scanner.Analyzers.Lang.Go/TASKS.md`, `src/StellaOps.Scanner.Analyzers.Lang.Node/TASKS.md`, `src/StellaOps.Scanner.Analyzers.Lang.Python/TASKS.md`, `src/StellaOps.Scanner.Analyzers.Lang.Rust/TASKS.md`. Focus on SCANNER-ANALYZERS-LANG-10-305A (TODO), SCANNER-ANALYZERS-LANG-10-304A (TODO), SCANNER-ANALYZERS-LANG-10-307N (TODO), SCANNER-ANALYZERS-LANG-10-303A (TODO), SCANNER-ANALYZERS-LANG-10-306A (TODO). Confirm prerequisites (internal: SCANNER-ANALYZERS-LANG-10-302C (Wave 0), SCANNER-ANALYZERS-LANG-10-307 (Wave 0)) before starting and report status in module TASKS.md.
|
- Team TBD: read EXECPLAN.md Wave 1 and SPRINTS.md rows for `src/StellaOps.Scanner.Analyzers.Lang.DotNet/TASKS.md`, `src/StellaOps.Scanner.Analyzers.Lang.Go/TASKS.md`, `src/StellaOps.Scanner.Analyzers.Lang.Node/TASKS.md`, `src/StellaOps.Scanner.Analyzers.Lang.Python/TASKS.md`, `src/StellaOps.Scanner.Analyzers.Lang.Rust/TASKS.md`. Focus on SCANNER-ANALYZERS-LANG-10-305A (DONE 2025-10-22), SCANNER-ANALYZERS-LANG-10-304A (DONE 2025-10-22), SCANNER-ANALYZERS-LANG-10-303A (DONE 2025-10-21), SCANNER-ANALYZERS-LANG-10-306A (TODO); Node add-ons 10-307N/10-308N/10-309N now DONE with restart-time packaging verified 2025-10-21. Confirm prerequisites (internal: SCANNER-ANALYZERS-LANG-10-302C (Wave 0), SCANNER-ANALYZERS-LANG-10-307 (Wave 0)) before starting and report status in module TASKS.md.
|
||||||
- Team Team Excititor Connectors – MSRC: read EXECPLAN.md Wave 1 and SPRINTS.md rows for `src/StellaOps.Excititor.Connectors.MSRC.CSAF/TASKS.md`. Focus on EXCITITOR-CONN-MS-01-003 (TODO). Confirm prerequisites (internal: EXCITITOR-CONN-MS-01-002 (Wave 0); external: EXCITITOR-POLICY-01-001) before starting and report status in module TASKS.md.
|
- Team Team Excititor Connectors – MSRC: read EXECPLAN.md Wave 1 and SPRINTS.md rows for `src/StellaOps.Excititor.Connectors.MSRC.CSAF/TASKS.md`. Focus on EXCITITOR-CONN-MS-01-003 (TODO). Confirm prerequisites (internal: EXCITITOR-CONN-MS-01-002 (Wave 0); external: EXCITITOR-POLICY-01-001) before starting and report status in module TASKS.md.
|
||||||
- Team Team Excititor Connectors – Oracle: read EXECPLAN.md Wave 1 and SPRINTS.md rows for `src/StellaOps.Excititor.Connectors.Oracle.CSAF/TASKS.md`. Focus on EXCITITOR-CONN-ORACLE-01-002 (TODO). Confirm prerequisites (internal: EXCITITOR-CONN-ORACLE-01-001 (Wave 0); external: EXCITITOR-STORAGE-01-003) before starting and report status in module TASKS.md.
|
- Team Team Excititor Connectors – Oracle: read EXECPLAN.md Wave 1 and SPRINTS.md rows for `src/StellaOps.Excititor.Connectors.Oracle.CSAF/TASKS.md`. Focus on EXCITITOR-CONN-ORACLE-01-002 (TODO). Confirm prerequisites (internal: EXCITITOR-CONN-ORACLE-01-001 (Wave 0); external: EXCITITOR-STORAGE-01-003) before starting and report status in module TASKS.md.
|
||||||
- Team Team Excititor Connectors – SUSE: read EXECPLAN.md Wave 1 and SPRINTS.md rows for `src/StellaOps.Excititor.Connectors.SUSE.RancherVEXHub/TASKS.md`. Focus on EXCITITOR-CONN-SUSE-01-003 (TODO). Confirm prerequisites (internal: EXCITITOR-CONN-SUSE-01-002 (Wave 0); external: EXCITITOR-POLICY-01-001) before starting and report status in module TASKS.md.
|
- Team Team Excititor Connectors – SUSE: read EXECPLAN.md Wave 1 and SPRINTS.md rows for `src/StellaOps.Excititor.Connectors.SUSE.RancherVEXHub/TASKS.md`. Focus on EXCITITOR-CONN-SUSE-01-003 (TODO). Confirm prerequisites (internal: EXCITITOR-CONN-SUSE-01-002 (Wave 0); external: EXCITITOR-POLICY-01-001) before starting and report status in module TASKS.md.
|
||||||
@@ -88,13 +88,13 @@ Generated from SPRINTS.md and module TASKS.md files on 2025-10-19. Waves cluster
|
|||||||
- Team Notify Queue Guild: read EXECPLAN.md Wave 2 and SPRINTS.md rows for `src/StellaOps.Notify.Queue/TASKS.md`. Focus on NOTIFY-QUEUE-15-403 (TODO), NOTIFY-QUEUE-15-402 (TODO). Confirm prerequisites (internal: NOTIFY-QUEUE-15-401 (Wave 1)) before starting and report status in module TASKS.md.
|
- Team Notify Queue Guild: read EXECPLAN.md Wave 2 and SPRINTS.md rows for `src/StellaOps.Notify.Queue/TASKS.md`. Focus on NOTIFY-QUEUE-15-403 (TODO), NOTIFY-QUEUE-15-402 (TODO). Confirm prerequisites (internal: NOTIFY-QUEUE-15-401 (Wave 1)) before starting and report status in module TASKS.md.
|
||||||
- Team Notify WebService Guild: read EXECPLAN.md Wave 2 and SPRINTS.md rows for `src/StellaOps.Notify.WebService/TASKS.md`. Focus on NOTIFY-WEB-15-104 (TODO). Confirm prerequisites (internal: NOTIFY-QUEUE-15-401 (Wave 1), NOTIFY-STORAGE-15-201 (Wave 0)) before starting and report status in module TASKS.md.
|
- Team Notify WebService Guild: read EXECPLAN.md Wave 2 and SPRINTS.md rows for `src/StellaOps.Notify.WebService/TASKS.md`. Focus on NOTIFY-WEB-15-104 (TODO). Confirm prerequisites (internal: NOTIFY-QUEUE-15-401 (Wave 1), NOTIFY-STORAGE-15-201 (Wave 0)) before starting and report status in module TASKS.md.
|
||||||
- Team Notify Worker Guild: read EXECPLAN.md Wave 2 and SPRINTS.md rows for `src/StellaOps.Notify.Worker/TASKS.md`. Focus on NOTIFY-WORKER-15-201 (TODO), NOTIFY-WORKER-15-202 (TODO). Confirm prerequisites (internal: NOTIFY-ENGINE-15-301 (Wave 1), NOTIFY-QUEUE-15-401 (Wave 1)) before starting and report status in module TASKS.md.
|
- Team Notify Worker Guild: read EXECPLAN.md Wave 2 and SPRINTS.md rows for `src/StellaOps.Notify.Worker/TASKS.md`. Focus on NOTIFY-WORKER-15-201 (TODO), NOTIFY-WORKER-15-202 (TODO). Confirm prerequisites (internal: NOTIFY-ENGINE-15-301 (Wave 1), NOTIFY-QUEUE-15-401 (Wave 1)) before starting and report status in module TASKS.md.
|
||||||
- Team Offline Kit Guild: read EXECPLAN.md Wave 2 and SPRINTS.md rows for `ops/offline-kit/TASKS.md`. Focus on DEVOPS-OFFLINE-14-002 (TODO). Confirm prerequisites (internal: DEVOPS-REL-14-001 (Wave 1)) before starting and report status in module TASKS.md.
|
- Team Offline Kit Guild: read EXECPLAN.md Wave 2 and SPRINTS.md rows for `ops/offline-kit/TASKS.md`. Focus on DEVOPS-OFFLINE-14-002 (TODO) and DEVOPS-OFFLINE-18-003 (TODO). Confirm prerequisites (internal: DEVOPS-REL-14-001 (Wave 1)) before starting and report status in module TASKS.md.
|
||||||
- Team Samples Guild, Policy Guild: read EXECPLAN.md Wave 2 and SPRINTS.md rows for `samples/TASKS.md`. Focus on SAMPLES-13-004 (TODO). Confirm prerequisites (internal: POLICY-CORE-09-006 (Wave 0), UI-POLICY-13-007 (Wave 1)) before starting and report status in module TASKS.md.
|
- Team Samples Guild, Policy Guild: read EXECPLAN.md Wave 2 and SPRINTS.md rows for `samples/TASKS.md`. Focus on SAMPLES-13-004 (TODO). Confirm prerequisites (internal: POLICY-CORE-09-006 (Wave 0), UI-POLICY-13-007 (Wave 1)) before starting and report status in module TASKS.md.
|
||||||
- Team Scanner WebService Guild: read EXECPLAN.md Wave 2 and SPRINTS.md rows for `src/StellaOps.Scanner.WebService/TASKS.md`. Focus on SCANNER-RUNTIME-12-302 (TODO). Confirm prerequisites (internal: SCANNER-RUNTIME-12-301 (Wave 1), ZASTAVA-CORE-12-201 (Wave 0)) before starting and report status in module TASKS.md.
|
- Team Scanner WebService Guild: read EXECPLAN.md Wave 2 and SPRINTS.md rows for `src/StellaOps.Scanner.WebService/TASKS.md`. Focus on SCANNER-RUNTIME-12-302 (TODO). Confirm prerequisites (internal: SCANNER-RUNTIME-12-301 (Wave 1), ZASTAVA-CORE-12-201 (Wave 0)) before starting and report status in module TASKS.md.
|
||||||
- Team Scheduler ImpactIndex Guild: read EXECPLAN.md Wave 2 and SPRINTS.md rows for `src/StellaOps.Scheduler.ImpactIndex/TASKS.md`. Focus on SCHED-IMPACT-16-303 (TODO), SCHED-IMPACT-16-302 (TODO). Confirm prerequisites (internal: SCHED-IMPACT-16-301 (Wave 1)) before starting and report status in module TASKS.md.
|
- Team Scheduler ImpactIndex Guild: read EXECPLAN.md Wave 2 and SPRINTS.md rows for `src/StellaOps.Scheduler.ImpactIndex/TASKS.md`. Focus on SCHED-IMPACT-16-303 (TODO), SCHED-IMPACT-16-302 (TODO). Confirm prerequisites (internal: SCHED-IMPACT-16-301 (Wave 1)) before starting and report status in module TASKS.md.
|
||||||
- Team Scheduler WebService Guild: read EXECPLAN.md Wave 2 and SPRINTS.md rows for `src/StellaOps.Scheduler.WebService/TASKS.md`. Focus on SCHED-WEB-16-103 (TODO). Confirm prerequisites (internal: SCHED-WEB-16-102 (Wave 1)) before starting and report status in module TASKS.md.
|
- Team Scheduler WebService Guild: read EXECPLAN.md Wave 2 and SPRINTS.md rows for `src/StellaOps.Scheduler.WebService/TASKS.md`. Focus on SCHED-WEB-16-103 (TODO). Confirm prerequisites (internal: SCHED-WEB-16-102 (Wave 1)) before starting and report status in module TASKS.md.
|
||||||
- Team Scheduler Worker Guild: read EXECPLAN.md Wave 2 and SPRINTS.md rows for `src/StellaOps.Scheduler.Worker/TASKS.md`. Focus on SCHED-WORKER-16-202 (TODO), SCHED-WORKER-16-205 (TODO). Confirm prerequisites (internal: SCHED-IMPACT-16-301 (Wave 1), SCHED-WORKER-16-201 (Wave 1)) before starting and report status in module TASKS.md.
|
- Team Scheduler Worker Guild: read EXECPLAN.md Wave 2 and SPRINTS.md rows for `src/StellaOps.Scheduler.Worker/TASKS.md`. Focus on SCHED-WORKER-16-202 (TODO), SCHED-WORKER-16-205 (TODO). Confirm prerequisites (internal: SCHED-IMPACT-16-301 (Wave 1), SCHED-WORKER-16-201 (Wave 1)) before starting and report status in module TASKS.md.
|
||||||
- Team TBD: read EXECPLAN.md Wave 2 and SPRINTS.md rows for `src/StellaOps.Scanner.Analyzers.Lang.DotNet/TASKS.md`, `src/StellaOps.Scanner.Analyzers.Lang.Go/TASKS.md`, `src/StellaOps.Scanner.Analyzers.Lang.Node/TASKS.md`, `src/StellaOps.Scanner.Analyzers.Lang.Python/TASKS.md`, `src/StellaOps.Scanner.Analyzers.Lang.Rust/TASKS.md`. Focus on SCANNER-ANALYZERS-LANG-10-305B (TODO), SCANNER-ANALYZERS-LANG-10-304B (TODO), SCANNER-ANALYZERS-LANG-10-308N (TODO), SCANNER-ANALYZERS-LANG-10-303B (TODO), SCANNER-ANALYZERS-LANG-10-306B (TODO). Confirm prerequisites (internal: SCANNER-ANALYZERS-LANG-10-303A (Wave 1), SCANNER-ANALYZERS-LANG-10-304A (Wave 1), SCANNER-ANALYZERS-LANG-10-305A (Wave 1), SCANNER-ANALYZERS-LANG-10-306A (Wave 1), SCANNER-ANALYZERS-LANG-10-307N (Wave 1)) before starting and report status in module TASKS.md.
|
- Team TBD: read EXECPLAN.md Wave 2 and SPRINTS.md rows for `src/StellaOps.Scanner.Analyzers.Lang.DotNet/TASKS.md`, `src/StellaOps.Scanner.Analyzers.Lang.Go/TASKS.md`, `src/StellaOps.Scanner.Analyzers.Lang.Node/TASKS.md`, `src/StellaOps.Scanner.Analyzers.Lang.Python/TASKS.md`, `src/StellaOps.Scanner.Analyzers.Lang.Rust/TASKS.md`. Focus on SCANNER-ANALYZERS-LANG-10-305B (TODO), SCANNER-ANALYZERS-LANG-10-304B (DONE 2025-10-22), SCANNER-ANALYZERS-LANG-10-303B (DONE 2025-10-21), SCANNER-ANALYZERS-LANG-10-306B (TODO); Node packaging milestone 10-308N closed 2025-10-21. Confirm prerequisites (internal: SCANNER-ANALYZERS-LANG-10-303A (Wave 1), SCANNER-ANALYZERS-LANG-10-304A (Wave 1), SCANNER-ANALYZERS-LANG-10-305A (Wave 1), SCANNER-ANALYZERS-LANG-10-306A (Wave 1), SCANNER-ANALYZERS-LANG-10-307N (Wave 1)) before starting and report status in module TASKS.md.
|
||||||
- Team Team Excititor Connectors – Oracle: read EXECPLAN.md Wave 2 and SPRINTS.md rows for `src/StellaOps.Excititor.Connectors.Oracle.CSAF/TASKS.md`. Focus on EXCITITOR-CONN-ORACLE-01-003 (TODO). Confirm prerequisites (internal: EXCITITOR-CONN-ORACLE-01-002 (Wave 1); external: EXCITITOR-POLICY-01-001) before starting and report status in module TASKS.md.
|
- Team Team Excititor Connectors – Oracle: read EXECPLAN.md Wave 2 and SPRINTS.md rows for `src/StellaOps.Excititor.Connectors.Oracle.CSAF/TASKS.md`. Focus on EXCITITOR-CONN-ORACLE-01-003 (TODO). Confirm prerequisites (internal: EXCITITOR-CONN-ORACLE-01-002 (Wave 1); external: EXCITITOR-POLICY-01-001) before starting and report status in module TASKS.md.
|
||||||
- Team Team Excititor Export: read EXECPLAN.md Wave 2 and SPRINTS.md rows for `src/StellaOps.Excititor.Export/TASKS.md`. Focus on EXCITITOR-EXPORT-01-007 (DONE 2025-10-21). Confirm prerequisites (internal: EXCITITOR-EXPORT-01-006 (Wave 1)) before starting and report status in module TASKS.md.
|
- Team Team Excititor Export: read EXECPLAN.md Wave 2 and SPRINTS.md rows for `src/StellaOps.Excititor.Export/TASKS.md`. Focus on EXCITITOR-EXPORT-01-007 (DONE 2025-10-21). Confirm prerequisites (internal: EXCITITOR-EXPORT-01-006 (Wave 1)) before starting and report status in module TASKS.md.
|
||||||
- Team Zastava Observer Guild: read EXECPLAN.md Wave 2 and SPRINTS.md rows for `src/StellaOps.Zastava.Observer/TASKS.md`. Focus on ZASTAVA-OBS-12-002 (TODO). Confirm prerequisites (internal: ZASTAVA-OBS-12-001 (Wave 1)) before starting and report status in module TASKS.md.
|
- Team Zastava Observer Guild: read EXECPLAN.md Wave 2 and SPRINTS.md rows for `src/StellaOps.Zastava.Observer/TASKS.md`. Focus on ZASTAVA-OBS-12-002 (TODO). Confirm prerequisites (internal: ZASTAVA-OBS-12-001 (Wave 1)) before starting and report status in module TASKS.md.
|
||||||
@@ -106,11 +106,11 @@ Generated from SPRINTS.md and module TASKS.md files on 2025-10-19. Waves cluster
|
|||||||
- Team Notify Engine Guild: read EXECPLAN.md Wave 3 and SPRINTS.md rows for `src/StellaOps.Notify.Engine/TASKS.md`. Focus on NOTIFY-ENGINE-15-303 (TODO). Confirm prerequisites (internal: NOTIFY-ENGINE-15-302 (Wave 2)) before starting and report status in module TASKS.md.
|
- Team Notify Engine Guild: read EXECPLAN.md Wave 3 and SPRINTS.md rows for `src/StellaOps.Notify.Engine/TASKS.md`. Focus on NOTIFY-ENGINE-15-303 (TODO). Confirm prerequisites (internal: NOTIFY-ENGINE-15-302 (Wave 2)) before starting and report status in module TASKS.md.
|
||||||
- Team Notify Worker Guild: read EXECPLAN.md Wave 3 and SPRINTS.md rows for `src/StellaOps.Notify.Worker/TASKS.md`. Focus on NOTIFY-WORKER-15-203 (TODO). Confirm prerequisites (internal: NOTIFY-ENGINE-15-302 (Wave 2)) before starting and report status in module TASKS.md.
|
- Team Notify Worker Guild: read EXECPLAN.md Wave 3 and SPRINTS.md rows for `src/StellaOps.Notify.Worker/TASKS.md`. Focus on NOTIFY-WORKER-15-203 (TODO). Confirm prerequisites (internal: NOTIFY-ENGINE-15-302 (Wave 2)) before starting and report status in module TASKS.md.
|
||||||
- Team Scheduler Worker Guild: read EXECPLAN.md Wave 3 and SPRINTS.md rows for `src/StellaOps.Scheduler.Worker/TASKS.md`. Focus on SCHED-WORKER-16-203 (TODO). Confirm prerequisites (internal: SCHED-WORKER-16-202 (Wave 2)) before starting and report status in module TASKS.md.
|
- Team Scheduler Worker Guild: read EXECPLAN.md Wave 3 and SPRINTS.md rows for `src/StellaOps.Scheduler.Worker/TASKS.md`. Focus on SCHED-WORKER-16-203 (TODO). Confirm prerequisites (internal: SCHED-WORKER-16-202 (Wave 2)) before starting and report status in module TASKS.md.
|
||||||
- Team TBD: read EXECPLAN.md Wave 3 and SPRINTS.md rows for `src/StellaOps.Scanner.Analyzers.Lang.DotNet/TASKS.md`, `src/StellaOps.Scanner.Analyzers.Lang.Go/TASKS.md`, `src/StellaOps.Scanner.Analyzers.Lang.Node/TASKS.md`, `src/StellaOps.Scanner.Analyzers.Lang.Python/TASKS.md`, `src/StellaOps.Scanner.Analyzers.Lang.Rust/TASKS.md`. Focus on SCANNER-ANALYZERS-LANG-10-305C (TODO), SCANNER-ANALYZERS-LANG-10-304C (TODO), SCANNER-ANALYZERS-LANG-10-309N (TODO), SCANNER-ANALYZERS-LANG-10-303C (TODO), SCANNER-ANALYZERS-LANG-10-306C (TODO). Confirm prerequisites (internal: SCANNER-ANALYZERS-LANG-10-303B (Wave 2), SCANNER-ANALYZERS-LANG-10-304B (Wave 2), SCANNER-ANALYZERS-LANG-10-305B (Wave 2), SCANNER-ANALYZERS-LANG-10-306B (Wave 2), SCANNER-ANALYZERS-LANG-10-308N (Wave 2)) before starting and report status in module TASKS.md.
|
- Team TBD: read EXECPLAN.md Wave 3 and SPRINTS.md rows for `src/StellaOps.Scanner.Analyzers.Lang.DotNet/TASKS.md`, `src/StellaOps.Scanner.Analyzers.Lang.Go/TASKS.md`, `src/StellaOps.Scanner.Analyzers.Lang.Node/TASKS.md`, `src/StellaOps.Scanner.Analyzers.Lang.Python/TASKS.md`, `src/StellaOps.Scanner.Analyzers.Lang.Rust/TASKS.md`. Focus on SCANNER-ANALYZERS-LANG-10-305C (TODO), SCANNER-ANALYZERS-LANG-10-304C (TODO), SCANNER-ANALYZERS-LANG-10-309N (TODO), SCANNER-ANALYZERS-LANG-10-303C (DONE 2025-10-21), SCANNER-ANALYZERS-LANG-10-306C (TODO). Confirm prerequisites (internal: SCANNER-ANALYZERS-LANG-10-303B (Wave 2), SCANNER-ANALYZERS-LANG-10-304B (Wave 2), SCANNER-ANALYZERS-LANG-10-305B (Wave 2), SCANNER-ANALYZERS-LANG-10-306B (Wave 2), SCANNER-ANALYZERS-LANG-10-308N (Wave 2)) before starting and report status in module TASKS.md.
|
||||||
- Team Zastava Observer Guild: read EXECPLAN.md Wave 3 and SPRINTS.md rows for `src/StellaOps.Zastava.Observer/TASKS.md`. Focus on ZASTAVA-OBS-12-003 (TODO), ZASTAVA-OBS-12-004 (TODO), ZASTAVA-OBS-17-005 (TODO). Confirm prerequisites (internal: ZASTAVA-OBS-12-002 (Wave 2)) before starting and report status in module TASKS.md.
|
- Team Zastava Observer Guild: read EXECPLAN.md Wave 3 and SPRINTS.md rows for `src/StellaOps.Zastava.Observer/TASKS.md`. Focus on ZASTAVA-OBS-12-003 (TODO), ZASTAVA-OBS-12-004 (TODO), ZASTAVA-OBS-17-005 (TODO). Confirm prerequisites (internal: ZASTAVA-OBS-12-002 (Wave 2)) before starting and report status in module TASKS.md.
|
||||||
|
|
||||||
### Wave 4
|
### Wave 4
|
||||||
- Team DevEx/CLI: read EXECPLAN.md Wave 4 and SPRINTS.md rows for `src/StellaOps.Cli/TASKS.md`. Focus on CLI-PLUGIN-13-007 (TODO). Confirm prerequisites (internal: CLI-OFFLINE-13-006 (Wave 3), CLI-RUNTIME-13-005 (Wave 0)) before starting and report status in module TASKS.md.
|
- Team DevEx/CLI: read EXECPLAN.md Wave 4 and SPRINTS.md rows for `src/StellaOps.Cli/TASKS.md`. Focus on CLI-PLUGIN-13-007 (DONE 2025-10-22). Confirm prerequisites (internal: CLI-OFFLINE-13-006 (Wave 3), CLI-RUNTIME-13-005 (Wave 0)) before starting and report status in module TASKS.md.
|
||||||
- Team Docs Guild: read EXECPLAN.md Wave 4 and SPRINTS.md rows for `docs/TASKS.md`. Focus on DOCS-RUNTIME-17-004 (TODO). Confirm prerequisites (internal: DEVOPS-REL-17-002 (Wave 2), SCANNER-EMIT-17-701 (Wave 1), ZASTAVA-OBS-17-005 (Wave 3)) before starting and report status in module TASKS.md.
|
- Team Docs Guild: read EXECPLAN.md Wave 4 and SPRINTS.md rows for `docs/TASKS.md`. Focus on DOCS-RUNTIME-17-004 (TODO). Confirm prerequisites (internal: DEVOPS-REL-17-002 (Wave 2), SCANNER-EMIT-17-701 (Wave 1), ZASTAVA-OBS-17-005 (Wave 3)) before starting and report status in module TASKS.md.
|
||||||
- Team Excititor Connectors – Stella: read EXECPLAN.md Wave 4 and SPRINTS.md rows for `src/StellaOps.Excititor.Connectors.StellaOpsMirror/TASKS.md`. Focus on EXCITITOR-CONN-STELLA-07-002 (TODO). Confirm prerequisites (internal: EXCITITOR-CONN-STELLA-07-001 (Wave 3)) before starting and report status in module TASKS.md.
|
- Team Excititor Connectors – Stella: read EXECPLAN.md Wave 4 and SPRINTS.md rows for `src/StellaOps.Excititor.Connectors.StellaOpsMirror/TASKS.md`. Focus on EXCITITOR-CONN-STELLA-07-002 (TODO). Confirm prerequisites (internal: EXCITITOR-CONN-STELLA-07-001 (Wave 3)) before starting and report status in module TASKS.md.
|
||||||
- Team Notify Connectors Guild: read EXECPLAN.md Wave 4 and SPRINTS.md rows for `src/StellaOps.Notify.Connectors.Email/TASKS.md`, `src/StellaOps.Notify.Connectors.Slack/TASKS.md`, `src/StellaOps.Notify.Connectors.Teams/TASKS.md`, `src/StellaOps.Notify.Connectors.Webhook/TASKS.md`. Focus on NOTIFY-CONN-SLACK-15-501 (TODO), NOTIFY-CONN-TEAMS-15-601 (TODO), NOTIFY-CONN-EMAIL-15-701 (TODO), NOTIFY-CONN-WEBHOOK-15-801 (TODO). Confirm prerequisites (internal: NOTIFY-ENGINE-15-303 (Wave 3)) before starting and report status in module TASKS.md.
|
- Team Notify Connectors Guild: read EXECPLAN.md Wave 4 and SPRINTS.md rows for `src/StellaOps.Notify.Connectors.Email/TASKS.md`, `src/StellaOps.Notify.Connectors.Slack/TASKS.md`, `src/StellaOps.Notify.Connectors.Teams/TASKS.md`, `src/StellaOps.Notify.Connectors.Webhook/TASKS.md`. Focus on NOTIFY-CONN-SLACK-15-501 (TODO), NOTIFY-CONN-TEAMS-15-601 (TODO), NOTIFY-CONN-EMAIL-15-701 (TODO), NOTIFY-CONN-WEBHOOK-15-801 (TODO). Confirm prerequisites (internal: NOTIFY-ENGINE-15-303 (Wave 3)) before starting and report status in module TASKS.md.
|
||||||
@@ -169,10 +169,10 @@ Generated from SPRINTS.md and module TASKS.md files on 2025-10-19. Waves cluster
|
|||||||
- Path: `src/StellaOps.Web/TASKS.md`
|
- Path: `src/StellaOps.Web/TASKS.md`
|
||||||
2. [DONE 2025-10-21] WEB1.TRIVY-SETTINGS-TESTS — Add headless UI test run (`ng test --watch=false`) and document prerequisites once Angular tooling is chained up.
|
2. [DONE 2025-10-21] WEB1.TRIVY-SETTINGS-TESTS — Add headless UI test run (`ng test --watch=false`) and document prerequisites once Angular tooling is chained up.
|
||||||
• Prereqs: WEB1.TRIVY-SETTINGS
|
• Prereqs: WEB1.TRIVY-SETTINGS
|
||||||
• Current: DONE (2025-10-21) – ChromeHeadless launcher + README updates merged; awaiting dependency hardening follow-up (WEB1.DEPS-13-001).
|
• Current: DONE (2025-10-21) – ChromeHeadless launcher + README updates merged; dependency hardening completed via WEB1.DEPS-13-001.
|
||||||
3. [TODO] WEB1.DEPS-13-001 — Stabilise Angular workspace dependencies for headless CI installs (`npm install`, Chromium handling, docs).
|
3. [DONE (2025-10-21)] WEB1.DEPS-13-001 — Stabilise Angular workspace dependencies for headless CI installs (`npm install`, Chromium handling, docs).
|
||||||
• Prereqs: WEB1.TRIVY-SETTINGS-TESTS
|
• Prereqs: WEB1.TRIVY-SETTINGS-TESTS
|
||||||
• Current: TODO – Capture deterministic lockfile flow, cache Puppeteer downloads, and validate `npm test` from clean checkout in air-gapped mode.
|
• Current: DONE (2025-10-21) – Lockfile generated via `npm ci`, Chromium auto-detection/verification scripts added, and deterministic install guide published for offline runners.
|
||||||
- **Sprint 1** · Developer Tooling
|
- **Sprint 1** · Developer Tooling
|
||||||
- Team: DevEx/CLI
|
- Team: DevEx/CLI
|
||||||
- Path: `src/StellaOps.Cli/TASKS.md`
|
- Path: `src/StellaOps.Cli/TASKS.md`
|
||||||
@@ -311,9 +311,9 @@ Generated from SPRINTS.md and module TASKS.md files on 2025-10-19. Waves cluster
|
|||||||
- **Sprint 10** · Backlog
|
- **Sprint 10** · Backlog
|
||||||
- Team: TBD
|
- Team: TBD
|
||||||
- Path: `src/StellaOps.Scanner.Analyzers.Lang.Node/TASKS.md`
|
- Path: `src/StellaOps.Scanner.Analyzers.Lang.Node/TASKS.md`
|
||||||
1. [TODO] SCANNER-ANALYZERS-LANG-10-302C — Surface script metadata (postinstall/preinstall) and policy hints; emit telemetry counters and evidence records.
|
1. [DONE 2025-10-19] SCANNER-ANALYZERS-LANG-10-302C — Surface script metadata (postinstall/preinstall) and policy hints; emit telemetry counters and evidence records.
|
||||||
• Prereqs: SCANNER-ANALYZERS-LANG-10-302B (external/completed)
|
• Prereqs: SCANNER-ANALYZERS-LANG-10-302B (external/completed)
|
||||||
• Current: TODO
|
• Current: DONE — Telemetry counter wired, lifecycle script evidence emitted; see Node analyzer fixtures.
|
||||||
- **Sprint 10** · Scanner Analyzers & SBOM
|
- **Sprint 10** · Scanner Analyzers & SBOM
|
||||||
- Team: Diff Guild
|
- Team: Diff Guild
|
||||||
- Path: `src/StellaOps.Scanner.Diff/TASKS.md`
|
- Path: `src/StellaOps.Scanner.Diff/TASKS.md`
|
||||||
@@ -375,15 +375,15 @@ Generated from SPRINTS.md and module TASKS.md files on 2025-10-19. Waves cluster
|
|||||||
• Prereqs: —
|
• Prereqs: —
|
||||||
• Current: TODO
|
• Current: TODO
|
||||||
- Path: `src/StellaOps.Scanner.Analyzers.Lang/TASKS.md`
|
- Path: `src/StellaOps.Scanner.Analyzers.Lang/TASKS.md`
|
||||||
1. [TODO] SCANNER-ANALYZERS-LANG-10-301 — Java analyzer emitting `pkg:maven` with provenance.
|
1. [DONE 2025-10-19] SCANNER-ANALYZERS-LANG-10-301 — Java analyzer emitting `pkg:maven` with provenance.
|
||||||
• Prereqs: —
|
• Prereqs: —
|
||||||
• Current: TODO
|
• Current: DONE — Java analyzer shipped with deterministic fixtures.
|
||||||
2. [TODO] SCANNER-ANALYZERS-LANG-10-307 — Shared language evidence helpers + usage flag propagation.
|
2. [DONE 2025-10-19] SCANNER-ANALYZERS-LANG-10-307 — Shared language evidence helpers + usage flag propagation.
|
||||||
• Prereqs: —
|
• Prereqs: —
|
||||||
• Current: TODO
|
• Current: DONE — Shared helpers live under Lang.Core and are consumed by Java/Node analyzers.
|
||||||
3. [TODO] SCANNER-ANALYZERS-LANG-10-308 — Determinism + fixture harness for language analyzers.
|
3. [DONE 2025-10-19] SCANNER-ANALYZERS-LANG-10-308 — Determinism + fixture harness for language analyzers.
|
||||||
• Prereqs: —
|
• Prereqs: —
|
||||||
• Current: TODO
|
• Current: DONE — Determinism harness + fixtures checked in; CI guard active.
|
||||||
- **Sprint 11** · Signing Chain Bring-up
|
- **Sprint 11** · Signing Chain Bring-up
|
||||||
- Team: Attestor Guild
|
- Team: Attestor Guild
|
||||||
- Path: `src/StellaOps.Attestor/TASKS.md`
|
- Path: `src/StellaOps.Attestor/TASKS.md`
|
||||||
@@ -494,6 +494,11 @@ Generated from SPRINTS.md and module TASKS.md files on 2025-10-19. Waves cluster
|
|||||||
3. [TODO] DEVOPS-LAUNCH-18-001 - Production launch cutover rehearsal and runbook publication.
|
3. [TODO] DEVOPS-LAUNCH-18-001 - Production launch cutover rehearsal and runbook publication.
|
||||||
• Prereqs: DEVOPS-LAUNCH-18-100, DEVOPS-LAUNCH-18-900
|
• Prereqs: DEVOPS-LAUNCH-18-100, DEVOPS-LAUNCH-18-900
|
||||||
• Current: TODO
|
• Current: TODO
|
||||||
|
- Team: Offline Kit Guild, UX Specialist
|
||||||
|
- Path: `ops/offline-kit/TASKS.md`
|
||||||
|
1. [TODO] DEVOPS-OFFLINE-18-003 — Capture Angular workspace npm cache + Chromium bundle for Offline Kit distribution and document refresh cadence.
|
||||||
|
• Prereqs: DEVOPS-OFFLINE-14-002 (Wave 2)
|
||||||
|
• Current: TODO
|
||||||
|
|
||||||
## Wave 1 — 45 task(s) ready after Wave 0
|
## Wave 1 — 45 task(s) ready after Wave 0
|
||||||
- **Sprint 6** · Excititor Ingest & Formats
|
- **Sprint 6** · Excititor Ingest & Formats
|
||||||
@@ -531,21 +536,21 @@ Generated from SPRINTS.md and module TASKS.md files on 2025-10-19. Waves cluster
|
|||||||
- **Sprint 10** · Backlog
|
- **Sprint 10** · Backlog
|
||||||
- Team: TBD
|
- Team: TBD
|
||||||
- Path: `src/StellaOps.Scanner.Analyzers.Lang.DotNet/TASKS.md`
|
- Path: `src/StellaOps.Scanner.Analyzers.Lang.DotNet/TASKS.md`
|
||||||
1. [TODO] SCANNER-ANALYZERS-LANG-10-305A — Parse `*.deps.json` + `runtimeconfig.json`, build RID graph, and normalize to `pkg:nuget` components.
|
1. [DONE 2025-10-22] SCANNER-ANALYZERS-LANG-10-305A — Parse `*.deps.json` + `runtimeconfig.json`, build RID graph, and normalize to `pkg:nuget` components.
|
||||||
• Prereqs: SCANNER-ANALYZERS-LANG-10-307 (Wave 0)
|
• Prereqs: SCANNER-ANALYZERS-LANG-10-307 (Wave 0)
|
||||||
• Current: TODO
|
• Current: DONE — RID-aware deps/runtimeconfig parser emitting deterministic NuGet components with tests landed.
|
||||||
- Path: `src/StellaOps.Scanner.Analyzers.Lang.Go/TASKS.md`
|
- Path: `src/StellaOps.Scanner.Analyzers.Lang.Go/TASKS.md`
|
||||||
1. [TODO] SCANNER-ANALYZERS-LANG-10-304A — Parse Go build info blob (`runtime/debug` format) and `.note.go.buildid`; map to module/version and evidence.
|
1. [DONE 2025-10-22] SCANNER-ANALYZERS-LANG-10-304A — Parse Go build info blob (`runtime/debug` format) and `.note.go.buildid`; map to module/version and evidence.
|
||||||
• Prereqs: SCANNER-ANALYZERS-LANG-10-307 (Wave 0)
|
• Prereqs: SCANNER-ANALYZERS-LANG-10-307 (Wave 0)
|
||||||
• Current: TODO
|
• Current: DONE – Varint build-info decoder implemented with fixtures and determinism harness coverage.
|
||||||
- Path: `src/StellaOps.Scanner.Analyzers.Lang.Node/TASKS.md`
|
- Path: `src/StellaOps.Scanner.Analyzers.Lang.Node/TASKS.md`
|
||||||
1. [TODO] SCANNER-ANALYZERS-LANG-10-307N — Integrate shared helpers for license/licence evidence, canonical JSON serialization, and usage flag propagation.
|
1. [DONE 2025-10-21] SCANNER-ANALYZERS-LANG-10-307N — Integrate shared helpers for license/licence evidence, canonical JSON serialization, and usage flag propagation.
|
||||||
• Prereqs: SCANNER-ANALYZERS-LANG-10-302C (Wave 0)
|
• Prereqs: SCANNER-ANALYZERS-LANG-10-302C (Wave 0)
|
||||||
• Current: TODO
|
• Current: DONE — Node analyzer now reuses shared metadata/evidence helpers.
|
||||||
- Path: `src/StellaOps.Scanner.Analyzers.Lang.Python/TASKS.md`
|
- Path: `src/StellaOps.Scanner.Analyzers.Lang.Python/TASKS.md`
|
||||||
1. [TODO] SCANNER-ANALYZERS-LANG-10-303A — STREAM-based parser for `*.dist-info` (`METADATA`, `WHEEL`, `entry_points.txt`) with normalization + evidence capture.
|
1. [DONE 2025-10-21] SCANNER-ANALYZERS-LANG-10-303A — STREAM-based parser for `*.dist-info` (`METADATA`, `WHEEL`, `entry_points.txt`) with normalization + evidence capture.
|
||||||
• Prereqs: SCANNER-ANALYZERS-LANG-10-307 (Wave 0)
|
• Prereqs: SCANNER-ANALYZERS-LANG-10-307 (Wave 0)
|
||||||
• Current: TODO
|
• Current: DONE — Python analyzer ingests METADATA/WHEEL/entry_points with deterministic ordering and UTF-8 normalization. Fixtures updated (`simple-venv`).
|
||||||
- Path: `src/StellaOps.Scanner.Analyzers.Lang.Rust/TASKS.md`
|
- Path: `src/StellaOps.Scanner.Analyzers.Lang.Rust/TASKS.md`
|
||||||
1. [TODO] SCANNER-ANALYZERS-LANG-10-306A — Parse Cargo metadata (`Cargo.lock`, `.fingerprint`, `.metadata`) and map crates to components with evidence.
|
1. [TODO] SCANNER-ANALYZERS-LANG-10-306A — Parse Cargo metadata (`Cargo.lock`, `.fingerprint`, `.metadata`) and map crates to components with evidence.
|
||||||
• Prereqs: SCANNER-ANALYZERS-LANG-10-307 (Wave 0)
|
• Prereqs: SCANNER-ANALYZERS-LANG-10-307 (Wave 0)
|
||||||
@@ -558,24 +563,24 @@ Generated from SPRINTS.md and module TASKS.md files on 2025-10-19. Waves cluster
|
|||||||
• Current: TODO
|
• Current: TODO
|
||||||
- Team: Language Analyzer Guild
|
- Team: Language Analyzer Guild
|
||||||
- Path: `src/StellaOps.Scanner.Analyzers.Lang/TASKS.md`
|
- Path: `src/StellaOps.Scanner.Analyzers.Lang/TASKS.md`
|
||||||
1. [DOING] SCANNER-ANALYZERS-LANG-10-309 — Package language analyzers as restart-time plug-ins (manifest + host registration).
|
1. [DONE 2025-10-21] SCANNER-ANALYZERS-LANG-10-309 — Package language analyzers as restart-time plug-ins (manifest + host registration).
|
||||||
• Prereqs: SCANNER-ANALYZERS-LANG-10-301 (Wave 0)
|
• Prereqs: SCANNER-ANALYZERS-LANG-10-301 (Wave 0)
|
||||||
• Current: DOING (2025-10-19)
|
• Current: DONE — Manifest published under `plugins/scanner/analyzers/lang/`, Worker loader wired, integration tests updated.
|
||||||
2. [TODO] SCANNER-ANALYZERS-LANG-10-306 — Rust analyzer detecting crate provenance or falling back to `bin:{sha256}`.
|
2. [TODO] SCANNER-ANALYZERS-LANG-10-306 — Rust analyzer detecting crate provenance or falling back to `bin:{sha256}`.
|
||||||
• Prereqs: SCANNER-ANALYZERS-LANG-10-307 (Wave 0)
|
• Prereqs: SCANNER-ANALYZERS-LANG-10-307 (Wave 0)
|
||||||
• Current: TODO
|
• Current: TODO
|
||||||
3. [DOING] SCANNER-ANALYZERS-LANG-10-302 — Node analyzer resolving workspaces/symlinks into `pkg:npm` identities.
|
3. [DONE 2025-10-21] SCANNER-ANALYZERS-LANG-10-302 — Node analyzer resolving workspaces/symlinks into `pkg:npm` identities.
|
||||||
• Prereqs: SCANNER-ANALYZERS-LANG-10-307 (Wave 0)
|
• Prereqs: SCANNER-ANALYZERS-LANG-10-307 (Wave 0)
|
||||||
• Current: DOING (2025-10-19)
|
• Current: DONE — Workspace/symlink coverage validated via determinism fixtures; metrics + lifecycle script evidence landed.
|
||||||
4. [TODO] SCANNER-ANALYZERS-LANG-10-304 — Go analyzer leveraging buildinfo for `pkg:golang` components.
|
4. [DOING 2025-10-22] SCANNER-ANALYZERS-LANG-10-304 — Go analyzer leveraging buildinfo for `pkg:golang` components.
|
||||||
• Prereqs: SCANNER-ANALYZERS-LANG-10-307 (Wave 0)
|
• Prereqs: SCANNER-ANALYZERS-LANG-10-307 (Wave 0)
|
||||||
• Current: TODO
|
• Current: TODO
|
||||||
5. [TODO] SCANNER-ANALYZERS-LANG-10-305 — .NET analyzer parsing `*.deps.json`, assembly metadata, and RID variants.
|
5. [DOING 2025-10-22] SCANNER-ANALYZERS-LANG-10-305 — .NET analyzer parsing `*.deps.json`, assembly metadata, and RID variants.
|
||||||
• Prereqs: SCANNER-ANALYZERS-LANG-10-307 (Wave 0)
|
• Prereqs: SCANNER-ANALYZERS-LANG-10-307 (Wave 0)
|
||||||
• Current: TODO
|
• Current: DOING — Implementing initial deps/runtimeconfig parsing for RID-aware components.
|
||||||
6. [TODO] SCANNER-ANALYZERS-LANG-10-303 — Python analyzer consuming `*.dist-info` metadata and RECORD hashes.
|
6. [DONE 2025-10-21] SCANNER-ANALYZERS-LANG-10-303 — Python analyzer consuming `*.dist-info` metadata and RECORD hashes.
|
||||||
• Prereqs: SCANNER-ANALYZERS-LANG-10-307 (Wave 0)
|
• Prereqs: SCANNER-ANALYZERS-LANG-10-307 (Wave 0)
|
||||||
• Current: TODO
|
• Current: DONE — Dist-info parser, RECORD verifier, editable install metadata, and entrypoint usage hints shipped with deterministic fixture/tests.
|
||||||
- **Sprint 11** · UI Integration
|
- **Sprint 11** · UI Integration
|
||||||
- Team: UI Guild
|
- Team: UI Guild
|
||||||
- Path: `src/StellaOps.UI/TASKS.md`
|
- Path: `src/StellaOps.UI/TASKS.md`
|
||||||
@@ -604,7 +609,7 @@ Generated from SPRINTS.md and module TASKS.md files on 2025-10-19. Waves cluster
|
|||||||
• Current: TODO – Build Spectre test harness exercising `runtime policy test` against a stubbed backend to lock output shape (table + `--json`) and guard regressions. Integrate into `dotnet test` suite.
|
• Current: TODO – Build Spectre test harness exercising `runtime policy test` against a stubbed backend to lock output shape (table + `--json`) and guard regressions. Integrate into `dotnet test` suite.
|
||||||
- Team: UX Specialist, Angular Eng, DevEx
|
- Team: UX Specialist, Angular Eng, DevEx
|
||||||
- Path: `src/StellaOps.Web/TASKS.md`
|
- Path: `src/StellaOps.Web/TASKS.md`
|
||||||
1. [TODO] WEB1.DEPS-13-001 — Stabilise Angular workspace dependencies for headless CI installs (`npm install`, Chromium handling, docs).
|
1. [DONE (2025-10-21)] WEB1.DEPS-13-001 — Stabilise Angular workspace dependencies for headless CI installs (`npm install`, Chromium handling, docs).
|
||||||
• Prereqs: WEB1.TRIVY-SETTINGS-TESTS (Wave 0)
|
• Prereqs: WEB1.TRIVY-SETTINGS-TESTS (Wave 0)
|
||||||
• Current: TODO – Capture deterministic lockfile flow, cache Puppeteer downloads, validate `npm test` from clean checkout offline, and update README.
|
• Current: TODO – Capture deterministic lockfile flow, cache Puppeteer downloads, validate `npm test` from clean checkout offline, and update README.
|
||||||
- Team: UI Guild
|
- Team: UI Guild
|
||||||
@@ -720,17 +725,17 @@ Generated from SPRINTS.md and module TASKS.md files on 2025-10-19. Waves cluster
|
|||||||
• Prereqs: SCANNER-ANALYZERS-LANG-10-305A (Wave 1)
|
• Prereqs: SCANNER-ANALYZERS-LANG-10-305A (Wave 1)
|
||||||
• Current: TODO
|
• Current: TODO
|
||||||
- Path: `src/StellaOps.Scanner.Analyzers.Lang.Go/TASKS.md`
|
- Path: `src/StellaOps.Scanner.Analyzers.Lang.Go/TASKS.md`
|
||||||
1. [TODO] SCANNER-ANALYZERS-LANG-10-304B — Implement DWARF-lite reader for VCS metadata + dirty flag; add cache to avoid re-reading identical binaries.
|
1. [DONE 2025-10-22] SCANNER-ANALYZERS-LANG-10-304B — Implement DWARF-lite reader for VCS metadata + dirty flag; add cache to avoid re-reading identical binaries.
|
||||||
• Prereqs: SCANNER-ANALYZERS-LANG-10-304A (Wave 1)
|
• Prereqs: SCANNER-ANALYZERS-LANG-10-304A (Wave 1)
|
||||||
• Current: TODO
|
• Current: DONE — DWARF fallback parses vcs.* markers, cache reuses metadata keyed by file identity.
|
||||||
- Path: `src/StellaOps.Scanner.Analyzers.Lang.Node/TASKS.md`
|
- Path: `src/StellaOps.Scanner.Analyzers.Lang.Node/TASKS.md`
|
||||||
1. [TODO] SCANNER-ANALYZERS-LANG-10-308N — Author determinism harness + fixtures for Node analyzer; add benchmark suite.
|
1. [DONE 2025-10-21] SCANNER-ANALYZERS-LANG-10-308N — Author determinism harness + fixtures for Node analyzer; add benchmark suite.
|
||||||
• Prereqs: SCANNER-ANALYZERS-LANG-10-307N (Wave 1)
|
• Prereqs: SCANNER-ANALYZERS-LANG-10-307N (Wave 1)
|
||||||
• Current: TODO
|
• Current: DONE — Harness + fixtures merged; benchmark CSV recorded under `bench/Scanner.Analyzers`.
|
||||||
- Path: `src/StellaOps.Scanner.Analyzers.Lang.Python/TASKS.md`
|
- Path: `src/StellaOps.Scanner.Analyzers.Lang.Python/TASKS.md`
|
||||||
1. [TODO] SCANNER-ANALYZERS-LANG-10-303B — RECORD hash verifier with chunked hashing, Zip64 support, and mismatch diagnostics.
|
1. [DONE 2025-10-21] SCANNER-ANALYZERS-LANG-10-303B — RECORD hash verifier with chunked hashing, Zip64 support, and mismatch diagnostics.
|
||||||
• Prereqs: SCANNER-ANALYZERS-LANG-10-303A (Wave 1)
|
• Prereqs: SCANNER-ANALYZERS-LANG-10-303A (Wave 1)
|
||||||
• Current: TODO
|
• Current: DONE — Streaming SHA-256 verification with deterministic mismatch evidence; unsupported algorithms tracked; fixtures validated.
|
||||||
- Path: `src/StellaOps.Scanner.Analyzers.Lang.Rust/TASKS.md`
|
- Path: `src/StellaOps.Scanner.Analyzers.Lang.Rust/TASKS.md`
|
||||||
1. [TODO] SCANNER-ANALYZERS-LANG-10-306B — Implement heuristic classifier using ELF section names, symbol mangling, and `.comment` data for stripped binaries.
|
1. [TODO] SCANNER-ANALYZERS-LANG-10-306B — Implement heuristic classifier using ELF section names, symbol mangling, and `.comment` data for stripped binaries.
|
||||||
• Prereqs: SCANNER-ANALYZERS-LANG-10-306A (Wave 1)
|
• Prereqs: SCANNER-ANALYZERS-LANG-10-306A (Wave 1)
|
||||||
@@ -855,13 +860,13 @@ Generated from SPRINTS.md and module TASKS.md files on 2025-10-19. Waves cluster
|
|||||||
• Prereqs: SCANNER-ANALYZERS-LANG-10-304B (Wave 2)
|
• Prereqs: SCANNER-ANALYZERS-LANG-10-304B (Wave 2)
|
||||||
• Current: TODO
|
• Current: TODO
|
||||||
- Path: `src/StellaOps.Scanner.Analyzers.Lang.Node/TASKS.md`
|
- Path: `src/StellaOps.Scanner.Analyzers.Lang.Node/TASKS.md`
|
||||||
1. [TODO] SCANNER-ANALYZERS-LANG-10-309N — Package Node analyzer as restart-time plug-in (manifest, DI registration, Offline Kit notes).
|
1. [DONE 2025-10-21] SCANNER-ANALYZERS-LANG-10-309N — Package Node analyzer as restart-time plug-in (manifest, DI registration, Offline Kit notes).
|
||||||
• Prereqs: SCANNER-ANALYZERS-LANG-10-308N (Wave 2)
|
• Prereqs: SCANNER-ANALYZERS-LANG-10-308N (Wave 2)
|
||||||
• Current: TODO
|
• Current: DONE — Manifest shipped, Worker catalog integration complete, Offline Kit docs updated.
|
||||||
- Path: `src/StellaOps.Scanner.Analyzers.Lang.Python/TASKS.md`
|
- Path: `src/StellaOps.Scanner.Analyzers.Lang.Python/TASKS.md`
|
||||||
1. [TODO] SCANNER-ANALYZERS-LANG-10-303C — Editable install + pip cache detection; integrate EntryTrace hints for runtime usage flags.
|
1. [DONE 2025-10-21] SCANNER-ANALYZERS-LANG-10-303C — Editable install + pip cache detection; integrate EntryTrace hints for runtime usage flags.
|
||||||
• Prereqs: SCANNER-ANALYZERS-LANG-10-303B (Wave 2)
|
• Prereqs: SCANNER-ANALYZERS-LANG-10-303B (Wave 2)
|
||||||
• Current: TODO
|
• Current: DONE — `direct_url.json` editable insights surfaced; EntryTrace usage hints mark console scripts; deterministic fixture covers editable vs wheel installs.
|
||||||
- Path: `src/StellaOps.Scanner.Analyzers.Lang.Rust/TASKS.md`
|
- Path: `src/StellaOps.Scanner.Analyzers.Lang.Rust/TASKS.md`
|
||||||
1. [TODO] SCANNER-ANALYZERS-LANG-10-306C — Integrate binary hash fallback (`bin:{sha256}`) and tie into shared quiet provenance helpers.
|
1. [TODO] SCANNER-ANALYZERS-LANG-10-306C — Integrate binary hash fallback (`bin:{sha256}`) and tie into shared quiet provenance helpers.
|
||||||
• Prereqs: SCANNER-ANALYZERS-LANG-10-306B (Wave 2)
|
• Prereqs: SCANNER-ANALYZERS-LANG-10-306B (Wave 2)
|
||||||
@@ -940,7 +945,7 @@ Generated from SPRINTS.md and module TASKS.md files on 2025-10-19. Waves cluster
|
|||||||
- **Sprint 13** · UX & CLI Experience
|
- **Sprint 13** · UX & CLI Experience
|
||||||
- Team: DevEx/CLI
|
- Team: DevEx/CLI
|
||||||
- Path: `src/StellaOps.Cli/TASKS.md`
|
- Path: `src/StellaOps.Cli/TASKS.md`
|
||||||
1. [TODO] CLI-PLUGIN-13-007 — CLI-PLUGIN-13-007 – Plugin packaging
|
1. [DONE 2025-10-22] CLI-PLUGIN-13-007 — CLI-PLUGIN-13-007 – Plugin packaging
|
||||||
• Prereqs: CLI-RUNTIME-13-005 (Wave 0), CLI-OFFLINE-13-006 (Wave 3)
|
• Prereqs: CLI-RUNTIME-13-005 (Wave 0), CLI-OFFLINE-13-006 (Wave 3)
|
||||||
• Current: TODO – Package non-core verbs as restart-time plug-ins (manifest + loader updates, tests ensuring no hot reload).
|
• Current: TODO – Package non-core verbs as restart-time plug-ins (manifest + loader updates, tests ensuring no hot reload).
|
||||||
- **Sprint 15** · Notify Foundations
|
- **Sprint 15** · Notify Foundations
|
||||||
|
|||||||
19
SPRINTS.md
19
SPRINTS.md
@@ -14,14 +14,14 @@ This file describe implementation of Stella Ops (docs/README.md). Implementation
|
|||||||
| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Analyzers.OS/TASKS.md | TODO | OS Analyzer Guild | SCANNER-ANALYZERS-OS-10-206 | Determinism harness + fixtures for OS analyzers. |
|
| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Analyzers.OS/TASKS.md | TODO | OS Analyzer Guild | SCANNER-ANALYZERS-OS-10-206 | Determinism harness + fixtures for OS analyzers. |
|
||||||
| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Analyzers.OS/TASKS.md | TODO | OS Analyzer Guild | SCANNER-ANALYZERS-OS-10-207 | Package OS analyzers as restart-time plug-ins (manifest + host registration). |
|
| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Analyzers.OS/TASKS.md | TODO | OS Analyzer Guild | SCANNER-ANALYZERS-OS-10-207 | Package OS analyzers as restart-time plug-ins (manifest + host registration). |
|
||||||
| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Analyzers.Lang/TASKS.md | TODO | Language Analyzer Guild | SCANNER-ANALYZERS-LANG-10-301 | Java analyzer emitting `pkg:maven` with provenance. |
|
| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Analyzers.Lang/TASKS.md | TODO | Language Analyzer Guild | SCANNER-ANALYZERS-LANG-10-301 | Java analyzer emitting `pkg:maven` with provenance. |
|
||||||
| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Analyzers.Lang/TASKS.md | TODO | Language Analyzer Guild | SCANNER-ANALYZERS-LANG-10-302 | Node analyzer handling workspaces/symlinks emitting `pkg:npm`. |
|
| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Analyzers.Lang/TASKS.md | DONE (2025-10-21) | Language Analyzer Guild | SCANNER-ANALYZERS-LANG-10-302 | Node analyzer handling workspaces/symlinks emitting `pkg:npm`. |
|
||||||
| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Analyzers.Lang/TASKS.md | TODO | Language Analyzer Guild | SCANNER-ANALYZERS-LANG-10-303 | Python analyzer reading `*.dist-info`, RECORD hashes, entry points. |
|
| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Analyzers.Lang/TASKS.md | DONE (2025-10-21) | Language Analyzer Guild | SCANNER-ANALYZERS-LANG-10-303 | Python analyzer reading `*.dist-info`, RECORD hashes, entry points. |
|
||||||
| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Analyzers.Lang/TASKS.md | TODO | Language Analyzer Guild | SCANNER-ANALYZERS-LANG-10-304 | Go analyzer leveraging buildinfo for `pkg:golang` components. |
|
| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Analyzers.Lang/TASKS.md | DOING (2025-10-22) | Language Analyzer Guild | SCANNER-ANALYZERS-LANG-10-304 | Go analyzer leveraging buildinfo for `pkg:golang` components. |
|
||||||
| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Analyzers.Lang/TASKS.md | TODO | Language Analyzer Guild | SCANNER-ANALYZERS-LANG-10-305 | .NET analyzer parsing `*.deps.json`, assembly metadata, RID variants. |
|
| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Analyzers.Lang/TASKS.md | DOING (2025-10-22) | Language Analyzer Guild | SCANNER-ANALYZERS-LANG-10-305 | .NET analyzer parsing `*.deps.json`, assembly metadata, RID variants. |
|
||||||
| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Analyzers.Lang/TASKS.md | TODO | Language Analyzer Guild | SCANNER-ANALYZERS-LANG-10-306 | Rust analyzer detecting crates or falling back to `bin:{sha256}`. |
|
| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Analyzers.Lang/TASKS.md | TODO | Language Analyzer Guild | SCANNER-ANALYZERS-LANG-10-306 | Rust analyzer detecting crates or falling back to `bin:{sha256}`. |
|
||||||
| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Analyzers.Lang/TASKS.md | TODO | Language Analyzer Guild | SCANNER-ANALYZERS-LANG-10-307 | Shared language evidence helpers + usage flag propagation. |
|
| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Analyzers.Lang/TASKS.md | DONE (2025-10-19) | Language Analyzer Guild | SCANNER-ANALYZERS-LANG-10-307 | Shared language evidence helpers + usage flag propagation. |
|
||||||
| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Analyzers.Lang/TASKS.md | TODO | Language Analyzer Guild | SCANNER-ANALYZERS-LANG-10-308 | Determinism + fixture harness for language analyzers. |
|
| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Analyzers.Lang/TASKS.md | DONE (2025-10-19) | Language Analyzer Guild | SCANNER-ANALYZERS-LANG-10-308 | Determinism + fixture harness for language analyzers. |
|
||||||
| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Analyzers.Lang/TASKS.md | TODO | Language Analyzer Guild | SCANNER-ANALYZERS-LANG-10-309 | Package language analyzers as restart-time plug-ins (manifest + host registration). |
|
| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Analyzers.Lang/TASKS.md | DONE (2025-10-21) | Language Analyzer Guild | SCANNER-ANALYZERS-LANG-10-309 | Package language analyzers as restart-time plug-ins (manifest + host registration). |
|
||||||
| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.EntryTrace/TASKS.md | TODO | EntryTrace Guild | SCANNER-ENTRYTRACE-10-401 | POSIX shell AST parser with deterministic output. |
|
| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.EntryTrace/TASKS.md | TODO | EntryTrace Guild | SCANNER-ENTRYTRACE-10-401 | POSIX shell AST parser with deterministic output. |
|
||||||
| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.EntryTrace/TASKS.md | TODO | EntryTrace Guild | SCANNER-ENTRYTRACE-10-402 | Command resolution across layered rootfs with evidence attribution. |
|
| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.EntryTrace/TASKS.md | TODO | EntryTrace Guild | SCANNER-ENTRYTRACE-10-402 | Command resolution across layered rootfs with evidence attribution. |
|
||||||
| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.EntryTrace/TASKS.md | TODO | EntryTrace Guild | SCANNER-ENTRYTRACE-10-403 | Interpreter tracing for shell wrappers to Python/Node/Java launchers. |
|
| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.EntryTrace/TASKS.md | TODO | EntryTrace Guild | SCANNER-ENTRYTRACE-10-403 | Interpreter tracing for shell wrappers to Python/Node/Java launchers. |
|
||||||
@@ -68,8 +68,8 @@ This file describe implementation of Stella Ops (docs/README.md). Implementation
|
|||||||
| Sprint 13 | UX & CLI Experience | src/StellaOps.UI/TASKS.md | TODO | UI Guild | UI-SCHED-13-005 | Scheduler panel: schedules CRUD, run history, dry-run preview. |
|
| Sprint 13 | UX & CLI Experience | src/StellaOps.UI/TASKS.md | TODO | UI Guild | UI-SCHED-13-005 | Scheduler panel: schedules CRUD, run history, dry-run preview. |
|
||||||
| Sprint 13 | UX & CLI Experience | src/StellaOps.UI/TASKS.md | DOING (2025-10-19) | UI Guild | UI-NOTIFY-13-006 | Notify panel: channels/rules CRUD, deliveries view, test send. |
|
| Sprint 13 | UX & CLI Experience | src/StellaOps.UI/TASKS.md | DOING (2025-10-19) | UI Guild | UI-NOTIFY-13-006 | Notify panel: channels/rules CRUD, deliveries view, test send. |
|
||||||
| Sprint 13 | UX & CLI Experience | src/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI | CLI-RUNTIME-13-005 | Add runtime policy test verbs that consume `/policy/runtime` and display verdicts. |
|
| Sprint 13 | UX & CLI Experience | src/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI | CLI-RUNTIME-13-005 | Add runtime policy test verbs that consume `/policy/runtime` and display verdicts. |
|
||||||
| Sprint 13 | UX & CLI Experience | src/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI | CLI-PLUGIN-13-007 | Package non-core CLI verbs as restart-time plug-ins (manifest + loader tests). |
|
| Sprint 13 | UX & CLI Experience | src/StellaOps.Cli/TASKS.md | DONE (2025-10-22) | DevEx/CLI | CLI-PLUGIN-13-007 | Package non-core CLI verbs as restart-time plug-ins (manifest + loader tests). |
|
||||||
| Sprint 13 | UX & CLI Experience | src/StellaOps.Web/TASKS.md | TODO | UX Specialist, Angular Eng, DevEx | WEB1.DEPS-13-001 | Stabilise Angular workspace dependencies for headless CI installs (`npm install`, Chromium handling, docs). |
|
| Sprint 13 | UX & CLI Experience | src/StellaOps.Web/TASKS.md | DONE (2025-10-21) | UX Specialist, Angular Eng, DevEx | WEB1.DEPS-13-001 | Stabilise Angular workspace dependencies for headless CI installs (`npm install`, Chromium handling, docs). |
|
||||||
| Sprint 13 | Platform Reliability | ops/devops/TASKS.md | TODO | DevOps Guild, Platform Leads | DEVOPS-NUGET-13-001 | Wire up .NET 10 preview feeds/local mirrors so `dotnet restore` succeeds offline; document updated NuGet bootstrap. |
|
| Sprint 13 | Platform Reliability | ops/devops/TASKS.md | TODO | DevOps Guild, Platform Leads | DEVOPS-NUGET-13-001 | Wire up .NET 10 preview feeds/local mirrors so `dotnet restore` succeeds offline; document updated NuGet bootstrap. |
|
||||||
| Sprint 14 | Release & Offline Ops | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-REL-14-001 | Deterministic build/release pipeline with SBOM/provenance, signing, and manifest generation. |
|
| Sprint 14 | Release & Offline Ops | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-REL-14-001 | Deterministic build/release pipeline with SBOM/provenance, signing, and manifest generation. |
|
||||||
| Sprint 14 | Release & Offline Ops | ops/offline-kit/TASKS.md | TODO | Offline Kit Guild | DEVOPS-OFFLINE-14-002 | Offline kit packaging workflow with integrity verification and documentation. |
|
| Sprint 14 | Release & Offline Ops | ops/offline-kit/TASKS.md | TODO | Offline Kit Guild | DEVOPS-OFFLINE-14-002 | Offline kit packaging workflow with integrity verification and documentation. |
|
||||||
@@ -127,3 +127,4 @@ This file describe implementation of Stella Ops (docs/README.md). Implementation
|
|||||||
| Sprint 17 | Symbol Intelligence & Forensics | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-REL-17-002 | Ship stripped debug artifacts organised by build-id within release/offline kits. |
|
| Sprint 17 | Symbol Intelligence & Forensics | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-REL-17-002 | Ship stripped debug artifacts organised by build-id within release/offline kits. |
|
||||||
| Sprint 17 | Symbol Intelligence & Forensics | docs/TASKS.md | TODO | Docs Guild | DOCS-RUNTIME-17-004 | Document build-id workflows for SBOMs, runtime events, and debug-store usage. |
|
| Sprint 17 | Symbol Intelligence & Forensics | docs/TASKS.md | TODO | Docs Guild | DOCS-RUNTIME-17-004 | Document build-id workflows for SBOMs, runtime events, and debug-store usage. |
|
||||||
| Sprint 18 | Launch Readiness | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-LAUNCH-18-001 | Production launch cutover rehearsal and runbook publication (blocked on implementation sign-off and environment setup). |
|
| Sprint 18 | Launch Readiness | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-LAUNCH-18-001 | Production launch cutover rehearsal and runbook publication (blocked on implementation sign-off and environment setup). |
|
||||||
|
| Sprint 18 | Launch Readiness | ops/offline-kit/TASKS.md | TODO | Offline Kit Guild, UX Specialist | DEVOPS-OFFLINE-18-003 | Capture Angular workspace npm cache + Chromium bundle for Offline Kit distribution and document refresh cadence. |
|
||||||
|
|||||||
@@ -17,9 +17,12 @@ completely isolated network:
|
|||||||
| **Provenance** | Cosign signature, SPDX 2.3 SBOM, in‑toto SLSA attestation |
|
| **Provenance** | Cosign signature, SPDX 2.3 SBOM, in‑toto SLSA attestation |
|
||||||
| **Attested manifest** | `offline-manifest.json` + detached JWS covering bundle metadata, signed during export. |
|
| **Attested manifest** | `offline-manifest.json` + detached JWS covering bundle metadata, signed during export. |
|
||||||
| **Delta patches** | Daily diff bundles keep size \< 350 MB |
|
| **Delta patches** | Daily diff bundles keep size \< 350 MB |
|
||||||
|
| **Scanner plug-ins** | OS analyzers and the Node.js language analyzer packaged under `plugins/scanner/analyzers/**` with manifests so Workers load deterministically offline. |
|
||||||
|
|
||||||
**RU BDU note:** ship the official Russian Trusted Root/Sub CA bundle (`certificates/russian_trusted_bundle.pem`) inside the kit so `concelier:httpClients:source.bdu:trustedRootPaths` can resolve it when the service runs in an air‑gapped network. Drop the most recent `vulxml.zip` alongside the kit if operators need a cold-start cache.
|
**RU BDU note:** ship the official Russian Trusted Root/Sub CA bundle (`certificates/russian_trusted_bundle.pem`) inside the kit so `concelier:httpClients:source.bdu:trustedRootPaths` can resolve it when the service runs in an air‑gapped network. Drop the most recent `vulxml.zip` alongside the kit if operators need a cold-start cache.
|
||||||
|
|
||||||
|
**Language analyzers:** the kit now carries the restart-only Node.js analyzer plug-in (`plugins/scanner/analyzers/lang/StellaOps.Scanner.Analyzers.Lang.Node/`). Drop the directory alongside Worker binaries so the unified plug-in catalog can load it without outbound fetches; upcoming Python/Go/.NET/Rust plug-ins will follow the same layout.
|
||||||
|
|
||||||
*Scanner core:* C# 12 on **.NET {{ dotnet }}**.
|
*Scanner core:* C# 12 on **.NET {{ dotnet }}**.
|
||||||
*Imports are idempotent and atomic — no service downtime.*
|
*Imports are idempotent and atomic — no service downtime.*
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ src/
|
|||||||
|
|
||||||
**Language/runtime**: .NET 10 **Native AOT** for speed/startup; Linux builds use **musl** static when possible.
|
**Language/runtime**: .NET 10 **Native AOT** for speed/startup; Linux builds use **musl** static when possible.
|
||||||
|
|
||||||
**Plug-in verbs.** Non-core verbs (Excititor, runtime helpers, future integrations) ship as restart-time plug-ins under `plugins/cli/**` with manifest descriptors. The launcher loads plug-ins on startup; hot reloading is intentionally unsupported.
|
**Plug-in verbs.** Non-core verbs (Excititor, runtime helpers, future integrations) ship as restart-time plug-ins under `plugins/cli/**` with manifest descriptors. The launcher loads plug-ins on startup; hot reloading is intentionally unsupported. The inaugural bundle, `StellaOps.Cli.Plugins.NonCore`, packages the Excititor, runtime, and offline-kit command groups and publishes its manifest at `plugins/cli/StellaOps.Cli.Plugins.NonCore/`.
|
||||||
|
|
||||||
**OS targets**: linux‑x64/arm64, windows‑x64/arm64, macOS‑x64/arm64.
|
**OS targets**: linux‑x64/arm64, windows‑x64/arm64, macOS‑x64/arm64.
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# Offline Kit Task Board
|
# Offline Kit Task Board
|
||||||
|
|
||||||
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
||||||
|----|--------|----------|------------|-------------|---------------|
|
|----|--------|----------|------------|-------------|---------------|
|
||||||
| DEVOPS-OFFLINE-14-002 | TODO | Offline Kit Guild | DEVOPS-REL-14-001 | Build offline kit packaging workflow (artifact bundling, manifest generation, signature verification). | Offline tarball generated with manifest + checksums + signatures; import script verifies integrity; docs updated. |
|
| DEVOPS-OFFLINE-14-002 | TODO | Offline Kit Guild | DEVOPS-REL-14-001 | Build offline kit packaging workflow (artifact bundling, manifest generation, signature verification). | Offline tarball generated with manifest + checksums + signatures; import script verifies integrity; docs updated. |
|
||||||
|
| DEVOPS-OFFLINE-18-003 | TODO | Offline Kit Guild, UX Specialist | DEVOPS-OFFLINE-14-002 | Capture Angular workspace npm cache + Chromium bundle in Offline Kit (`out/offline-kit/web/`) and document refresh cadence. | Web cache directory added to kit manifest; documentation updated with `npm run ci:install`/`verify:chromium` workflow; periodic refresh SOP recorded in Offline Kit guide. |
|
||||||
|
|||||||
21
plugins/cli/StellaOps.Cli.Plugins.NonCore/manifest.json
Normal file
21
plugins/cli/StellaOps.Cli.Plugins.NonCore/manifest.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"schemaVersion": "1.0",
|
||||||
|
"id": "stellaops.cli.plugins.noncore",
|
||||||
|
"displayName": "StellaOps CLI Non-core Verbs",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"requiresRestart": true,
|
||||||
|
"entryPoint": {
|
||||||
|
"type": "dotnet",
|
||||||
|
"assembly": "StellaOps.Cli.Plugins.NonCore.dll",
|
||||||
|
"typeName": "StellaOps.Cli.Plugins.NonCore.NonCoreCliCommandModule"
|
||||||
|
},
|
||||||
|
"capabilities": [
|
||||||
|
"cli",
|
||||||
|
"excititor",
|
||||||
|
"runtime-policy",
|
||||||
|
"offline-kit"
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"org.stellaops.restart.required": "true"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"schemaVersion": "1.0",
|
||||||
|
"id": "stellaops.analyzer.lang.dotnet",
|
||||||
|
"displayName": "StellaOps .NET Analyzer (preview)",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"requiresRestart": true,
|
||||||
|
"entryPoint": {
|
||||||
|
"type": "dotnet",
|
||||||
|
"assembly": "StellaOps.Scanner.Analyzers.Lang.DotNet.dll",
|
||||||
|
"typeName": "StellaOps.Scanner.Analyzers.Lang.DotNet.DotNetAnalyzerPlugin"
|
||||||
|
},
|
||||||
|
"capabilities": [
|
||||||
|
"language-analyzer",
|
||||||
|
"dotnet",
|
||||||
|
"nuget"
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"org.stellaops.analyzer.language": "dotnet",
|
||||||
|
"org.stellaops.analyzer.kind": "language",
|
||||||
|
"org.stellaops.restart.required": "true",
|
||||||
|
"org.stellaops.analyzer.status": "preview"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"schemaVersion": "1.0",
|
||||||
|
"id": "stellaops.analyzer.lang.go",
|
||||||
|
"displayName": "StellaOps Go Analyzer (preview)",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"requiresRestart": true,
|
||||||
|
"entryPoint": {
|
||||||
|
"type": "dotnet",
|
||||||
|
"assembly": "StellaOps.Scanner.Analyzers.Lang.Go.dll",
|
||||||
|
"typeName": "StellaOps.Scanner.Analyzers.Lang.Go.GoAnalyzerPlugin"
|
||||||
|
},
|
||||||
|
"capabilities": [
|
||||||
|
"language-analyzer",
|
||||||
|
"golang",
|
||||||
|
"go"
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"org.stellaops.analyzer.language": "go",
|
||||||
|
"org.stellaops.analyzer.kind": "language",
|
||||||
|
"org.stellaops.restart.required": "true",
|
||||||
|
"org.stellaops.analyzer.status": "preview"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"schemaVersion": "1.0",
|
||||||
|
"id": "stellaops.analyzer.lang.node",
|
||||||
|
"displayName": "StellaOps Node.js Analyzer",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"requiresRestart": true,
|
||||||
|
"entryPoint": {
|
||||||
|
"type": "dotnet",
|
||||||
|
"assembly": "StellaOps.Scanner.Analyzers.Lang.Node.dll",
|
||||||
|
"typeName": "StellaOps.Scanner.Analyzers.Lang.Node.NodeAnalyzerPlugin"
|
||||||
|
},
|
||||||
|
"capabilities": [
|
||||||
|
"language-analyzer",
|
||||||
|
"node",
|
||||||
|
"npm"
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"org.stellaops.analyzer.language": "node",
|
||||||
|
"org.stellaops.analyzer.kind": "language",
|
||||||
|
"org.stellaops.restart.required": "true"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"schemaVersion": "1.0",
|
||||||
|
"id": "stellaops.analyzer.lang.python",
|
||||||
|
"displayName": "StellaOps Python Analyzer (preview)",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"requiresRestart": true,
|
||||||
|
"entryPoint": {
|
||||||
|
"type": "dotnet",
|
||||||
|
"assembly": "StellaOps.Scanner.Analyzers.Lang.Python.dll",
|
||||||
|
"typeName": "StellaOps.Scanner.Analyzers.Lang.Python.PythonAnalyzerPlugin"
|
||||||
|
},
|
||||||
|
"capabilities": [
|
||||||
|
"language-analyzer",
|
||||||
|
"python",
|
||||||
|
"pypi"
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"org.stellaops.analyzer.language": "python",
|
||||||
|
"org.stellaops.analyzer.kind": "language",
|
||||||
|
"org.stellaops.restart.required": "true",
|
||||||
|
"org.stellaops.analyzer.status": "preview"
|
||||||
|
}
|
||||||
|
}
|
||||||
416
src/StellaOps.Cli.Plugins.NonCore/NonCoreCliCommandModule.cs
Normal file
416
src/StellaOps.Cli.Plugins.NonCore/NonCoreCliCommandModule.cs
Normal file
@@ -0,0 +1,416 @@
|
|||||||
|
using System;
|
||||||
|
using System.CommandLine;
|
||||||
|
using System.Threading;
|
||||||
|
using StellaOps.Cli.Commands;
|
||||||
|
using StellaOps.Cli.Configuration;
|
||||||
|
using StellaOps.Cli.Plugins;
|
||||||
|
|
||||||
|
namespace StellaOps.Cli.Plugins.NonCore;
|
||||||
|
|
||||||
|
public sealed class NonCoreCliCommandModule : ICliCommandModule
|
||||||
|
{
|
||||||
|
public string Name => "stellaops.cli.plugins.noncore";
|
||||||
|
|
||||||
|
public bool IsAvailable(IServiceProvider services) => true;
|
||||||
|
|
||||||
|
public void RegisterCommands(
|
||||||
|
RootCommand root,
|
||||||
|
IServiceProvider services,
|
||||||
|
StellaOpsCliOptions options,
|
||||||
|
Option<bool> verboseOption,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(root);
|
||||||
|
ArgumentNullException.ThrowIfNull(services);
|
||||||
|
ArgumentNullException.ThrowIfNull(verboseOption);
|
||||||
|
|
||||||
|
root.Add(BuildExcititorCommand(services, verboseOption, cancellationToken));
|
||||||
|
root.Add(BuildRuntimeCommand(services, verboseOption, cancellationToken));
|
||||||
|
root.Add(BuildOfflineCommand(services, verboseOption, cancellationToken));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Command BuildExcititorCommand(IServiceProvider services, Option<bool> verboseOption, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var excititor = new Command("excititor", "Manage Excititor ingest, exports, and reconciliation workflows.");
|
||||||
|
|
||||||
|
var init = new Command("init", "Initialize Excititor ingest state.");
|
||||||
|
var initProviders = new Option<string[]>("--provider", new[] { "-p" })
|
||||||
|
{
|
||||||
|
Description = "Optional provider identifier(s) to initialize.",
|
||||||
|
Arity = ArgumentArity.ZeroOrMore
|
||||||
|
};
|
||||||
|
var resumeOption = new Option<bool>("--resume")
|
||||||
|
{
|
||||||
|
Description = "Resume ingest from the last persisted checkpoint instead of starting fresh."
|
||||||
|
};
|
||||||
|
init.Add(initProviders);
|
||||||
|
init.Add(resumeOption);
|
||||||
|
init.SetAction((parseResult, _) =>
|
||||||
|
{
|
||||||
|
var providers = parseResult.GetValue(initProviders) ?? Array.Empty<string>();
|
||||||
|
var resume = parseResult.GetValue(resumeOption);
|
||||||
|
var verbose = parseResult.GetValue(verboseOption);
|
||||||
|
return CommandHandlers.HandleExcititorInitAsync(services, providers, resume, verbose, cancellationToken);
|
||||||
|
});
|
||||||
|
|
||||||
|
var pull = new Command("pull", "Trigger Excititor ingest for configured providers.");
|
||||||
|
var pullProviders = new Option<string[]>("--provider", new[] { "-p" })
|
||||||
|
{
|
||||||
|
Description = "Optional provider identifier(s) to ingest.",
|
||||||
|
Arity = ArgumentArity.ZeroOrMore
|
||||||
|
};
|
||||||
|
var sinceOption = new Option<DateTimeOffset?>("--since")
|
||||||
|
{
|
||||||
|
Description = "Optional ISO-8601 timestamp to begin the ingest window."
|
||||||
|
};
|
||||||
|
var windowOption = new Option<TimeSpan?>("--window")
|
||||||
|
{
|
||||||
|
Description = "Optional window duration (e.g. 24:00:00)."
|
||||||
|
};
|
||||||
|
var forceOption = new Option<bool>("--force")
|
||||||
|
{
|
||||||
|
Description = "Force ingestion even if the backend reports no pending work."
|
||||||
|
};
|
||||||
|
pull.Add(pullProviders);
|
||||||
|
pull.Add(sinceOption);
|
||||||
|
pull.Add(windowOption);
|
||||||
|
pull.Add(forceOption);
|
||||||
|
pull.SetAction((parseResult, _) =>
|
||||||
|
{
|
||||||
|
var providers = parseResult.GetValue(pullProviders) ?? Array.Empty<string>();
|
||||||
|
var since = parseResult.GetValue(sinceOption);
|
||||||
|
var window = parseResult.GetValue(windowOption);
|
||||||
|
var force = parseResult.GetValue(forceOption);
|
||||||
|
var verbose = parseResult.GetValue(verboseOption);
|
||||||
|
return CommandHandlers.HandleExcititorPullAsync(services, providers, since, window, force, verbose, cancellationToken);
|
||||||
|
});
|
||||||
|
|
||||||
|
var resume = new Command("resume", "Resume Excititor ingest using a checkpoint token.");
|
||||||
|
var resumeProviders = new Option<string[]>("--provider", new[] { "-p" })
|
||||||
|
{
|
||||||
|
Description = "Optional provider identifier(s) to resume.",
|
||||||
|
Arity = ArgumentArity.ZeroOrMore
|
||||||
|
};
|
||||||
|
var checkpointOption = new Option<string?>("--checkpoint")
|
||||||
|
{
|
||||||
|
Description = "Optional checkpoint identifier to resume from."
|
||||||
|
};
|
||||||
|
resume.Add(resumeProviders);
|
||||||
|
resume.Add(checkpointOption);
|
||||||
|
resume.SetAction((parseResult, _) =>
|
||||||
|
{
|
||||||
|
var providers = parseResult.GetValue(resumeProviders) ?? Array.Empty<string>();
|
||||||
|
var checkpoint = parseResult.GetValue(checkpointOption);
|
||||||
|
var verbose = parseResult.GetValue(verboseOption);
|
||||||
|
return CommandHandlers.HandleExcititorResumeAsync(services, providers, checkpoint, verbose, cancellationToken);
|
||||||
|
});
|
||||||
|
|
||||||
|
var list = new Command("list-providers", "List Excititor providers and their ingest status.");
|
||||||
|
var includeDisabledOption = new Option<bool>("--include-disabled")
|
||||||
|
{
|
||||||
|
Description = "Include disabled providers in the listing."
|
||||||
|
};
|
||||||
|
list.Add(includeDisabledOption);
|
||||||
|
list.SetAction((parseResult, _) =>
|
||||||
|
{
|
||||||
|
var includeDisabled = parseResult.GetValue(includeDisabledOption);
|
||||||
|
var verbose = parseResult.GetValue(verboseOption);
|
||||||
|
return CommandHandlers.HandleExcititorListProvidersAsync(services, includeDisabled, verbose, cancellationToken);
|
||||||
|
});
|
||||||
|
|
||||||
|
var export = new Command("export", "Trigger Excititor export generation.");
|
||||||
|
var formatOption = new Option<string>("--format")
|
||||||
|
{
|
||||||
|
Description = "Export format (e.g. openvex, json)."
|
||||||
|
};
|
||||||
|
var exportDeltaOption = new Option<bool>("--delta")
|
||||||
|
{
|
||||||
|
Description = "Request a delta export when supported."
|
||||||
|
};
|
||||||
|
var exportScopeOption = new Option<string?>("--scope")
|
||||||
|
{
|
||||||
|
Description = "Optional policy scope or tenant identifier."
|
||||||
|
};
|
||||||
|
var exportSinceOption = new Option<DateTimeOffset?>("--since")
|
||||||
|
{
|
||||||
|
Description = "Optional ISO-8601 timestamp to restrict export contents."
|
||||||
|
};
|
||||||
|
var exportProviderOption = new Option<string?>("--provider")
|
||||||
|
{
|
||||||
|
Description = "Optional provider identifier when requesting targeted exports."
|
||||||
|
};
|
||||||
|
var exportOutputOption = new Option<string?>("--output")
|
||||||
|
{
|
||||||
|
Description = "Optional path to download the export artifact."
|
||||||
|
};
|
||||||
|
export.Add(formatOption);
|
||||||
|
export.Add(exportDeltaOption);
|
||||||
|
export.Add(exportScopeOption);
|
||||||
|
export.Add(exportSinceOption);
|
||||||
|
export.Add(exportProviderOption);
|
||||||
|
export.Add(exportOutputOption);
|
||||||
|
export.SetAction((parseResult, _) =>
|
||||||
|
{
|
||||||
|
var format = parseResult.GetValue(formatOption) ?? "openvex";
|
||||||
|
var delta = parseResult.GetValue(exportDeltaOption);
|
||||||
|
var scope = parseResult.GetValue(exportScopeOption);
|
||||||
|
var since = parseResult.GetValue(exportSinceOption);
|
||||||
|
var provider = parseResult.GetValue(exportProviderOption);
|
||||||
|
var output = parseResult.GetValue(exportOutputOption);
|
||||||
|
var verbose = parseResult.GetValue(verboseOption);
|
||||||
|
return CommandHandlers.HandleExcititorExportAsync(services, format, delta, scope, since, provider, output, verbose, cancellationToken);
|
||||||
|
});
|
||||||
|
|
||||||
|
var backfill = new Command("backfill-statements", "Replay historical raw documents into Excititor statements.");
|
||||||
|
var backfillRetrievedSinceOption = new Option<DateTimeOffset?>("--retrieved-since")
|
||||||
|
{
|
||||||
|
Description = "Only process raw documents retrieved on or after the provided ISO-8601 timestamp."
|
||||||
|
};
|
||||||
|
var backfillForceOption = new Option<bool>("--force")
|
||||||
|
{
|
||||||
|
Description = "Reprocess documents even if statements already exist."
|
||||||
|
};
|
||||||
|
var backfillBatchSizeOption = new Option<int>("--batch-size")
|
||||||
|
{
|
||||||
|
Description = "Number of raw documents to fetch per batch (default 100)."
|
||||||
|
};
|
||||||
|
var backfillMaxDocumentsOption = new Option<int?>("--max-documents")
|
||||||
|
{
|
||||||
|
Description = "Optional maximum number of raw documents to process."
|
||||||
|
};
|
||||||
|
backfill.Add(backfillRetrievedSinceOption);
|
||||||
|
backfill.Add(backfillForceOption);
|
||||||
|
backfill.Add(backfillBatchSizeOption);
|
||||||
|
backfill.Add(backfillMaxDocumentsOption);
|
||||||
|
backfill.SetAction((parseResult, _) =>
|
||||||
|
{
|
||||||
|
var retrievedSince = parseResult.GetValue(backfillRetrievedSinceOption);
|
||||||
|
var force = parseResult.GetValue(backfillForceOption);
|
||||||
|
var batchSize = parseResult.GetValue(backfillBatchSizeOption);
|
||||||
|
if (batchSize <= 0)
|
||||||
|
{
|
||||||
|
batchSize = 100;
|
||||||
|
}
|
||||||
|
var maxDocuments = parseResult.GetValue(backfillMaxDocumentsOption);
|
||||||
|
var verbose = parseResult.GetValue(verboseOption);
|
||||||
|
return CommandHandlers.HandleExcititorBackfillStatementsAsync(
|
||||||
|
services,
|
||||||
|
retrievedSince,
|
||||||
|
force,
|
||||||
|
batchSize,
|
||||||
|
maxDocuments,
|
||||||
|
verbose,
|
||||||
|
cancellationToken);
|
||||||
|
});
|
||||||
|
|
||||||
|
var verify = new Command("verify", "Verify Excititor exports or attestations.");
|
||||||
|
var exportIdOption = new Option<string?>("--export-id")
|
||||||
|
{
|
||||||
|
Description = "Export identifier to verify."
|
||||||
|
};
|
||||||
|
var digestOption = new Option<string?>("--digest")
|
||||||
|
{
|
||||||
|
Description = "Expected digest for the export or attestation."
|
||||||
|
};
|
||||||
|
var attestationOption = new Option<string?>("--attestation")
|
||||||
|
{
|
||||||
|
Description = "Path to a local attestation file to verify (base64 content will be uploaded)."
|
||||||
|
};
|
||||||
|
verify.Add(exportIdOption);
|
||||||
|
verify.Add(digestOption);
|
||||||
|
verify.Add(attestationOption);
|
||||||
|
verify.SetAction((parseResult, _) =>
|
||||||
|
{
|
||||||
|
var exportId = parseResult.GetValue(exportIdOption);
|
||||||
|
var digest = parseResult.GetValue(digestOption);
|
||||||
|
var attestation = parseResult.GetValue(attestationOption);
|
||||||
|
var verbose = parseResult.GetValue(verboseOption);
|
||||||
|
return CommandHandlers.HandleExcititorVerifyAsync(services, exportId, digest, attestation, verbose, cancellationToken);
|
||||||
|
});
|
||||||
|
|
||||||
|
var reconcile = new Command("reconcile", "Trigger Excititor reconciliation against canonical advisories.");
|
||||||
|
var reconcileProviders = new Option<string[]>("--provider", new[] { "-p" })
|
||||||
|
{
|
||||||
|
Description = "Optional provider identifier(s) to reconcile.",
|
||||||
|
Arity = ArgumentArity.ZeroOrMore
|
||||||
|
};
|
||||||
|
var maxAgeOption = new Option<TimeSpan?>("--max-age")
|
||||||
|
{
|
||||||
|
Description = "Optional maximum age window (e.g. 7.00:00:00)."
|
||||||
|
};
|
||||||
|
reconcile.Add(reconcileProviders);
|
||||||
|
reconcile.Add(maxAgeOption);
|
||||||
|
reconcile.SetAction((parseResult, _) =>
|
||||||
|
{
|
||||||
|
var providers = parseResult.GetValue(reconcileProviders) ?? Array.Empty<string>();
|
||||||
|
var maxAge = parseResult.GetValue(maxAgeOption);
|
||||||
|
var verbose = parseResult.GetValue(verboseOption);
|
||||||
|
return CommandHandlers.HandleExcititorReconcileAsync(services, providers, maxAge, verbose, cancellationToken);
|
||||||
|
});
|
||||||
|
|
||||||
|
excititor.Add(init);
|
||||||
|
excititor.Add(pull);
|
||||||
|
excititor.Add(resume);
|
||||||
|
excititor.Add(list);
|
||||||
|
excititor.Add(export);
|
||||||
|
excititor.Add(backfill);
|
||||||
|
excititor.Add(verify);
|
||||||
|
excititor.Add(reconcile);
|
||||||
|
return excititor;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Command BuildRuntimeCommand(IServiceProvider services, Option<bool> verboseOption, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var runtime = new Command("runtime", "Interact with runtime admission policy APIs.");
|
||||||
|
var policy = new Command("policy", "Runtime policy operations.");
|
||||||
|
|
||||||
|
var test = new Command("test", "Evaluate runtime policy decisions for image digests.");
|
||||||
|
var namespaceOption = new Option<string?>("--namespace", new[] { "--ns" })
|
||||||
|
{
|
||||||
|
Description = "Namespace or logical scope for the evaluation."
|
||||||
|
};
|
||||||
|
|
||||||
|
var imageOption = new Option<string[]>("--image", new[] { "-i", "--images" })
|
||||||
|
{
|
||||||
|
Description = "Image digests to evaluate (repeatable).",
|
||||||
|
Arity = ArgumentArity.ZeroOrMore
|
||||||
|
};
|
||||||
|
|
||||||
|
var fileOption = new Option<string?>("--file", new[] { "-f" })
|
||||||
|
{
|
||||||
|
Description = "Path to a file containing image digests (one per line)."
|
||||||
|
};
|
||||||
|
|
||||||
|
var labelOption = new Option<string[]>("--label", new[] { "-l", "--labels" })
|
||||||
|
{
|
||||||
|
Description = "Pod labels in key=value format (repeatable).",
|
||||||
|
Arity = ArgumentArity.ZeroOrMore
|
||||||
|
};
|
||||||
|
|
||||||
|
var jsonOption = new Option<bool>("--json")
|
||||||
|
{
|
||||||
|
Description = "Emit the raw JSON response."
|
||||||
|
};
|
||||||
|
|
||||||
|
test.Add(namespaceOption);
|
||||||
|
test.Add(imageOption);
|
||||||
|
test.Add(fileOption);
|
||||||
|
test.Add(labelOption);
|
||||||
|
test.Add(jsonOption);
|
||||||
|
|
||||||
|
test.SetAction((parseResult, _) =>
|
||||||
|
{
|
||||||
|
var nsValue = parseResult.GetValue(namespaceOption);
|
||||||
|
var images = parseResult.GetValue(imageOption) ?? Array.Empty<string>();
|
||||||
|
var file = parseResult.GetValue(fileOption);
|
||||||
|
var labels = parseResult.GetValue(labelOption) ?? Array.Empty<string>();
|
||||||
|
var outputJson = parseResult.GetValue(jsonOption);
|
||||||
|
var verbose = parseResult.GetValue(verboseOption);
|
||||||
|
|
||||||
|
return CommandHandlers.HandleRuntimePolicyTestAsync(
|
||||||
|
services,
|
||||||
|
nsValue,
|
||||||
|
images,
|
||||||
|
file,
|
||||||
|
labels,
|
||||||
|
outputJson,
|
||||||
|
verbose,
|
||||||
|
cancellationToken);
|
||||||
|
});
|
||||||
|
|
||||||
|
policy.Add(test);
|
||||||
|
runtime.Add(policy);
|
||||||
|
return runtime;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Command BuildOfflineCommand(IServiceProvider services, Option<bool> verboseOption, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var offline = new Command("offline", "Offline kit workflows and utilities.");
|
||||||
|
|
||||||
|
var kit = new Command("kit", "Manage offline kit bundles.");
|
||||||
|
|
||||||
|
var pull = new Command("pull", "Download the latest offline kit bundle.");
|
||||||
|
var bundleIdOption = new Option<string?>("--bundle-id")
|
||||||
|
{
|
||||||
|
Description = "Optional bundle identifier. Defaults to the latest available."
|
||||||
|
};
|
||||||
|
var destinationOption = new Option<string?>("--destination")
|
||||||
|
{
|
||||||
|
Description = "Directory to store downloaded bundles (defaults to the configured offline kits directory)."
|
||||||
|
};
|
||||||
|
var overwriteOption = new Option<bool>("--overwrite")
|
||||||
|
{
|
||||||
|
Description = "Overwrite existing files even if checksums match."
|
||||||
|
};
|
||||||
|
var noResumeOption = new Option<bool>("--no-resume")
|
||||||
|
{
|
||||||
|
Description = "Disable resuming partial downloads."
|
||||||
|
};
|
||||||
|
|
||||||
|
pull.Add(bundleIdOption);
|
||||||
|
pull.Add(destinationOption);
|
||||||
|
pull.Add(overwriteOption);
|
||||||
|
pull.Add(noResumeOption);
|
||||||
|
pull.SetAction((parseResult, _) =>
|
||||||
|
{
|
||||||
|
var bundleId = parseResult.GetValue(bundleIdOption);
|
||||||
|
var destination = parseResult.GetValue(destinationOption);
|
||||||
|
var overwrite = parseResult.GetValue(overwriteOption);
|
||||||
|
var resume = !parseResult.GetValue(noResumeOption);
|
||||||
|
var verbose = parseResult.GetValue(verboseOption);
|
||||||
|
return CommandHandlers.HandleOfflineKitPullAsync(services, bundleId, destination, overwrite, resume, verbose, cancellationToken);
|
||||||
|
});
|
||||||
|
|
||||||
|
var import = new Command("import", "Upload an offline kit bundle to the backend.");
|
||||||
|
var bundleArgument = new Argument<string>("bundle")
|
||||||
|
{
|
||||||
|
Description = "Path to the offline kit tarball (.tgz)."
|
||||||
|
};
|
||||||
|
var manifestOption = new Option<string?>("--manifest")
|
||||||
|
{
|
||||||
|
Description = "Offline manifest JSON path (defaults to metadata or sibling file)."
|
||||||
|
};
|
||||||
|
var bundleSignatureOption = new Option<string?>("--bundle-signature")
|
||||||
|
{
|
||||||
|
Description = "Detached signature for the offline bundle (e.g. .sig)."
|
||||||
|
};
|
||||||
|
var manifestSignatureOption = new Option<string?>("--manifest-signature")
|
||||||
|
{
|
||||||
|
Description = "Detached signature for the offline manifest (e.g. .jws)."
|
||||||
|
};
|
||||||
|
|
||||||
|
import.Add(bundleArgument);
|
||||||
|
import.Add(manifestOption);
|
||||||
|
import.Add(bundleSignatureOption);
|
||||||
|
import.Add(manifestSignatureOption);
|
||||||
|
import.SetAction((parseResult, _) =>
|
||||||
|
{
|
||||||
|
var bundlePath = parseResult.GetValue(bundleArgument) ?? string.Empty;
|
||||||
|
var manifest = parseResult.GetValue(manifestOption);
|
||||||
|
var bundleSignature = parseResult.GetValue(bundleSignatureOption);
|
||||||
|
var manifestSignature = parseResult.GetValue(manifestSignatureOption);
|
||||||
|
var verbose = parseResult.GetValue(verboseOption);
|
||||||
|
return CommandHandlers.HandleOfflineKitImportAsync(services, bundlePath, manifest, bundleSignature, manifestSignature, verbose, cancellationToken);
|
||||||
|
});
|
||||||
|
|
||||||
|
var status = new Command("status", "Display offline kit installation status.");
|
||||||
|
var jsonOption = new Option<bool>("--json")
|
||||||
|
{
|
||||||
|
Description = "Emit status as JSON."
|
||||||
|
};
|
||||||
|
status.Add(jsonOption);
|
||||||
|
status.SetAction((parseResult, _) =>
|
||||||
|
{
|
||||||
|
var asJson = parseResult.GetValue(jsonOption);
|
||||||
|
var verbose = parseResult.GetValue(verboseOption);
|
||||||
|
return CommandHandlers.HandleOfflineKitStatusAsync(services, asJson, verbose, cancellationToken);
|
||||||
|
});
|
||||||
|
|
||||||
|
kit.Add(pull);
|
||||||
|
kit.Add(import);
|
||||||
|
kit.Add(status);
|
||||||
|
|
||||||
|
offline.Add(kit);
|
||||||
|
return offline;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<LangVersion>preview</LangVersion>
|
||||||
|
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||||
|
<PluginOutputDirectory>$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..\\..\\plugins\\cli\\StellaOps.Cli.Plugins.NonCore\\'))</PluginOutputDirectory>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\StellaOps.Cli\StellaOps.Cli.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<Target Name="CopyPluginBinaries" AfterTargets="Build">
|
||||||
|
<MakeDir Directories="$(PluginOutputDirectory)" />
|
||||||
|
<Copy SourceFiles="$(TargetDir)$(TargetFileName)" DestinationFolder="$(PluginOutputDirectory)" />
|
||||||
|
<Copy SourceFiles="$(TargetDir)$(TargetName).pdb"
|
||||||
|
DestinationFolder="$(PluginOutputDirectory)"
|
||||||
|
Condition="Exists('$(TargetDir)$(TargetName).pdb')" />
|
||||||
|
</Target>
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
using System;
|
||||||
|
using System.CommandLine;
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using StellaOps.Cli.Configuration;
|
||||||
|
using StellaOps.Cli.Plugins;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace StellaOps.Cli.Tests.Plugins;
|
||||||
|
|
||||||
|
public sealed class CliCommandModuleLoaderTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void RegisterModules_LoadsNonCoreCommandsFromPlugin()
|
||||||
|
{
|
||||||
|
var options = new StellaOpsCliOptions();
|
||||||
|
var repoRoot = Path.GetFullPath(
|
||||||
|
Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", ".."));
|
||||||
|
|
||||||
|
options.Plugins.BaseDirectory = repoRoot;
|
||||||
|
options.Plugins.Directory = "plugins/cli";
|
||||||
|
|
||||||
|
var services = new ServiceCollection()
|
||||||
|
.AddSingleton(options)
|
||||||
|
.BuildServiceProvider();
|
||||||
|
|
||||||
|
var logger = NullLoggerFactory.Instance.CreateLogger<CliCommandModuleLoader>();
|
||||||
|
var loader = new CliCommandModuleLoader(services, options, logger);
|
||||||
|
|
||||||
|
var root = new RootCommand();
|
||||||
|
var verbose = new Option<bool>("--verbose");
|
||||||
|
|
||||||
|
loader.RegisterModules(root, verbose, CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.Contains(root.Children, command => string.Equals(command.Name, "excititor", StringComparison.Ordinal));
|
||||||
|
Assert.Contains(root.Children, command => string.Equals(command.Name, "runtime", StringComparison.Ordinal));
|
||||||
|
Assert.Contains(root.Children, command => string.Equals(command.Name, "offline", StringComparison.Ordinal));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
using StellaOps.Cli.Plugins;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace StellaOps.Cli.Tests.Plugins;
|
||||||
|
|
||||||
|
public sealed class RestartOnlyCliPluginGuardTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void EnsureRegistrationAllowed_AllowsDuringStartup()
|
||||||
|
{
|
||||||
|
var guard = new RestartOnlyCliPluginGuard();
|
||||||
|
guard.EnsureRegistrationAllowed("./plugins/sample.dll");
|
||||||
|
guard.Seal();
|
||||||
|
|
||||||
|
// Re-registering known plug-ins after sealing should succeed.
|
||||||
|
guard.EnsureRegistrationAllowed("./plugins/sample.dll");
|
||||||
|
Assert.True(guard.IsSealed);
|
||||||
|
Assert.Single(guard.KnownPlugins);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void EnsureRegistrationAllowed_ThrowsForUnknownAfterSeal()
|
||||||
|
{
|
||||||
|
var guard = new RestartOnlyCliPluginGuard();
|
||||||
|
guard.Seal();
|
||||||
|
|
||||||
|
Assert.Throws<InvalidOperationException>(() => guard.EnsureRegistrationAllowed("./plugins/new.dll"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,19 +1,27 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.CommandLine;
|
using System.CommandLine;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using StellaOps.Cli.Configuration;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using StellaOps.Cli.Configuration;
|
||||||
|
using StellaOps.Cli.Plugins;
|
||||||
|
|
||||||
namespace StellaOps.Cli.Commands;
|
namespace StellaOps.Cli.Commands;
|
||||||
|
|
||||||
internal static class CommandFactory
|
internal static class CommandFactory
|
||||||
{
|
{
|
||||||
public static RootCommand Create(IServiceProvider services, StellaOpsCliOptions options, CancellationToken cancellationToken)
|
public static RootCommand Create(
|
||||||
{
|
IServiceProvider services,
|
||||||
var verboseOption = new Option<bool>("--verbose", new[] { "-v" })
|
StellaOpsCliOptions options,
|
||||||
{
|
CancellationToken cancellationToken,
|
||||||
Description = "Enable verbose logging output."
|
ILoggerFactory loggerFactory)
|
||||||
};
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(loggerFactory);
|
||||||
|
|
||||||
|
var verboseOption = new Option<bool>("--verbose", new[] { "-v" })
|
||||||
|
{
|
||||||
|
Description = "Enable verbose logging output."
|
||||||
|
};
|
||||||
|
|
||||||
var root = new RootCommand("StellaOps command-line interface")
|
var root = new RootCommand("StellaOps command-line interface")
|
||||||
{
|
{
|
||||||
@@ -24,12 +32,13 @@ internal static class CommandFactory
|
|||||||
root.Add(BuildScannerCommand(services, verboseOption, cancellationToken));
|
root.Add(BuildScannerCommand(services, verboseOption, cancellationToken));
|
||||||
root.Add(BuildScanCommand(services, options, verboseOption, cancellationToken));
|
root.Add(BuildScanCommand(services, options, verboseOption, cancellationToken));
|
||||||
root.Add(BuildDatabaseCommand(services, verboseOption, cancellationToken));
|
root.Add(BuildDatabaseCommand(services, verboseOption, cancellationToken));
|
||||||
root.Add(BuildExcititorCommand(services, verboseOption, cancellationToken));
|
|
||||||
root.Add(BuildRuntimeCommand(services, verboseOption, cancellationToken));
|
|
||||||
root.Add(BuildAuthCommand(services, options, verboseOption, cancellationToken));
|
root.Add(BuildAuthCommand(services, options, verboseOption, cancellationToken));
|
||||||
root.Add(BuildOfflineCommand(services, verboseOption, cancellationToken));
|
|
||||||
root.Add(BuildConfigCommand(options));
|
root.Add(BuildConfigCommand(options));
|
||||||
|
|
||||||
|
var pluginLogger = loggerFactory.CreateLogger<CliCommandModuleLoader>();
|
||||||
|
var pluginLoader = new CliCommandModuleLoader(services, options, pluginLogger);
|
||||||
|
pluginLoader.RegisterModules(root, verboseOption, cancellationToken);
|
||||||
|
|
||||||
return root;
|
return root;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,300 +236,6 @@ internal static class CommandFactory
|
|||||||
return db;
|
return db;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Command BuildExcititorCommand(IServiceProvider services, Option<bool> verboseOption, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var excititor = new Command("excititor", "Manage Excititor ingest, exports, and reconciliation workflows.");
|
|
||||||
|
|
||||||
var init = new Command("init", "Initialize Excititor ingest state.");
|
|
||||||
var initProviders = new Option<string[]>("--provider", new[] { "-p" })
|
|
||||||
{
|
|
||||||
Description = "Optional provider identifier(s) to initialize.",
|
|
||||||
Arity = ArgumentArity.ZeroOrMore
|
|
||||||
};
|
|
||||||
var resumeOption = new Option<bool>("--resume")
|
|
||||||
{
|
|
||||||
Description = "Resume ingest from the last persisted checkpoint instead of starting fresh."
|
|
||||||
};
|
|
||||||
init.Add(initProviders);
|
|
||||||
init.Add(resumeOption);
|
|
||||||
init.SetAction((parseResult, _) =>
|
|
||||||
{
|
|
||||||
var providers = parseResult.GetValue(initProviders) ?? Array.Empty<string>();
|
|
||||||
var resume = parseResult.GetValue(resumeOption);
|
|
||||||
var verbose = parseResult.GetValue(verboseOption);
|
|
||||||
return CommandHandlers.HandleExcititorInitAsync(services, providers, resume, verbose, cancellationToken);
|
|
||||||
});
|
|
||||||
|
|
||||||
var pull = new Command("pull", "Trigger Excititor ingest for configured providers.");
|
|
||||||
var pullProviders = new Option<string[]>("--provider", new[] { "-p" })
|
|
||||||
{
|
|
||||||
Description = "Optional provider identifier(s) to ingest.",
|
|
||||||
Arity = ArgumentArity.ZeroOrMore
|
|
||||||
};
|
|
||||||
var sinceOption = new Option<DateTimeOffset?>("--since")
|
|
||||||
{
|
|
||||||
Description = "Optional ISO-8601 timestamp to begin the ingest window."
|
|
||||||
};
|
|
||||||
var windowOption = new Option<TimeSpan?>("--window")
|
|
||||||
{
|
|
||||||
Description = "Optional window duration (e.g. 24:00:00)."
|
|
||||||
};
|
|
||||||
var forceOption = new Option<bool>("--force")
|
|
||||||
{
|
|
||||||
Description = "Force ingestion even if the backend reports no pending work."
|
|
||||||
};
|
|
||||||
pull.Add(pullProviders);
|
|
||||||
pull.Add(sinceOption);
|
|
||||||
pull.Add(windowOption);
|
|
||||||
pull.Add(forceOption);
|
|
||||||
pull.SetAction((parseResult, _) =>
|
|
||||||
{
|
|
||||||
var providers = parseResult.GetValue(pullProviders) ?? Array.Empty<string>();
|
|
||||||
var since = parseResult.GetValue(sinceOption);
|
|
||||||
var window = parseResult.GetValue(windowOption);
|
|
||||||
var force = parseResult.GetValue(forceOption);
|
|
||||||
var verbose = parseResult.GetValue(verboseOption);
|
|
||||||
return CommandHandlers.HandleExcititorPullAsync(services, providers, since, window, force, verbose, cancellationToken);
|
|
||||||
});
|
|
||||||
|
|
||||||
var resume = new Command("resume", "Resume Excititor ingest using a checkpoint token.");
|
|
||||||
var resumeProviders = new Option<string[]>("--provider", new[] { "-p" })
|
|
||||||
{
|
|
||||||
Description = "Optional provider identifier(s) to resume.",
|
|
||||||
Arity = ArgumentArity.ZeroOrMore
|
|
||||||
};
|
|
||||||
var checkpointOption = new Option<string?>("--checkpoint")
|
|
||||||
{
|
|
||||||
Description = "Optional checkpoint identifier to resume from."
|
|
||||||
};
|
|
||||||
resume.Add(resumeProviders);
|
|
||||||
resume.Add(checkpointOption);
|
|
||||||
resume.SetAction((parseResult, _) =>
|
|
||||||
{
|
|
||||||
var providers = parseResult.GetValue(resumeProviders) ?? Array.Empty<string>();
|
|
||||||
var checkpoint = parseResult.GetValue(checkpointOption);
|
|
||||||
var verbose = parseResult.GetValue(verboseOption);
|
|
||||||
return CommandHandlers.HandleExcititorResumeAsync(services, providers, checkpoint, verbose, cancellationToken);
|
|
||||||
});
|
|
||||||
|
|
||||||
var list = new Command("list-providers", "List Excititor providers and their ingest status.");
|
|
||||||
var includeDisabledOption = new Option<bool>("--include-disabled")
|
|
||||||
{
|
|
||||||
Description = "Include disabled providers in the listing."
|
|
||||||
};
|
|
||||||
list.Add(includeDisabledOption);
|
|
||||||
list.SetAction((parseResult, _) =>
|
|
||||||
{
|
|
||||||
var includeDisabled = parseResult.GetValue(includeDisabledOption);
|
|
||||||
var verbose = parseResult.GetValue(verboseOption);
|
|
||||||
return CommandHandlers.HandleExcititorListProvidersAsync(services, includeDisabled, verbose, cancellationToken);
|
|
||||||
});
|
|
||||||
|
|
||||||
var export = new Command("export", "Trigger Excititor export generation.");
|
|
||||||
var formatOption = new Option<string>("--format")
|
|
||||||
{
|
|
||||||
Description = "Export format (e.g. openvex, json)."
|
|
||||||
};
|
|
||||||
var exportDeltaOption = new Option<bool>("--delta")
|
|
||||||
{
|
|
||||||
Description = "Request a delta export when supported."
|
|
||||||
};
|
|
||||||
var exportScopeOption = new Option<string?>("--scope")
|
|
||||||
{
|
|
||||||
Description = "Optional policy scope or tenant identifier."
|
|
||||||
};
|
|
||||||
var exportSinceOption = new Option<DateTimeOffset?>("--since")
|
|
||||||
{
|
|
||||||
Description = "Optional ISO-8601 timestamp to restrict export contents."
|
|
||||||
};
|
|
||||||
var exportProviderOption = new Option<string?>("--provider")
|
|
||||||
{
|
|
||||||
Description = "Optional provider identifier when requesting targeted exports."
|
|
||||||
};
|
|
||||||
var exportOutputOption = new Option<string?>("--output")
|
|
||||||
{
|
|
||||||
Description = "Optional path to download the export artifact."
|
|
||||||
};
|
|
||||||
export.Add(formatOption);
|
|
||||||
export.Add(exportDeltaOption);
|
|
||||||
export.Add(exportScopeOption);
|
|
||||||
export.Add(exportSinceOption);
|
|
||||||
export.Add(exportProviderOption);
|
|
||||||
export.Add(exportOutputOption);
|
|
||||||
export.SetAction((parseResult, _) =>
|
|
||||||
{
|
|
||||||
var format = parseResult.GetValue(formatOption) ?? "openvex";
|
|
||||||
var delta = parseResult.GetValue(exportDeltaOption);
|
|
||||||
var scope = parseResult.GetValue(exportScopeOption);
|
|
||||||
var since = parseResult.GetValue(exportSinceOption);
|
|
||||||
var provider = parseResult.GetValue(exportProviderOption);
|
|
||||||
var output = parseResult.GetValue(exportOutputOption);
|
|
||||||
var verbose = parseResult.GetValue(verboseOption);
|
|
||||||
return CommandHandlers.HandleExcititorExportAsync(services, format, delta, scope, since, provider, output, verbose, cancellationToken);
|
|
||||||
});
|
|
||||||
|
|
||||||
var backfill = new Command("backfill-statements", "Replay historical raw documents into Excititor statements.");
|
|
||||||
var backfillRetrievedSinceOption = new Option<DateTimeOffset?>("--retrieved-since")
|
|
||||||
{
|
|
||||||
Description = "Only process raw documents retrieved on or after the provided ISO-8601 timestamp."
|
|
||||||
};
|
|
||||||
var backfillForceOption = new Option<bool>("--force")
|
|
||||||
{
|
|
||||||
Description = "Reprocess documents even if statements already exist."
|
|
||||||
};
|
|
||||||
var backfillBatchSizeOption = new Option<int>("--batch-size")
|
|
||||||
{
|
|
||||||
Description = "Number of raw documents to fetch per batch (default 100)."
|
|
||||||
};
|
|
||||||
var backfillMaxDocumentsOption = new Option<int?>("--max-documents")
|
|
||||||
{
|
|
||||||
Description = "Optional maximum number of raw documents to process."
|
|
||||||
};
|
|
||||||
backfill.Add(backfillRetrievedSinceOption);
|
|
||||||
backfill.Add(backfillForceOption);
|
|
||||||
backfill.Add(backfillBatchSizeOption);
|
|
||||||
backfill.Add(backfillMaxDocumentsOption);
|
|
||||||
backfill.SetAction((parseResult, _) =>
|
|
||||||
{
|
|
||||||
var retrievedSince = parseResult.GetValue(backfillRetrievedSinceOption);
|
|
||||||
var force = parseResult.GetValue(backfillForceOption);
|
|
||||||
var batchSize = parseResult.GetValue(backfillBatchSizeOption);
|
|
||||||
if (batchSize <= 0)
|
|
||||||
{
|
|
||||||
batchSize = 100;
|
|
||||||
}
|
|
||||||
var maxDocuments = parseResult.GetValue(backfillMaxDocumentsOption);
|
|
||||||
var verbose = parseResult.GetValue(verboseOption);
|
|
||||||
return CommandHandlers.HandleExcititorBackfillStatementsAsync(
|
|
||||||
services,
|
|
||||||
retrievedSince,
|
|
||||||
force,
|
|
||||||
batchSize,
|
|
||||||
maxDocuments,
|
|
||||||
verbose,
|
|
||||||
cancellationToken);
|
|
||||||
});
|
|
||||||
|
|
||||||
var verify = new Command("verify", "Verify Excititor exports or attestations.");
|
|
||||||
var exportIdOption = new Option<string?>("--export-id")
|
|
||||||
{
|
|
||||||
Description = "Export identifier to verify."
|
|
||||||
};
|
|
||||||
var digestOption = new Option<string?>("--digest")
|
|
||||||
{
|
|
||||||
Description = "Expected digest for the export or attestation."
|
|
||||||
};
|
|
||||||
var attestationOption = new Option<string?>("--attestation")
|
|
||||||
{
|
|
||||||
Description = "Path to a local attestation file to verify (base64 content will be uploaded)."
|
|
||||||
};
|
|
||||||
verify.Add(exportIdOption);
|
|
||||||
verify.Add(digestOption);
|
|
||||||
verify.Add(attestationOption);
|
|
||||||
verify.SetAction((parseResult, _) =>
|
|
||||||
{
|
|
||||||
var exportId = parseResult.GetValue(exportIdOption);
|
|
||||||
var digest = parseResult.GetValue(digestOption);
|
|
||||||
var attestation = parseResult.GetValue(attestationOption);
|
|
||||||
var verbose = parseResult.GetValue(verboseOption);
|
|
||||||
return CommandHandlers.HandleExcititorVerifyAsync(services, exportId, digest, attestation, verbose, cancellationToken);
|
|
||||||
});
|
|
||||||
|
|
||||||
var reconcile = new Command("reconcile", "Trigger Excititor reconciliation against canonical advisories.");
|
|
||||||
var reconcileProviders = new Option<string[]>("--provider", new[] { "-p" })
|
|
||||||
{
|
|
||||||
Description = "Optional provider identifier(s) to reconcile.",
|
|
||||||
Arity = ArgumentArity.ZeroOrMore
|
|
||||||
};
|
|
||||||
var maxAgeOption = new Option<TimeSpan?>("--max-age")
|
|
||||||
{
|
|
||||||
Description = "Optional maximum age window (e.g. 7.00:00:00)."
|
|
||||||
};
|
|
||||||
reconcile.Add(reconcileProviders);
|
|
||||||
reconcile.Add(maxAgeOption);
|
|
||||||
reconcile.SetAction((parseResult, _) =>
|
|
||||||
{
|
|
||||||
var providers = parseResult.GetValue(reconcileProviders) ?? Array.Empty<string>();
|
|
||||||
var maxAge = parseResult.GetValue(maxAgeOption);
|
|
||||||
var verbose = parseResult.GetValue(verboseOption);
|
|
||||||
return CommandHandlers.HandleExcititorReconcileAsync(services, providers, maxAge, verbose, cancellationToken);
|
|
||||||
});
|
|
||||||
|
|
||||||
excititor.Add(init);
|
|
||||||
excititor.Add(pull);
|
|
||||||
excititor.Add(resume);
|
|
||||||
excititor.Add(list);
|
|
||||||
excititor.Add(export);
|
|
||||||
excititor.Add(backfill);
|
|
||||||
excititor.Add(verify);
|
|
||||||
excititor.Add(reconcile);
|
|
||||||
return excititor;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Command BuildRuntimeCommand(IServiceProvider services, Option<bool> verboseOption, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var runtime = new Command("runtime", "Interact with runtime admission policy APIs.");
|
|
||||||
var policy = new Command("policy", "Runtime policy operations.");
|
|
||||||
|
|
||||||
var test = new Command("test", "Evaluate runtime policy decisions for image digests.");
|
|
||||||
var namespaceOption = new Option<string?>("--namespace", new[] { "--ns" })
|
|
||||||
{
|
|
||||||
Description = "Namespace or logical scope for the evaluation."
|
|
||||||
};
|
|
||||||
|
|
||||||
var imageOption = new Option<string[]>("--image", new[] { "-i", "--images" })
|
|
||||||
{
|
|
||||||
Description = "Image digests to evaluate (repeatable).",
|
|
||||||
Arity = ArgumentArity.ZeroOrMore
|
|
||||||
};
|
|
||||||
|
|
||||||
var fileOption = new Option<string?>("--file", new[] { "-f" })
|
|
||||||
{
|
|
||||||
Description = "Path to a file containing image digests (one per line)."
|
|
||||||
};
|
|
||||||
|
|
||||||
var labelOption = new Option<string[]>("--label", new[] { "-l", "--labels" })
|
|
||||||
{
|
|
||||||
Description = "Pod labels in key=value format (repeatable).",
|
|
||||||
Arity = ArgumentArity.ZeroOrMore
|
|
||||||
};
|
|
||||||
|
|
||||||
var jsonOption = new Option<bool>("--json")
|
|
||||||
{
|
|
||||||
Description = "Emit the raw JSON response."
|
|
||||||
};
|
|
||||||
|
|
||||||
test.Add(namespaceOption);
|
|
||||||
test.Add(imageOption);
|
|
||||||
test.Add(fileOption);
|
|
||||||
test.Add(labelOption);
|
|
||||||
test.Add(jsonOption);
|
|
||||||
|
|
||||||
test.SetAction((parseResult, _) =>
|
|
||||||
{
|
|
||||||
var nsValue = parseResult.GetValue(namespaceOption);
|
|
||||||
var images = parseResult.GetValue(imageOption) ?? Array.Empty<string>();
|
|
||||||
var file = parseResult.GetValue(fileOption);
|
|
||||||
var labels = parseResult.GetValue(labelOption) ?? Array.Empty<string>();
|
|
||||||
var outputJson = parseResult.GetValue(jsonOption);
|
|
||||||
var verbose = parseResult.GetValue(verboseOption);
|
|
||||||
|
|
||||||
return CommandHandlers.HandleRuntimePolicyTestAsync(
|
|
||||||
services,
|
|
||||||
nsValue,
|
|
||||||
images,
|
|
||||||
file,
|
|
||||||
labels,
|
|
||||||
outputJson,
|
|
||||||
verbose,
|
|
||||||
cancellationToken);
|
|
||||||
});
|
|
||||||
|
|
||||||
policy.Add(test);
|
|
||||||
runtime.Add(policy);
|
|
||||||
return runtime;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Command BuildAuthCommand(IServiceProvider services, StellaOpsCliOptions options, Option<bool> verboseOption, CancellationToken cancellationToken)
|
private static Command BuildAuthCommand(IServiceProvider services, StellaOpsCliOptions options, Option<bool> verboseOption, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var auth = new Command("auth", "Manage authentication with StellaOps Authority.");
|
var auth = new Command("auth", "Manage authentication with StellaOps Authority.");
|
||||||
@@ -607,97 +322,6 @@ internal static class CommandFactory
|
|||||||
return auth;
|
return auth;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Command BuildOfflineCommand(IServiceProvider services, Option<bool> verboseOption, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var offline = new Command("offline", "Offline kit workflows and utilities.");
|
|
||||||
|
|
||||||
var kit = new Command("kit", "Manage offline kit bundles.");
|
|
||||||
|
|
||||||
var pull = new Command("pull", "Download the latest offline kit bundle.");
|
|
||||||
var bundleIdOption = new Option<string?>("--bundle-id")
|
|
||||||
{
|
|
||||||
Description = "Optional bundle identifier. Defaults to the latest available."
|
|
||||||
};
|
|
||||||
var destinationOption = new Option<string?>("--destination")
|
|
||||||
{
|
|
||||||
Description = "Directory to store downloaded bundles (defaults to the configured offline kits directory)."
|
|
||||||
};
|
|
||||||
var overwriteOption = new Option<bool>("--overwrite")
|
|
||||||
{
|
|
||||||
Description = "Overwrite existing files even if checksums match."
|
|
||||||
};
|
|
||||||
var noResumeOption = new Option<bool>("--no-resume")
|
|
||||||
{
|
|
||||||
Description = "Disable resuming partial downloads."
|
|
||||||
};
|
|
||||||
|
|
||||||
pull.Add(bundleIdOption);
|
|
||||||
pull.Add(destinationOption);
|
|
||||||
pull.Add(overwriteOption);
|
|
||||||
pull.Add(noResumeOption);
|
|
||||||
pull.SetAction((parseResult, _) =>
|
|
||||||
{
|
|
||||||
var bundleId = parseResult.GetValue(bundleIdOption);
|
|
||||||
var destination = parseResult.GetValue(destinationOption);
|
|
||||||
var overwrite = parseResult.GetValue(overwriteOption);
|
|
||||||
var resume = !parseResult.GetValue(noResumeOption);
|
|
||||||
var verbose = parseResult.GetValue(verboseOption);
|
|
||||||
return CommandHandlers.HandleOfflineKitPullAsync(services, bundleId, destination, overwrite, resume, verbose, cancellationToken);
|
|
||||||
});
|
|
||||||
|
|
||||||
var import = new Command("import", "Upload an offline kit bundle to the backend.");
|
|
||||||
var bundleArgument = new Argument<string>("bundle")
|
|
||||||
{
|
|
||||||
Description = "Path to the offline kit tarball (.tgz)."
|
|
||||||
};
|
|
||||||
var manifestOption = new Option<string?>("--manifest")
|
|
||||||
{
|
|
||||||
Description = "Offline manifest JSON path (defaults to metadata or sibling file)."
|
|
||||||
};
|
|
||||||
var bundleSignatureOption = new Option<string?>("--bundle-signature")
|
|
||||||
{
|
|
||||||
Description = "Detached signature for the offline bundle (e.g. .sig)."
|
|
||||||
};
|
|
||||||
var manifestSignatureOption = new Option<string?>("--manifest-signature")
|
|
||||||
{
|
|
||||||
Description = "Detached signature for the offline manifest (e.g. .jws)."
|
|
||||||
};
|
|
||||||
|
|
||||||
import.Add(bundleArgument);
|
|
||||||
import.Add(manifestOption);
|
|
||||||
import.Add(bundleSignatureOption);
|
|
||||||
import.Add(manifestSignatureOption);
|
|
||||||
import.SetAction((parseResult, _) =>
|
|
||||||
{
|
|
||||||
var bundlePath = parseResult.GetValue(bundleArgument) ?? string.Empty;
|
|
||||||
var manifest = parseResult.GetValue(manifestOption);
|
|
||||||
var bundleSignature = parseResult.GetValue(bundleSignatureOption);
|
|
||||||
var manifestSignature = parseResult.GetValue(manifestSignatureOption);
|
|
||||||
var verbose = parseResult.GetValue(verboseOption);
|
|
||||||
return CommandHandlers.HandleOfflineKitImportAsync(services, bundlePath, manifest, bundleSignature, manifestSignature, verbose, cancellationToken);
|
|
||||||
});
|
|
||||||
|
|
||||||
var status = new Command("status", "Display offline kit installation status.");
|
|
||||||
var jsonOption = new Option<bool>("--json")
|
|
||||||
{
|
|
||||||
Description = "Emit status as JSON."
|
|
||||||
};
|
|
||||||
status.Add(jsonOption);
|
|
||||||
status.SetAction((parseResult, _) =>
|
|
||||||
{
|
|
||||||
var asJson = parseResult.GetValue(jsonOption);
|
|
||||||
var verbose = parseResult.GetValue(verboseOption);
|
|
||||||
return CommandHandlers.HandleOfflineKitStatusAsync(services, asJson, verbose, cancellationToken);
|
|
||||||
});
|
|
||||||
|
|
||||||
kit.Add(pull);
|
|
||||||
kit.Add(import);
|
|
||||||
kit.Add(status);
|
|
||||||
|
|
||||||
offline.Add(kit);
|
|
||||||
return offline;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Command BuildConfigCommand(StellaOpsCliOptions options)
|
private static Command BuildConfigCommand(StellaOpsCliOptions options)
|
||||||
{
|
{
|
||||||
var config = new Command("config", "Inspect CLI configuration state.");
|
var config = new Command("config", "Inspect CLI configuration state.");
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using StellaOps.Configuration;
|
using StellaOps.Configuration;
|
||||||
using StellaOps.Auth.Abstractions;
|
using StellaOps.Auth.Abstractions;
|
||||||
@@ -234,6 +235,93 @@ public static class CliBootstrapper
|
|||||||
"Offline:MirrorUrl");
|
"Offline:MirrorUrl");
|
||||||
|
|
||||||
offline.MirrorUrl = string.IsNullOrWhiteSpace(mirror) ? null : mirror.Trim();
|
offline.MirrorUrl = string.IsNullOrWhiteSpace(mirror) ? null : mirror.Trim();
|
||||||
|
|
||||||
|
cliOptions.Plugins ??= new StellaOpsCliPluginOptions();
|
||||||
|
var pluginOptions = cliOptions.Plugins;
|
||||||
|
|
||||||
|
pluginOptions.BaseDirectory = ResolveWithFallback(
|
||||||
|
pluginOptions.BaseDirectory,
|
||||||
|
configuration,
|
||||||
|
"STELLAOPS_CLI_PLUGIN_BASE_DIRECTORY",
|
||||||
|
"StellaOps:Plugins:BaseDirectory",
|
||||||
|
"Plugins:BaseDirectory");
|
||||||
|
|
||||||
|
pluginOptions.BaseDirectory = (pluginOptions.BaseDirectory ?? string.Empty).Trim();
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(pluginOptions.BaseDirectory))
|
||||||
|
{
|
||||||
|
pluginOptions.BaseDirectory = AppContext.BaseDirectory;
|
||||||
|
}
|
||||||
|
|
||||||
|
pluginOptions.BaseDirectory = Path.GetFullPath(pluginOptions.BaseDirectory);
|
||||||
|
|
||||||
|
pluginOptions.Directory = ResolveWithFallback(
|
||||||
|
pluginOptions.Directory,
|
||||||
|
configuration,
|
||||||
|
"STELLAOPS_CLI_PLUGIN_DIRECTORY",
|
||||||
|
"StellaOps:Plugins:Directory",
|
||||||
|
"Plugins:Directory");
|
||||||
|
|
||||||
|
pluginOptions.Directory = (pluginOptions.Directory ?? string.Empty).Trim();
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(pluginOptions.Directory))
|
||||||
|
{
|
||||||
|
pluginOptions.Directory = Path.Combine("plugins", "cli");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Path.IsPathRooted(pluginOptions.Directory))
|
||||||
|
{
|
||||||
|
pluginOptions.Directory = Path.GetFullPath(Path.Combine(pluginOptions.BaseDirectory, pluginOptions.Directory));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
pluginOptions.Directory = Path.GetFullPath(pluginOptions.Directory);
|
||||||
|
}
|
||||||
|
|
||||||
|
pluginOptions.ManifestSearchPattern = ResolveWithFallback(
|
||||||
|
pluginOptions.ManifestSearchPattern,
|
||||||
|
configuration,
|
||||||
|
"STELLAOPS_CLI_PLUGIN_MANIFEST_PATTERN",
|
||||||
|
"StellaOps:Plugins:ManifestSearchPattern",
|
||||||
|
"Plugins:ManifestSearchPattern");
|
||||||
|
|
||||||
|
pluginOptions.ManifestSearchPattern = (pluginOptions.ManifestSearchPattern ?? string.Empty).Trim();
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(pluginOptions.ManifestSearchPattern))
|
||||||
|
{
|
||||||
|
pluginOptions.ManifestSearchPattern = "*.manifest.json";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pluginOptions.SearchPatterns is null || pluginOptions.SearchPatterns.Count == 0)
|
||||||
|
{
|
||||||
|
pluginOptions.SearchPatterns = new List<string> { "StellaOps.Cli.Plugin.*.dll" };
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
pluginOptions.SearchPatterns = pluginOptions.SearchPatterns
|
||||||
|
.Where(pattern => !string.IsNullOrWhiteSpace(pattern))
|
||||||
|
.Select(pattern => pattern.Trim())
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (pluginOptions.SearchPatterns.Count == 0)
|
||||||
|
{
|
||||||
|
pluginOptions.SearchPatterns.Add("StellaOps.Cli.Plugin.*.dll");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pluginOptions.PluginOrder is null)
|
||||||
|
{
|
||||||
|
pluginOptions.PluginOrder = new List<string>();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
pluginOptions.PluginOrder = pluginOptions.PluginOrder
|
||||||
|
.Where(name => !string.IsNullOrWhiteSpace(name))
|
||||||
|
.Select(name => name.Trim())
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using StellaOps.Auth.Abstractions;
|
using StellaOps.Auth.Abstractions;
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
namespace StellaOps.Cli.Configuration;
|
namespace StellaOps.Cli.Configuration;
|
||||||
|
|
||||||
@@ -25,6 +26,8 @@ public sealed class StellaOpsCliOptions
|
|||||||
public StellaOpsCliAuthorityOptions Authority { get; set; } = new();
|
public StellaOpsCliAuthorityOptions Authority { get; set; } = new();
|
||||||
|
|
||||||
public StellaOpsCliOfflineOptions Offline { get; set; } = new();
|
public StellaOpsCliOfflineOptions Offline { get; set; } = new();
|
||||||
|
|
||||||
|
public StellaOpsCliPluginOptions Plugins { get; set; } = new();
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class StellaOpsCliAuthorityOptions
|
public sealed class StellaOpsCliAuthorityOptions
|
||||||
@@ -63,3 +66,16 @@ public sealed class StellaOpsCliOfflineOptions
|
|||||||
|
|
||||||
public string? MirrorUrl { get; set; }
|
public string? MirrorUrl { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sealed class StellaOpsCliPluginOptions
|
||||||
|
{
|
||||||
|
public string BaseDirectory { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string Directory { get; set; } = "plugins/cli";
|
||||||
|
|
||||||
|
public IList<string> SearchPatterns { get; set; } = new List<string>();
|
||||||
|
|
||||||
|
public IList<string> PluginOrder { get; set; } = new List<string>();
|
||||||
|
|
||||||
|
public string ManifestSearchPattern { get; set; } = "*.manifest.json";
|
||||||
|
}
|
||||||
|
|||||||
278
src/StellaOps.Cli/Plugins/CliCommandModuleLoader.cs
Normal file
278
src/StellaOps.Cli/Plugins/CliCommandModuleLoader.cs
Normal file
@@ -0,0 +1,278 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.CommandLine;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using StellaOps.Cli.Configuration;
|
||||||
|
using StellaOps.Plugin.Hosting;
|
||||||
|
|
||||||
|
namespace StellaOps.Cli.Plugins;
|
||||||
|
|
||||||
|
internal sealed class CliCommandModuleLoader
|
||||||
|
{
|
||||||
|
private readonly IServiceProvider _services;
|
||||||
|
private readonly StellaOpsCliOptions _options;
|
||||||
|
private readonly ILogger<CliCommandModuleLoader> _logger;
|
||||||
|
private readonly RestartOnlyCliPluginGuard _guard = new();
|
||||||
|
|
||||||
|
private IReadOnlyList<ICliCommandModule> _modules = Array.Empty<ICliCommandModule>();
|
||||||
|
private bool _loaded;
|
||||||
|
|
||||||
|
public CliCommandModuleLoader(
|
||||||
|
IServiceProvider services,
|
||||||
|
StellaOpsCliOptions options,
|
||||||
|
ILogger<CliCommandModuleLoader> logger)
|
||||||
|
{
|
||||||
|
_services = services ?? throw new ArgumentNullException(nameof(services));
|
||||||
|
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||||
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyList<ICliCommandModule> LoadModules()
|
||||||
|
{
|
||||||
|
if (_loaded)
|
||||||
|
{
|
||||||
|
return _modules;
|
||||||
|
}
|
||||||
|
|
||||||
|
var pluginOptions = _options.Plugins ?? new StellaOpsCliPluginOptions();
|
||||||
|
|
||||||
|
var baseDirectory = ResolveBaseDirectory(pluginOptions);
|
||||||
|
var pluginsDirectory = ResolvePluginsDirectory(pluginOptions, baseDirectory);
|
||||||
|
var searchPatterns = ResolveSearchPatterns(pluginOptions);
|
||||||
|
var manifestPattern = string.IsNullOrWhiteSpace(pluginOptions.ManifestSearchPattern)
|
||||||
|
? "*.manifest.json"
|
||||||
|
: pluginOptions.ManifestSearchPattern;
|
||||||
|
|
||||||
|
_logger.LogDebug("Loading CLI plug-ins from '{Directory}' (base: '{Base}').", pluginsDirectory, baseDirectory);
|
||||||
|
|
||||||
|
var manifestLoader = new CliPluginManifestLoader(pluginsDirectory, manifestPattern);
|
||||||
|
IReadOnlyList<CliPluginManifest> manifests;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
manifests = manifestLoader.LoadAsync(CancellationToken.None).GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Failed to enumerate CLI plug-in manifests from '{Directory}'.", pluginsDirectory);
|
||||||
|
manifests = Array.Empty<CliPluginManifest>();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (manifests.Count == 0)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("No CLI plug-in manifests discovered under '{Directory}'.", pluginsDirectory);
|
||||||
|
_loaded = true;
|
||||||
|
_guard.Seal();
|
||||||
|
_modules = Array.Empty<ICliCommandModule>();
|
||||||
|
return _modules;
|
||||||
|
}
|
||||||
|
|
||||||
|
var hostOptions = new PluginHostOptions
|
||||||
|
{
|
||||||
|
BaseDirectory = baseDirectory,
|
||||||
|
PluginsDirectory = pluginsDirectory,
|
||||||
|
EnsureDirectoryExists = false,
|
||||||
|
RecursiveSearch = true,
|
||||||
|
PrimaryPrefix = "StellaOps.Cli"
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var pattern in searchPatterns)
|
||||||
|
{
|
||||||
|
hostOptions.SearchPatterns.Add(pattern);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var ordered in pluginOptions.PluginOrder ?? Array.Empty<string>())
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(ordered))
|
||||||
|
{
|
||||||
|
hostOptions.PluginOrder.Add(ordered);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var loadResult = PluginHost.LoadPlugins(hostOptions, _logger);
|
||||||
|
|
||||||
|
var assemblies = loadResult.Plugins.ToDictionary(
|
||||||
|
descriptor => Normalize(descriptor.AssemblyPath),
|
||||||
|
descriptor => descriptor.Assembly,
|
||||||
|
StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
var modules = new List<ICliCommandModule>(manifests.Count);
|
||||||
|
|
||||||
|
foreach (var manifest in manifests)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var assemblyPath = ResolveAssemblyPath(manifest);
|
||||||
|
_guard.EnsureRegistrationAllowed(assemblyPath);
|
||||||
|
|
||||||
|
if (!assemblies.TryGetValue(assemblyPath, out var assembly))
|
||||||
|
{
|
||||||
|
if (!File.Exists(assemblyPath))
|
||||||
|
{
|
||||||
|
throw new FileNotFoundException($"Plug-in assembly '{assemblyPath}' referenced by manifest '{manifest.Id}' was not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
assembly = Assembly.LoadFrom(assemblyPath);
|
||||||
|
assemblies[assemblyPath] = assembly;
|
||||||
|
}
|
||||||
|
|
||||||
|
var module = CreateModule(assembly, manifest);
|
||||||
|
if (module is null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
modules.Add(module);
|
||||||
|
_logger.LogInformation("Registered CLI plug-in '{PluginId}' ({PluginName}) from '{AssemblyPath}'.", manifest.Id, module.Name, assemblyPath);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Failed to register CLI plug-in '{PluginId}'.", manifest.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_modules = modules;
|
||||||
|
_loaded = true;
|
||||||
|
_guard.Seal();
|
||||||
|
return _modules;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RegisterModules(RootCommand root, Option<bool> verboseOption, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (root is null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(root));
|
||||||
|
}
|
||||||
|
if (verboseOption is null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(verboseOption));
|
||||||
|
}
|
||||||
|
|
||||||
|
var modules = LoadModules();
|
||||||
|
if (modules.Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var module in modules)
|
||||||
|
{
|
||||||
|
if (!module.IsAvailable(_services))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("CLI plug-in '{Name}' reported unavailable; skipping registration.", module.Name);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
module.RegisterCommands(root, _services, _options, verboseOption, cancellationToken);
|
||||||
|
_logger.LogInformation("CLI plug-in '{Name}' commands registered.", module.Name);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "CLI plug-in '{Name}' failed to register commands.", module.Name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ResolveAssemblyPath(CliPluginManifest manifest)
|
||||||
|
{
|
||||||
|
if (manifest.EntryPoint is null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Manifest '{manifest.SourcePath}' does not define an entry point.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var assemblyPath = manifest.EntryPoint.Assembly;
|
||||||
|
if (string.IsNullOrWhiteSpace(assemblyPath))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Manifest '{manifest.SourcePath}' specifies an empty assembly path.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Path.IsPathRooted(assemblyPath))
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(manifest.SourceDirectory))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Manifest '{manifest.SourcePath}' cannot resolve relative assembly path without source directory metadata.");
|
||||||
|
}
|
||||||
|
|
||||||
|
assemblyPath = Path.Combine(manifest.SourceDirectory, assemblyPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Normalize(assemblyPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ICliCommandModule? CreateModule(Assembly assembly, CliPluginManifest manifest)
|
||||||
|
{
|
||||||
|
if (manifest.EntryPoint is null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var type = assembly.GetType(manifest.EntryPoint.TypeName, throwOnError: true);
|
||||||
|
if (type is null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Plug-in type '{manifest.EntryPoint.TypeName}' could not be loaded from assembly '{assembly.FullName}'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var module = ActivatorUtilities.CreateInstance(_services, type) as ICliCommandModule;
|
||||||
|
if (module is null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Plug-in type '{manifest.EntryPoint.TypeName}' does not implement {nameof(ICliCommandModule)}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return module;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ResolveBaseDirectory(StellaOpsCliPluginOptions options)
|
||||||
|
{
|
||||||
|
var baseDirectory = options.BaseDirectory;
|
||||||
|
if (string.IsNullOrWhiteSpace(baseDirectory))
|
||||||
|
{
|
||||||
|
baseDirectory = AppContext.BaseDirectory;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Path.GetFullPath(baseDirectory);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ResolvePluginsDirectory(StellaOpsCliPluginOptions options, string baseDirectory)
|
||||||
|
{
|
||||||
|
var directory = options.Directory;
|
||||||
|
if (string.IsNullOrWhiteSpace(directory))
|
||||||
|
{
|
||||||
|
directory = Path.Combine("plugins", "cli");
|
||||||
|
}
|
||||||
|
|
||||||
|
directory = directory.Trim();
|
||||||
|
|
||||||
|
if (!Path.IsPathRooted(directory))
|
||||||
|
{
|
||||||
|
directory = Path.Combine(baseDirectory, directory);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Path.GetFullPath(directory);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IReadOnlyList<string> ResolveSearchPatterns(StellaOpsCliPluginOptions options)
|
||||||
|
{
|
||||||
|
if (options.SearchPatterns is null || options.SearchPatterns.Count == 0)
|
||||||
|
{
|
||||||
|
return new[] { "StellaOps.Cli.Plugin.*.dll" };
|
||||||
|
}
|
||||||
|
|
||||||
|
return options.SearchPatterns
|
||||||
|
.Where(pattern => !string.IsNullOrWhiteSpace(pattern))
|
||||||
|
.Select(pattern => pattern.Trim())
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Normalize(string path)
|
||||||
|
{
|
||||||
|
var full = Path.GetFullPath(path);
|
||||||
|
return full.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
|
||||||
|
}
|
||||||
|
}
|
||||||
39
src/StellaOps.Cli/Plugins/CliPluginManifest.cs
Normal file
39
src/StellaOps.Cli/Plugins/CliPluginManifest.cs
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace StellaOps.Cli.Plugins;
|
||||||
|
|
||||||
|
public sealed record CliPluginManifest
|
||||||
|
{
|
||||||
|
public const string CurrentSchemaVersion = "1.0";
|
||||||
|
|
||||||
|
public string SchemaVersion { get; init; } = CurrentSchemaVersion;
|
||||||
|
|
||||||
|
public string Id { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
public string DisplayName { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
public string Version { get; init; } = "0.0.0";
|
||||||
|
|
||||||
|
public bool RequiresRestart { get; init; } = true;
|
||||||
|
|
||||||
|
public CliPluginEntryPoint? EntryPoint { get; init; }
|
||||||
|
|
||||||
|
public IReadOnlyList<string> Capabilities { get; init; } = Array.Empty<string>();
|
||||||
|
|
||||||
|
public IReadOnlyDictionary<string, string> Metadata { get; init; } =
|
||||||
|
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
public string? SourcePath { get; init; }
|
||||||
|
|
||||||
|
public string? SourceDirectory { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record CliPluginEntryPoint
|
||||||
|
{
|
||||||
|
public string Type { get; init; } = "dotnet";
|
||||||
|
|
||||||
|
public string Assembly { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
public string TypeName { get; init; } = string.Empty;
|
||||||
|
}
|
||||||
150
src/StellaOps.Cli/Plugins/CliPluginManifestLoader.cs
Normal file
150
src/StellaOps.Cli/Plugins/CliPluginManifestLoader.cs
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace StellaOps.Cli.Plugins;
|
||||||
|
|
||||||
|
internal sealed class CliPluginManifestLoader
|
||||||
|
{
|
||||||
|
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web)
|
||||||
|
{
|
||||||
|
AllowTrailingCommas = true,
|
||||||
|
ReadCommentHandling = JsonCommentHandling.Skip,
|
||||||
|
PropertyNameCaseInsensitive = true
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly string _directory;
|
||||||
|
private readonly string _searchPattern;
|
||||||
|
|
||||||
|
public CliPluginManifestLoader(string directory, string searchPattern)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(directory))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Plug-in manifest directory is required.", nameof(directory));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(searchPattern))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Manifest search pattern is required.", nameof(searchPattern));
|
||||||
|
}
|
||||||
|
|
||||||
|
_directory = Path.GetFullPath(directory);
|
||||||
|
_searchPattern = searchPattern;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<CliPluginManifest>> LoadAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (!Directory.Exists(_directory))
|
||||||
|
{
|
||||||
|
return Array.Empty<CliPluginManifest>();
|
||||||
|
}
|
||||||
|
|
||||||
|
var manifests = new List<CliPluginManifest>();
|
||||||
|
|
||||||
|
foreach (var file in Directory.EnumerateFiles(_directory, _searchPattern, SearchOption.AllDirectories))
|
||||||
|
{
|
||||||
|
if (IsHidden(file))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var manifest = await DeserializeAsync(file, cancellationToken).ConfigureAwait(false);
|
||||||
|
manifests.Add(manifest);
|
||||||
|
}
|
||||||
|
|
||||||
|
return manifests
|
||||||
|
.OrderBy(static m => m.Id, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ThenBy(static m => m.Version, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsHidden(string path)
|
||||||
|
{
|
||||||
|
var directory = Path.GetDirectoryName(path);
|
||||||
|
while (!string.IsNullOrEmpty(directory))
|
||||||
|
{
|
||||||
|
var name = Path.GetFileName(directory);
|
||||||
|
if (name.StartsWith(".", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
directory = Path.GetDirectoryName(directory);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<CliPluginManifest> DeserializeAsync(string file, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await using var stream = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, FileOptions.Asynchronous);
|
||||||
|
CliPluginManifest? manifest;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
manifest = await JsonSerializer.DeserializeAsync<CliPluginManifest>(stream, SerializerOptions, cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (JsonException ex)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Failed to parse CLI plug-in manifest '{file}'.", ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (manifest is null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"CLI plug-in manifest '{file}' is empty or invalid.");
|
||||||
|
}
|
||||||
|
|
||||||
|
ValidateManifest(manifest, file);
|
||||||
|
|
||||||
|
var directory = Path.GetDirectoryName(file);
|
||||||
|
return manifest with
|
||||||
|
{
|
||||||
|
SourcePath = file,
|
||||||
|
SourceDirectory = directory
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidateManifest(CliPluginManifest manifest, string file)
|
||||||
|
{
|
||||||
|
if (!string.Equals(manifest.SchemaVersion, CliPluginManifest.CurrentSchemaVersion, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Manifest '{file}' uses unsupported schema version '{manifest.SchemaVersion}'. Expected '{CliPluginManifest.CurrentSchemaVersion}'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(manifest.Id))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Manifest '{file}' must specify a non-empty 'id'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (manifest.EntryPoint is null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Manifest '{file}' must specify an 'entryPoint'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.Equals(manifest.EntryPoint.Type, "dotnet", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Manifest '{file}' entry point type '{manifest.EntryPoint.Type}' is not supported. Expected 'dotnet'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(manifest.EntryPoint.Assembly))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Manifest '{file}' must specify an entry point assembly.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(manifest.EntryPoint.TypeName))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Manifest '{file}' must specify an entry point type.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!manifest.RequiresRestart)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Manifest '{file}' must set 'requiresRestart' to true.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
20
src/StellaOps.Cli/Plugins/ICliCommandModule.cs
Normal file
20
src/StellaOps.Cli/Plugins/ICliCommandModule.cs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
using System;
|
||||||
|
using System.CommandLine;
|
||||||
|
using System.Threading;
|
||||||
|
using StellaOps.Cli.Configuration;
|
||||||
|
|
||||||
|
namespace StellaOps.Cli.Plugins;
|
||||||
|
|
||||||
|
public interface ICliCommandModule
|
||||||
|
{
|
||||||
|
string Name { get; }
|
||||||
|
|
||||||
|
bool IsAvailable(IServiceProvider services);
|
||||||
|
|
||||||
|
void RegisterCommands(
|
||||||
|
RootCommand root,
|
||||||
|
IServiceProvider services,
|
||||||
|
StellaOpsCliOptions options,
|
||||||
|
Option<bool> verboseOption,
|
||||||
|
CancellationToken cancellationToken);
|
||||||
|
}
|
||||||
41
src/StellaOps.Cli/Plugins/RestartOnlyCliPluginGuard.cs
Normal file
41
src/StellaOps.Cli/Plugins/RestartOnlyCliPluginGuard.cs
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace StellaOps.Cli.Plugins;
|
||||||
|
|
||||||
|
internal sealed class RestartOnlyCliPluginGuard
|
||||||
|
{
|
||||||
|
private readonly ConcurrentDictionary<string, byte> _plugins = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
private bool _sealed;
|
||||||
|
|
||||||
|
public IReadOnlyCollection<string> KnownPlugins => _plugins.Keys.ToArray();
|
||||||
|
|
||||||
|
public bool IsSealed => Volatile.Read(ref _sealed);
|
||||||
|
|
||||||
|
public void EnsureRegistrationAllowed(string pluginPath)
|
||||||
|
{
|
||||||
|
ArgumentException.ThrowIfNullOrWhiteSpace(pluginPath);
|
||||||
|
|
||||||
|
var normalized = Normalize(pluginPath);
|
||||||
|
if (IsSealed && !_plugins.ContainsKey(normalized))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Plug-in '{pluginPath}' cannot be registered after startup. Restart required.");
|
||||||
|
}
|
||||||
|
|
||||||
|
_plugins.TryAdd(normalized, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Seal()
|
||||||
|
{
|
||||||
|
Volatile.Write(ref _sealed, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Normalize(string path)
|
||||||
|
{
|
||||||
|
var full = Path.GetFullPath(path);
|
||||||
|
return full.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -116,7 +116,7 @@ internal static class Program
|
|||||||
cts.Cancel();
|
cts.Cancel();
|
||||||
};
|
};
|
||||||
|
|
||||||
var rootCommand = CommandFactory.Create(serviceProvider, options, cts.Token);
|
var rootCommand = CommandFactory.Create(serviceProvider, options, cts.Token, loggerFactory);
|
||||||
var commandConfiguration = new CommandLineConfiguration(rootCommand);
|
var commandConfiguration = new CommandLineConfiguration(rootCommand);
|
||||||
var commandExit = await commandConfiguration.InvokeAsync(args, cts.Token).ConfigureAwait(false);
|
var commandExit = await commandConfiguration.InvokeAsync(args, cts.Token).ConfigureAwait(false);
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
[assembly: InternalsVisibleTo("StellaOps.Cli.Tests")]
|
[assembly: InternalsVisibleTo("StellaOps.Cli.Tests")]
|
||||||
|
[assembly: InternalsVisibleTo("StellaOps.Cli.Plugins.NonCore")]
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
<PackageReference Include="Microsoft.Extensions.Configuration.CommandLine" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.CommandLine" Version="8.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.0" />
|
||||||
<PackageReference Include="NetEscapades.Configuration.Yaml" Version="2.1.0" />
|
<PackageReference Include="NetEscapades.Configuration.Yaml" Version="2.1.0" />
|
||||||
@@ -39,6 +40,7 @@
|
|||||||
<ProjectReference Include="..\StellaOps.Configuration\StellaOps.Configuration.csproj" />
|
<ProjectReference Include="..\StellaOps.Configuration\StellaOps.Configuration.csproj" />
|
||||||
<ProjectReference Include="..\StellaOps.Authority\StellaOps.Auth.Abstractions\StellaOps.Auth.Abstractions.csproj" />
|
<ProjectReference Include="..\StellaOps.Authority\StellaOps.Auth.Abstractions\StellaOps.Auth.Abstractions.csproj" />
|
||||||
<ProjectReference Include="..\StellaOps.Authority\StellaOps.Auth.Client\StellaOps.Auth.Client.csproj" />
|
<ProjectReference Include="..\StellaOps.Authority\StellaOps.Auth.Client\StellaOps.Auth.Client.csproj" />
|
||||||
|
<ProjectReference Include="..\StellaOps.Plugin\StellaOps.Plugin.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -19,6 +19,6 @@ If you are working on this file you need to read docs/ARCHITECTURE_EXCITITOR.md
|
|||||||
|EXCITITOR-CLI-01-003 – CLI docs & examples for Excititor|Docs/CLI|EXCITITOR-CLI-01-001|**DOING (2025-10-19)** – Update docs/09_API_CLI_REFERENCE.md and quickstart snippets to cover Excititor verbs, offline guidance, and attestation verification workflow.|
|
|EXCITITOR-CLI-01-003 – CLI docs & examples for Excititor|Docs/CLI|EXCITITOR-CLI-01-001|**DOING (2025-10-19)** – Update docs/09_API_CLI_REFERENCE.md and quickstart snippets to cover Excititor verbs, offline guidance, and attestation verification workflow.|
|
||||||
|CLI-RUNTIME-13-005 – Runtime policy test verbs|DevEx/CLI|SCANNER-RUNTIME-12-302, ZASTAVA-WEBHOOK-12-102|**DONE (2025-10-19)** – Added `runtime policy test` command (stdin/file support, JSON output), backend client method + typed models, verdict table output, docs/tests updated (`dotnet test src/StellaOps.Cli.Tests`).|
|
|CLI-RUNTIME-13-005 – Runtime policy test verbs|DevEx/CLI|SCANNER-RUNTIME-12-302, ZASTAVA-WEBHOOK-12-102|**DONE (2025-10-19)** – Added `runtime policy test` command (stdin/file support, JSON output), backend client method + typed models, verdict table output, docs/tests updated (`dotnet test src/StellaOps.Cli.Tests`).|
|
||||||
|CLI-OFFLINE-13-006 – Offline kit workflows|DevEx/CLI|DEVOPS-OFFLINE-14-002|**DONE (2025-10-21)** – Added `offline kit pull/import/status` commands with resumable downloads, digest/metadata validation, metrics, docs updates, and regression coverage (`dotnet test src/StellaOps.Cli.Tests`).|
|
|CLI-OFFLINE-13-006 – Offline kit workflows|DevEx/CLI|DEVOPS-OFFLINE-14-002|**DONE (2025-10-21)** – Added `offline kit pull/import/status` commands with resumable downloads, digest/metadata validation, metrics, docs updates, and regression coverage (`dotnet test src/StellaOps.Cli.Tests`).|
|
||||||
|CLI-PLUGIN-13-007 – Plugin packaging|DevEx/CLI|CLI-RUNTIME-13-005, CLI-OFFLINE-13-006|TODO – Package non-core verbs as restart-time plug-ins (manifest + loader updates, tests ensuring no hot reload).|
|
|CLI-PLUGIN-13-007 – Plugin packaging|DevEx/CLI|CLI-RUNTIME-13-005, CLI-OFFLINE-13-006|DONE (2025-10-22) – Packaged non-core verbs as restart-time plug-ins with manifest + loader updates and tests ensuring no hot reload.|
|
||||||
|CLI-RUNTIME-13-008 – Runtime policy contract sync|DevEx/CLI, Scanner WebService Guild|SCANNER-RUNTIME-12-302|**DONE (2025-10-19)** – CLI runtime table/JSON now align with SCANNER-RUNTIME-12-302 (SBOM referrers, quieted provenance, confidence, verified Rekor); docs/09 updated with joint sign-off note.|
|
|CLI-RUNTIME-13-008 – Runtime policy contract sync|DevEx/CLI, Scanner WebService Guild|SCANNER-RUNTIME-12-302|**DONE (2025-10-19)** – CLI runtime table/JSON now align with SCANNER-RUNTIME-12-302 (SBOM referrers, quieted provenance, confidence, verified Rekor); docs/09 updated with joint sign-off note.|
|
||||||
|CLI-RUNTIME-13-009 – Runtime policy smoke fixture|DevEx/CLI, QA Guild|CLI-RUNTIME-13-005|**DONE (2025-10-19)** – Spectre console harness + regression tests cover table and `--json` output paths for `runtime policy test`, using stubbed backend and integrated into `dotnet test` suite.|
|
|CLI-RUNTIME-13-009 – Runtime policy smoke fixture|DevEx/CLI, QA Guild|CLI-RUNTIME-13-005|**DONE (2025-10-19)** – Spectre console harness + regression tests cover table and `--json` output paths for `runtime policy test`, using stubbed backend and integrated into `dotnet test` suite.|
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
{
|
||||||
|
"advisoryKey": "GHSA-aaaa-bbbb-cccc",
|
||||||
|
"affectedPackages": [
|
||||||
|
{
|
||||||
|
"type": "semver",
|
||||||
|
"identifier": "pkg:npm/example-widget",
|
||||||
|
"platform": null,
|
||||||
|
"versionRanges": [
|
||||||
|
{
|
||||||
|
"fixedVersion": "2.5.1",
|
||||||
|
"introducedVersion": null,
|
||||||
|
"lastAffectedVersion": null,
|
||||||
|
"primitives": null,
|
||||||
|
"provenance": {
|
||||||
|
"source": "ghsa",
|
||||||
|
"kind": "map",
|
||||||
|
"value": "ghsa-aaaa-bbbb-cccc",
|
||||||
|
"decisionReason": null,
|
||||||
|
"recordedAt": "2024-03-05T10:00:00+00:00",
|
||||||
|
"fieldMask": []
|
||||||
|
},
|
||||||
|
"rangeExpression": ">=0.0.0 <2.5.1",
|
||||||
|
"rangeKind": "semver"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fixedVersion": "3.2.4",
|
||||||
|
"introducedVersion": "3.0.0",
|
||||||
|
"lastAffectedVersion": null,
|
||||||
|
"primitives": null,
|
||||||
|
"provenance": {
|
||||||
|
"source": "ghsa",
|
||||||
|
"kind": "map",
|
||||||
|
"value": "ghsa-aaaa-bbbb-cccc",
|
||||||
|
"decisionReason": null,
|
||||||
|
"recordedAt": "2024-03-05T10:00:00+00:00",
|
||||||
|
"fieldMask": []
|
||||||
|
},
|
||||||
|
"rangeExpression": null,
|
||||||
|
"rangeKind": "semver"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"normalizedVersions": [],
|
||||||
|
"statuses": [],
|
||||||
|
"provenance": [
|
||||||
|
{
|
||||||
|
"source": "ghsa",
|
||||||
|
"kind": "map",
|
||||||
|
"value": "ghsa-aaaa-bbbb-cccc",
|
||||||
|
"decisionReason": null,
|
||||||
|
"recordedAt": "2024-03-05T10:00:00+00:00",
|
||||||
|
"fieldMask": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"aliases": [
|
||||||
|
"CVE-2024-2222",
|
||||||
|
"GHSA-aaaa-bbbb-cccc"
|
||||||
|
],
|
||||||
|
"canonicalMetricId": null,
|
||||||
|
"credits": [],
|
||||||
|
"cvssMetrics": [
|
||||||
|
{
|
||||||
|
"baseScore": 8.8,
|
||||||
|
"baseSeverity": "high",
|
||||||
|
"provenance": {
|
||||||
|
"source": "ghsa",
|
||||||
|
"kind": "map",
|
||||||
|
"value": "ghsa-aaaa-bbbb-cccc",
|
||||||
|
"decisionReason": null,
|
||||||
|
"recordedAt": "2024-03-05T10:00:00+00:00",
|
||||||
|
"fieldMask": []
|
||||||
|
},
|
||||||
|
"vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H",
|
||||||
|
"version": "3.1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cwes": [],
|
||||||
|
"description": null,
|
||||||
|
"exploitKnown": false,
|
||||||
|
"language": "en",
|
||||||
|
"modified": "2024-03-04T12:00:00+00:00",
|
||||||
|
"provenance": [
|
||||||
|
{
|
||||||
|
"source": "ghsa",
|
||||||
|
"kind": "map",
|
||||||
|
"value": "ghsa-aaaa-bbbb-cccc",
|
||||||
|
"decisionReason": null,
|
||||||
|
"recordedAt": "2024-03-05T10:00:00+00:00",
|
||||||
|
"fieldMask": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"published": "2024-03-04T00:00:00+00:00",
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"kind": "patch",
|
||||||
|
"provenance": {
|
||||||
|
"source": "ghsa",
|
||||||
|
"kind": "map",
|
||||||
|
"value": "ghsa-aaaa-bbbb-cccc",
|
||||||
|
"decisionReason": null,
|
||||||
|
"recordedAt": "2024-03-05T10:00:00+00:00",
|
||||||
|
"fieldMask": []
|
||||||
|
},
|
||||||
|
"sourceTag": "ghsa",
|
||||||
|
"summary": "Patch commit",
|
||||||
|
"url": "https://github.com/example/widget/commit/abcd1234"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "advisory",
|
||||||
|
"provenance": {
|
||||||
|
"source": "ghsa",
|
||||||
|
"kind": "map",
|
||||||
|
"value": "ghsa-aaaa-bbbb-cccc",
|
||||||
|
"decisionReason": null,
|
||||||
|
"recordedAt": "2024-03-05T10:00:00+00:00",
|
||||||
|
"fieldMask": []
|
||||||
|
},
|
||||||
|
"sourceTag": "ghsa",
|
||||||
|
"summary": "GitHub Security Advisory",
|
||||||
|
"url": "https://github.com/example/widget/security/advisories/GHSA-aaaa-bbbb-cccc"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"severity": "high",
|
||||||
|
"summary": "A crafted payload can pollute Object.prototype leading to RCE.",
|
||||||
|
"title": "Prototype pollution in widget.js"
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"advisoryKey": "CVE-2023-9999",
|
||||||
|
"affectedPackages": [],
|
||||||
|
"aliases": [
|
||||||
|
"CVE-2023-9999"
|
||||||
|
],
|
||||||
|
"canonicalMetricId": null,
|
||||||
|
"credits": [],
|
||||||
|
"cvssMetrics": [],
|
||||||
|
"cwes": [],
|
||||||
|
"description": null,
|
||||||
|
"exploitKnown": true,
|
||||||
|
"language": "en",
|
||||||
|
"modified": "2024-02-09T16:22:00+00:00",
|
||||||
|
"provenance": [
|
||||||
|
{
|
||||||
|
"source": "cisa-kev",
|
||||||
|
"kind": "annotate",
|
||||||
|
"value": "kev",
|
||||||
|
"decisionReason": null,
|
||||||
|
"recordedAt": "2024-02-10T09:30:00+00:00",
|
||||||
|
"fieldMask": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"published": "2023-11-20T00:00:00+00:00",
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"kind": "kev",
|
||||||
|
"provenance": {
|
||||||
|
"source": "cisa-kev",
|
||||||
|
"kind": "annotate",
|
||||||
|
"value": "kev",
|
||||||
|
"decisionReason": null,
|
||||||
|
"recordedAt": "2024-02-10T09:30:00+00:00",
|
||||||
|
"fieldMask": []
|
||||||
|
},
|
||||||
|
"sourceTag": "cisa",
|
||||||
|
"summary": "CISA KEV entry",
|
||||||
|
"url": "https://www.cisa.gov/known-exploited-vulnerabilities-catalog"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"severity": "critical",
|
||||||
|
"summary": "Unauthenticated RCE due to unsafe deserialization.",
|
||||||
|
"title": "Remote code execution in LegacyServer"
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
{
|
||||||
|
"advisoryKey": "CVE-2024-1234",
|
||||||
|
"affectedPackages": [
|
||||||
|
{
|
||||||
|
"type": "cpe",
|
||||||
|
"identifier": "cpe:/a:examplecms:examplecms:1.0",
|
||||||
|
"platform": null,
|
||||||
|
"versionRanges": [
|
||||||
|
{
|
||||||
|
"fixedVersion": "1.0.5",
|
||||||
|
"introducedVersion": "1.0",
|
||||||
|
"lastAffectedVersion": null,
|
||||||
|
"primitives": null,
|
||||||
|
"provenance": {
|
||||||
|
"source": "nvd",
|
||||||
|
"kind": "map",
|
||||||
|
"value": "cve-2024-1234",
|
||||||
|
"decisionReason": null,
|
||||||
|
"recordedAt": "2024-08-01T12:00:00+00:00",
|
||||||
|
"fieldMask": []
|
||||||
|
},
|
||||||
|
"rangeExpression": null,
|
||||||
|
"rangeKind": "version"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"normalizedVersions": [],
|
||||||
|
"statuses": [
|
||||||
|
{
|
||||||
|
"provenance": {
|
||||||
|
"source": "nvd",
|
||||||
|
"kind": "map",
|
||||||
|
"value": "cve-2024-1234",
|
||||||
|
"decisionReason": null,
|
||||||
|
"recordedAt": "2024-08-01T12:00:00+00:00",
|
||||||
|
"fieldMask": []
|
||||||
|
},
|
||||||
|
"status": "affected"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"provenance": [
|
||||||
|
{
|
||||||
|
"source": "nvd",
|
||||||
|
"kind": "map",
|
||||||
|
"value": "cve-2024-1234",
|
||||||
|
"decisionReason": null,
|
||||||
|
"recordedAt": "2024-08-01T12:00:00+00:00",
|
||||||
|
"fieldMask": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"aliases": [
|
||||||
|
"CVE-2024-1234"
|
||||||
|
],
|
||||||
|
"canonicalMetricId": null,
|
||||||
|
"credits": [],
|
||||||
|
"cvssMetrics": [
|
||||||
|
{
|
||||||
|
"baseScore": 9.8,
|
||||||
|
"baseSeverity": "critical",
|
||||||
|
"provenance": {
|
||||||
|
"source": "nvd",
|
||||||
|
"kind": "map",
|
||||||
|
"value": "cve-2024-1234",
|
||||||
|
"decisionReason": null,
|
||||||
|
"recordedAt": "2024-08-01T12:00:00+00:00",
|
||||||
|
"fieldMask": []
|
||||||
|
},
|
||||||
|
"vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
|
||||||
|
"version": "3.1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cwes": [],
|
||||||
|
"description": null,
|
||||||
|
"exploitKnown": false,
|
||||||
|
"language": "en",
|
||||||
|
"modified": "2024-07-16T10:35:00+00:00",
|
||||||
|
"provenance": [
|
||||||
|
{
|
||||||
|
"source": "nvd",
|
||||||
|
"kind": "map",
|
||||||
|
"value": "cve-2024-1234",
|
||||||
|
"decisionReason": null,
|
||||||
|
"recordedAt": "2024-08-01T12:00:00+00:00",
|
||||||
|
"fieldMask": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"published": "2024-07-15T00:00:00+00:00",
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"kind": "advisory",
|
||||||
|
"provenance": {
|
||||||
|
"source": "example",
|
||||||
|
"kind": "fetch",
|
||||||
|
"value": "bulletin",
|
||||||
|
"decisionReason": null,
|
||||||
|
"recordedAt": "2024-07-14T15:00:00+00:00",
|
||||||
|
"fieldMask": []
|
||||||
|
},
|
||||||
|
"sourceTag": "vendor",
|
||||||
|
"summary": "Vendor bulletin",
|
||||||
|
"url": "https://example.org/security/CVE-2024-1234"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "advisory",
|
||||||
|
"provenance": {
|
||||||
|
"source": "nvd",
|
||||||
|
"kind": "map",
|
||||||
|
"value": "cve-2024-1234",
|
||||||
|
"decisionReason": null,
|
||||||
|
"recordedAt": "2024-08-01T12:00:00+00:00",
|
||||||
|
"fieldMask": []
|
||||||
|
},
|
||||||
|
"sourceTag": "nvd",
|
||||||
|
"summary": "NVD entry",
|
||||||
|
"url": "https://nvd.nist.gov/vuln/detail/CVE-2024-1234"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"severity": "high",
|
||||||
|
"summary": "An integer overflow in ExampleCMS allows remote attackers to escalate privileges.",
|
||||||
|
"title": "Integer overflow in ExampleCMS"
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
{
|
||||||
|
"advisoryKey": "RHSA-2024:0252",
|
||||||
|
"affectedPackages": [
|
||||||
|
{
|
||||||
|
"type": "rpm",
|
||||||
|
"identifier": "kernel-0:4.18.0-553.el8.x86_64",
|
||||||
|
"platform": "rhel-8",
|
||||||
|
"versionRanges": [
|
||||||
|
{
|
||||||
|
"fixedVersion": null,
|
||||||
|
"introducedVersion": "0:4.18.0-553.el8",
|
||||||
|
"lastAffectedVersion": null,
|
||||||
|
"primitives": null,
|
||||||
|
"provenance": {
|
||||||
|
"source": "redhat",
|
||||||
|
"kind": "map",
|
||||||
|
"value": "rhsa-2024:0252",
|
||||||
|
"decisionReason": null,
|
||||||
|
"recordedAt": "2024-05-11T09:00:00+00:00",
|
||||||
|
"fieldMask": []
|
||||||
|
},
|
||||||
|
"rangeExpression": null,
|
||||||
|
"rangeKind": "nevra"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"normalizedVersions": [],
|
||||||
|
"statuses": [
|
||||||
|
{
|
||||||
|
"provenance": {
|
||||||
|
"source": "redhat",
|
||||||
|
"kind": "map",
|
||||||
|
"value": "rhsa-2024:0252",
|
||||||
|
"decisionReason": null,
|
||||||
|
"recordedAt": "2024-05-11T09:00:00+00:00",
|
||||||
|
"fieldMask": []
|
||||||
|
},
|
||||||
|
"status": "fixed"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"provenance": [
|
||||||
|
{
|
||||||
|
"source": "redhat",
|
||||||
|
"kind": "enrich",
|
||||||
|
"value": "cve-2024-5678",
|
||||||
|
"decisionReason": null,
|
||||||
|
"recordedAt": "2024-05-11T09:05:00+00:00",
|
||||||
|
"fieldMask": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "redhat",
|
||||||
|
"kind": "map",
|
||||||
|
"value": "rhsa-2024:0252",
|
||||||
|
"decisionReason": null,
|
||||||
|
"recordedAt": "2024-05-11T09:00:00+00:00",
|
||||||
|
"fieldMask": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"aliases": [
|
||||||
|
"CVE-2024-5678",
|
||||||
|
"RHSA-2024:0252"
|
||||||
|
],
|
||||||
|
"canonicalMetricId": null,
|
||||||
|
"credits": [],
|
||||||
|
"cvssMetrics": [
|
||||||
|
{
|
||||||
|
"baseScore": 6.7,
|
||||||
|
"baseSeverity": "medium",
|
||||||
|
"provenance": {
|
||||||
|
"source": "redhat",
|
||||||
|
"kind": "map",
|
||||||
|
"value": "rhsa-2024:0252",
|
||||||
|
"decisionReason": null,
|
||||||
|
"recordedAt": "2024-05-11T09:00:00+00:00",
|
||||||
|
"fieldMask": []
|
||||||
|
},
|
||||||
|
"vector": "CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H",
|
||||||
|
"version": "3.1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cwes": [],
|
||||||
|
"description": null,
|
||||||
|
"exploitKnown": false,
|
||||||
|
"language": "en",
|
||||||
|
"modified": "2024-05-11T08:15:00+00:00",
|
||||||
|
"provenance": [
|
||||||
|
{
|
||||||
|
"source": "redhat",
|
||||||
|
"kind": "enrich",
|
||||||
|
"value": "cve-2024-5678",
|
||||||
|
"decisionReason": null,
|
||||||
|
"recordedAt": "2024-05-11T09:05:00+00:00",
|
||||||
|
"fieldMask": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "redhat",
|
||||||
|
"kind": "map",
|
||||||
|
"value": "rhsa-2024:0252",
|
||||||
|
"decisionReason": null,
|
||||||
|
"recordedAt": "2024-05-11T09:00:00+00:00",
|
||||||
|
"fieldMask": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"published": "2024-05-10T19:28:00+00:00",
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"kind": "advisory",
|
||||||
|
"provenance": {
|
||||||
|
"source": "redhat",
|
||||||
|
"kind": "map",
|
||||||
|
"value": "rhsa-2024:0252",
|
||||||
|
"decisionReason": null,
|
||||||
|
"recordedAt": "2024-05-11T09:00:00+00:00",
|
||||||
|
"fieldMask": []
|
||||||
|
},
|
||||||
|
"sourceTag": "redhat",
|
||||||
|
"summary": "Red Hat security advisory",
|
||||||
|
"url": "https://access.redhat.com/errata/RHSA-2024:0252"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"severity": "critical",
|
||||||
|
"summary": "Updates the Red Hat Enterprise Linux kernel to address CVE-2024-5678.",
|
||||||
|
"title": "Important: kernel security update"
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
using System;
|
||||||
|
using StellaOps.Scanner.Analyzers.Lang.Plugin;
|
||||||
|
|
||||||
|
namespace StellaOps.Scanner.Analyzers.Lang.DotNet;
|
||||||
|
|
||||||
|
public sealed class DotNetAnalyzerPlugin : ILanguageAnalyzerPlugin
|
||||||
|
{
|
||||||
|
public string Name => "StellaOps.Scanner.Analyzers.Lang.DotNet";
|
||||||
|
|
||||||
|
public bool IsAvailable(IServiceProvider services) => services is not null;
|
||||||
|
|
||||||
|
public ILanguageAnalyzer CreateAnalyzer(IServiceProvider services)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(services);
|
||||||
|
return new DotNetLanguageAnalyzer();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
using StellaOps.Scanner.Analyzers.Lang.DotNet.Internal;
|
||||||
|
|
||||||
|
namespace StellaOps.Scanner.Analyzers.Lang.DotNet;
|
||||||
|
|
||||||
|
public sealed class DotNetLanguageAnalyzer : ILanguageAnalyzer
|
||||||
|
{
|
||||||
|
public string Id => "dotnet";
|
||||||
|
|
||||||
|
public string DisplayName => ".NET Analyzer (preview)";
|
||||||
|
|
||||||
|
public async ValueTask AnalyzeAsync(LanguageAnalyzerContext context, LanguageComponentWriter writer, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(context);
|
||||||
|
ArgumentNullException.ThrowIfNull(writer);
|
||||||
|
|
||||||
|
var packages = await DotNetDependencyCollector.CollectAsync(context, cancellationToken).ConfigureAwait(false);
|
||||||
|
if (packages.Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var package in packages)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
writer.AddFromPurl(
|
||||||
|
analyzerId: Id,
|
||||||
|
purl: package.Purl,
|
||||||
|
name: package.Name,
|
||||||
|
version: package.Version,
|
||||||
|
type: "nuget",
|
||||||
|
metadata: package.Metadata,
|
||||||
|
evidence: package.Evidence,
|
||||||
|
usedByEntrypoint: package.UsedByEntrypoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,416 @@
|
|||||||
|
using System.Linq;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace StellaOps.Scanner.Analyzers.Lang.DotNet.Internal;
|
||||||
|
|
||||||
|
internal static class DotNetDependencyCollector
|
||||||
|
{
|
||||||
|
private static readonly EnumerationOptions Enumeration = new()
|
||||||
|
{
|
||||||
|
RecurseSubdirectories = true,
|
||||||
|
IgnoreInaccessible = true,
|
||||||
|
AttributesToSkip = FileAttributes.Device | FileAttributes.ReparsePoint
|
||||||
|
};
|
||||||
|
|
||||||
|
public static ValueTask<IReadOnlyList<DotNetPackage>> CollectAsync(LanguageAnalyzerContext context, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(context);
|
||||||
|
|
||||||
|
var depsFiles = Directory
|
||||||
|
.EnumerateFiles(context.RootPath, "*.deps.json", Enumeration)
|
||||||
|
.OrderBy(static path => path, StringComparer.Ordinal)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
if (depsFiles.Length == 0)
|
||||||
|
{
|
||||||
|
return ValueTask.FromResult<IReadOnlyList<DotNetPackage>>(Array.Empty<DotNetPackage>());
|
||||||
|
}
|
||||||
|
|
||||||
|
var aggregator = new DotNetPackageAggregator();
|
||||||
|
|
||||||
|
foreach (var depsPath in depsFiles)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var relativeDepsPath = NormalizeRelative(context.GetRelativePath(depsPath));
|
||||||
|
var depsFile = DotNetDepsFile.Load(depsPath, relativeDepsPath, cancellationToken);
|
||||||
|
if (depsFile is null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
DotNetRuntimeConfig? runtimeConfig = null;
|
||||||
|
var runtimeConfigPath = Path.ChangeExtension(depsPath, ".runtimeconfig.json");
|
||||||
|
if (!string.IsNullOrEmpty(runtimeConfigPath) && File.Exists(runtimeConfigPath))
|
||||||
|
{
|
||||||
|
var relativeRuntimePath = NormalizeRelative(context.GetRelativePath(runtimeConfigPath));
|
||||||
|
runtimeConfig = DotNetRuntimeConfig.Load(runtimeConfigPath, relativeRuntimePath, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
aggregator.Add(depsFile, runtimeConfig);
|
||||||
|
}
|
||||||
|
catch (IOException)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
catch (JsonException)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
catch (UnauthorizedAccessException)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var packages = aggregator.Build();
|
||||||
|
return ValueTask.FromResult<IReadOnlyList<DotNetPackage>>(packages);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string NormalizeRelative(string path)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(path) || path == ".")
|
||||||
|
{
|
||||||
|
return ".";
|
||||||
|
}
|
||||||
|
|
||||||
|
var normalized = path.Replace('\\', '/');
|
||||||
|
return string.IsNullOrWhiteSpace(normalized) ? "." : normalized;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class DotNetPackageAggregator
|
||||||
|
{
|
||||||
|
private readonly Dictionary<string, DotNetPackageBuilder> _packages = new(StringComparer.Ordinal);
|
||||||
|
|
||||||
|
public void Add(DotNetDepsFile depsFile, DotNetRuntimeConfig? runtimeConfig)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(depsFile);
|
||||||
|
|
||||||
|
foreach (var library in depsFile.Libraries.Values)
|
||||||
|
{
|
||||||
|
if (!library.IsPackage)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var normalizedId = DotNetPackageBuilder.NormalizeId(library.Id);
|
||||||
|
var key = DotNetPackageBuilder.BuildKey(normalizedId, library.Version);
|
||||||
|
|
||||||
|
if (!_packages.TryGetValue(key, out var builder))
|
||||||
|
{
|
||||||
|
builder = new DotNetPackageBuilder(library.Id, normalizedId, library.Version);
|
||||||
|
_packages[key] = builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.AddLibrary(library, depsFile.RelativePath, runtimeConfig);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyList<DotNetPackage> Build()
|
||||||
|
{
|
||||||
|
if (_packages.Count == 0)
|
||||||
|
{
|
||||||
|
return Array.Empty<DotNetPackage>();
|
||||||
|
}
|
||||||
|
|
||||||
|
var items = new List<DotNetPackage>(_packages.Count);
|
||||||
|
foreach (var builder in _packages.Values)
|
||||||
|
{
|
||||||
|
items.Add(builder.Build());
|
||||||
|
}
|
||||||
|
|
||||||
|
items.Sort(static (left, right) => string.CompareOrdinal(left.ComponentKey, right.ComponentKey));
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class DotNetPackageBuilder
|
||||||
|
{
|
||||||
|
private readonly string _originalId;
|
||||||
|
private readonly string _normalizedId;
|
||||||
|
private readonly string _version;
|
||||||
|
|
||||||
|
private bool? _serviceable;
|
||||||
|
|
||||||
|
private readonly SortedSet<string> _sha512 = new(StringComparer.Ordinal);
|
||||||
|
private readonly SortedSet<string> _packagePaths = new(StringComparer.Ordinal);
|
||||||
|
private readonly SortedSet<string> _hashPaths = new(StringComparer.Ordinal);
|
||||||
|
private readonly SortedSet<string> _depsPaths = new(StringComparer.Ordinal);
|
||||||
|
private readonly SortedSet<string> _targetFrameworks = new(StringComparer.Ordinal);
|
||||||
|
private readonly SortedSet<string> _runtimeIdentifiers = new(StringComparer.Ordinal);
|
||||||
|
private readonly SortedSet<string> _dependencies = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
private readonly SortedSet<string> _runtimeConfigPaths = new(StringComparer.Ordinal);
|
||||||
|
private readonly SortedSet<string> _runtimeConfigTfms = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
private readonly SortedSet<string> _runtimeConfigFrameworks = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
private readonly SortedSet<string> _runtimeConfigGraph = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
private readonly HashSet<LanguageComponentEvidence> _evidence = new(new LanguageComponentEvidenceComparer());
|
||||||
|
|
||||||
|
public DotNetPackageBuilder(string originalId, string normalizedId, string version)
|
||||||
|
{
|
||||||
|
_originalId = string.IsNullOrWhiteSpace(originalId) ? normalizedId : originalId.Trim();
|
||||||
|
_normalizedId = normalizedId;
|
||||||
|
_version = version ?? string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string BuildKey(string normalizedId, string version)
|
||||||
|
=> $"{normalizedId}::{version}";
|
||||||
|
|
||||||
|
public static string NormalizeId(string id)
|
||||||
|
=> string.IsNullOrWhiteSpace(id) ? string.Empty : id.Trim().ToLowerInvariant();
|
||||||
|
|
||||||
|
public void AddLibrary(DotNetLibrary library, string relativeDepsPath, DotNetRuntimeConfig? runtimeConfig)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(library);
|
||||||
|
|
||||||
|
if (library.Serviceable is bool serviceable)
|
||||||
|
{
|
||||||
|
_serviceable = _serviceable.HasValue
|
||||||
|
? _serviceable.Value || serviceable
|
||||||
|
: serviceable;
|
||||||
|
}
|
||||||
|
|
||||||
|
AddIfPresent(_sha512, library.Sha512);
|
||||||
|
AddIfPresent(_packagePaths, library.PackagePath);
|
||||||
|
AddIfPresent(_hashPaths, library.HashPath);
|
||||||
|
AddIfPresent(_depsPaths, NormalizeRelativePath(relativeDepsPath));
|
||||||
|
|
||||||
|
foreach (var dependency in library.Dependencies)
|
||||||
|
{
|
||||||
|
AddIfPresent(_dependencies, dependency, normalizeLower: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var tfm in library.TargetFrameworks)
|
||||||
|
{
|
||||||
|
AddIfPresent(_targetFrameworks, tfm);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var rid in library.RuntimeIdentifiers)
|
||||||
|
{
|
||||||
|
AddIfPresent(_runtimeIdentifiers, rid);
|
||||||
|
}
|
||||||
|
|
||||||
|
_evidence.Add(new LanguageComponentEvidence(
|
||||||
|
LanguageEvidenceKind.File,
|
||||||
|
"deps.json",
|
||||||
|
NormalizeRelativePath(relativeDepsPath),
|
||||||
|
library.Key,
|
||||||
|
Sha256: null));
|
||||||
|
|
||||||
|
if (runtimeConfig is not null)
|
||||||
|
{
|
||||||
|
AddIfPresent(_runtimeConfigPaths, runtimeConfig.RelativePath);
|
||||||
|
|
||||||
|
foreach (var tfm in runtimeConfig.Tfms)
|
||||||
|
{
|
||||||
|
AddIfPresent(_runtimeConfigTfms, tfm);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var framework in runtimeConfig.Frameworks)
|
||||||
|
{
|
||||||
|
AddIfPresent(_runtimeConfigFrameworks, framework);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var entry in runtimeConfig.RuntimeGraph)
|
||||||
|
{
|
||||||
|
var value = BuildRuntimeGraphValue(entry.Rid, entry.Fallbacks);
|
||||||
|
AddIfPresent(_runtimeConfigGraph, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
_evidence.Add(new LanguageComponentEvidence(
|
||||||
|
LanguageEvidenceKind.File,
|
||||||
|
"runtimeconfig.json",
|
||||||
|
runtimeConfig.RelativePath,
|
||||||
|
Value: null,
|
||||||
|
Sha256: null));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public DotNetPackage Build()
|
||||||
|
{
|
||||||
|
var metadata = new List<KeyValuePair<string, string?>>(32)
|
||||||
|
{
|
||||||
|
new("package.id", _originalId),
|
||||||
|
new("package.id.normalized", _normalizedId),
|
||||||
|
new("package.version", _version)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (_serviceable.HasValue)
|
||||||
|
{
|
||||||
|
metadata.Add(new KeyValuePair<string, string?>("package.serviceable", _serviceable.Value ? "true" : "false"));
|
||||||
|
}
|
||||||
|
|
||||||
|
AddIndexed(metadata, "package.sha512", _sha512);
|
||||||
|
AddIndexed(metadata, "package.path", _packagePaths);
|
||||||
|
AddIndexed(metadata, "package.hashPath", _hashPaths);
|
||||||
|
AddIndexed(metadata, "deps.path", _depsPaths);
|
||||||
|
AddIndexed(metadata, "deps.dependency", _dependencies);
|
||||||
|
AddIndexed(metadata, "deps.tfm", _targetFrameworks);
|
||||||
|
AddIndexed(metadata, "deps.rid", _runtimeIdentifiers);
|
||||||
|
AddIndexed(metadata, "runtimeconfig.path", _runtimeConfigPaths);
|
||||||
|
AddIndexed(metadata, "runtimeconfig.tfm", _runtimeConfigTfms);
|
||||||
|
AddIndexed(metadata, "runtimeconfig.framework", _runtimeConfigFrameworks);
|
||||||
|
AddIndexed(metadata, "runtimeconfig.graph", _runtimeConfigGraph);
|
||||||
|
|
||||||
|
metadata.Sort(static (left, right) => string.CompareOrdinal(left.Key, right.Key));
|
||||||
|
|
||||||
|
var evidence = _evidence
|
||||||
|
.OrderBy(static item => item.Source, StringComparer.Ordinal)
|
||||||
|
.ThenBy(static item => item.Locator, StringComparer.Ordinal)
|
||||||
|
.ThenBy(static item => item.Value, StringComparer.Ordinal)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
return new DotNetPackage(
|
||||||
|
name: _originalId,
|
||||||
|
normalizedId: _normalizedId,
|
||||||
|
version: _version,
|
||||||
|
metadata: metadata,
|
||||||
|
evidence: evidence,
|
||||||
|
usedByEntrypoint: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddIfPresent(ISet<string> set, string? value, bool normalizeLower = false)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var normalized = value.Trim();
|
||||||
|
if (normalizeLower)
|
||||||
|
{
|
||||||
|
normalized = normalized.ToLowerInvariant();
|
||||||
|
}
|
||||||
|
|
||||||
|
set.Add(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddIndexed(ICollection<KeyValuePair<string, string?>> metadata, string prefix, IEnumerable<string> values)
|
||||||
|
{
|
||||||
|
if (metadata is null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(metadata));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (values is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var index = 0;
|
||||||
|
foreach (var value in values)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata.Add(new KeyValuePair<string, string?>($"{prefix}[{index++}]", value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string NormalizeRelativePath(string path)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(path) || path == ".")
|
||||||
|
{
|
||||||
|
return ".";
|
||||||
|
}
|
||||||
|
|
||||||
|
return path.Replace('\\', '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string BuildRuntimeGraphValue(string rid, IReadOnlyList<string> fallbacks)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(rid))
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fallbacks.Count == 0)
|
||||||
|
{
|
||||||
|
return rid.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
var ordered = fallbacks
|
||||||
|
.Where(static fallback => !string.IsNullOrWhiteSpace(fallback))
|
||||||
|
.Select(static fallback => fallback.Trim())
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.OrderBy(static fallback => fallback, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
return ordered.Length == 0
|
||||||
|
? rid.Trim()
|
||||||
|
: $"{rid.Trim()}=>{string.Join(';', ordered)}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class LanguageComponentEvidenceComparer : IEqualityComparer<LanguageComponentEvidence>
|
||||||
|
{
|
||||||
|
public bool Equals(LanguageComponentEvidence? x, LanguageComponentEvidence? y)
|
||||||
|
{
|
||||||
|
if (ReferenceEquals(x, y))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (x is null || y is null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return x.Kind == y.Kind &&
|
||||||
|
string.Equals(x.Source, y.Source, StringComparison.Ordinal) &&
|
||||||
|
string.Equals(x.Locator, y.Locator, StringComparison.Ordinal) &&
|
||||||
|
string.Equals(x.Value, y.Value, StringComparison.Ordinal) &&
|
||||||
|
string.Equals(x.Sha256, y.Sha256, StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int GetHashCode(LanguageComponentEvidence obj)
|
||||||
|
{
|
||||||
|
var hash = new HashCode();
|
||||||
|
hash.Add(obj.Kind);
|
||||||
|
hash.Add(obj.Source, StringComparer.Ordinal);
|
||||||
|
hash.Add(obj.Locator, StringComparer.Ordinal);
|
||||||
|
hash.Add(obj.Value, StringComparer.Ordinal);
|
||||||
|
hash.Add(obj.Sha256, StringComparer.Ordinal);
|
||||||
|
return hash.ToHashCode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class DotNetPackage
|
||||||
|
{
|
||||||
|
public DotNetPackage(
|
||||||
|
string name,
|
||||||
|
string normalizedId,
|
||||||
|
string version,
|
||||||
|
IReadOnlyList<KeyValuePair<string, string?>> metadata,
|
||||||
|
IReadOnlyCollection<LanguageComponentEvidence> evidence,
|
||||||
|
bool usedByEntrypoint)
|
||||||
|
{
|
||||||
|
Name = string.IsNullOrWhiteSpace(name) ? normalizedId : name.Trim();
|
||||||
|
NormalizedId = normalizedId;
|
||||||
|
Version = version ?? string.Empty;
|
||||||
|
Metadata = metadata ?? Array.Empty<KeyValuePair<string, string?>>();
|
||||||
|
Evidence = evidence ?? Array.Empty<LanguageComponentEvidence>();
|
||||||
|
UsedByEntrypoint = usedByEntrypoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Name { get; }
|
||||||
|
|
||||||
|
public string NormalizedId { get; }
|
||||||
|
|
||||||
|
public string Version { get; }
|
||||||
|
|
||||||
|
public IReadOnlyList<KeyValuePair<string, string?>> Metadata { get; }
|
||||||
|
|
||||||
|
public IReadOnlyCollection<LanguageComponentEvidence> Evidence { get; }
|
||||||
|
|
||||||
|
public bool UsedByEntrypoint { get; }
|
||||||
|
|
||||||
|
public string Purl => $"pkg:nuget/{NormalizedId}@{Version}";
|
||||||
|
|
||||||
|
public string ComponentKey => $"purl::{Purl}";
|
||||||
|
}
|
||||||
@@ -0,0 +1,318 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace StellaOps.Scanner.Analyzers.Lang.DotNet.Internal;
|
||||||
|
|
||||||
|
internal sealed class DotNetDepsFile
|
||||||
|
{
|
||||||
|
private DotNetDepsFile(string relativePath, IReadOnlyDictionary<string, DotNetLibrary> libraries)
|
||||||
|
{
|
||||||
|
RelativePath = relativePath;
|
||||||
|
Libraries = libraries;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string RelativePath { get; }
|
||||||
|
|
||||||
|
public IReadOnlyDictionary<string, DotNetLibrary> Libraries { get; }
|
||||||
|
|
||||||
|
public static DotNetDepsFile? Load(string absolutePath, string relativePath, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
using var stream = File.OpenRead(absolutePath);
|
||||||
|
using var document = JsonDocument.Parse(stream, new JsonDocumentOptions
|
||||||
|
{
|
||||||
|
AllowTrailingCommas = true,
|
||||||
|
CommentHandling = JsonCommentHandling.Skip
|
||||||
|
});
|
||||||
|
|
||||||
|
var root = document.RootElement;
|
||||||
|
if (root.ValueKind is not JsonValueKind.Object)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var libraries = ParseLibraries(root, cancellationToken);
|
||||||
|
if (libraries.Count == 0)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
PopulateTargets(root, libraries, cancellationToken);
|
||||||
|
return new DotNetDepsFile(relativePath, libraries);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Dictionary<string, DotNetLibrary> ParseLibraries(JsonElement root, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var result = new Dictionary<string, DotNetLibrary>(StringComparer.Ordinal);
|
||||||
|
|
||||||
|
if (!root.TryGetProperty("libraries", out var librariesElement) || librariesElement.ValueKind is not JsonValueKind.Object)
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var property in librariesElement.EnumerateObject())
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
if (DotNetLibrary.TryCreate(property.Name, property.Value, out var library))
|
||||||
|
{
|
||||||
|
result[property.Name] = library;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void PopulateTargets(JsonElement root, IDictionary<string, DotNetLibrary> libraries, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (!root.TryGetProperty("targets", out var targetsElement) || targetsElement.ValueKind is not JsonValueKind.Object)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var targetProperty in targetsElement.EnumerateObject())
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
var (tfm, rid) = ParseTargetKey(targetProperty.Name);
|
||||||
|
if (targetProperty.Value.ValueKind is not JsonValueKind.Object)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var libraryProperty in targetProperty.Value.EnumerateObject())
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
if (!libraries.TryGetValue(libraryProperty.Name, out var library))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(tfm))
|
||||||
|
{
|
||||||
|
library.AddTargetFramework(tfm);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(rid))
|
||||||
|
{
|
||||||
|
library.AddRuntimeIdentifier(rid);
|
||||||
|
}
|
||||||
|
|
||||||
|
library.MergeTargetMetadata(libraryProperty.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (string tfm, string? rid) ParseTargetKey(string value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
return (string.Empty, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
var separatorIndex = value.IndexOf('/');
|
||||||
|
if (separatorIndex < 0)
|
||||||
|
{
|
||||||
|
return (value.Trim(), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
var tfm = value[..separatorIndex].Trim();
|
||||||
|
var rid = value[(separatorIndex + 1)..].Trim();
|
||||||
|
return (tfm, string.IsNullOrEmpty(rid) ? null : rid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class DotNetLibrary
|
||||||
|
{
|
||||||
|
private readonly HashSet<string> _dependencies = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
private readonly HashSet<string> _runtimeIdentifiers = new(StringComparer.Ordinal);
|
||||||
|
private readonly HashSet<string> _targetFrameworks = new(StringComparer.Ordinal);
|
||||||
|
|
||||||
|
private DotNetLibrary(
|
||||||
|
string key,
|
||||||
|
string id,
|
||||||
|
string version,
|
||||||
|
string type,
|
||||||
|
bool? serviceable,
|
||||||
|
string? sha512,
|
||||||
|
string? path,
|
||||||
|
string? hashPath)
|
||||||
|
{
|
||||||
|
Key = key;
|
||||||
|
Id = id;
|
||||||
|
Version = version;
|
||||||
|
Type = type;
|
||||||
|
Serviceable = serviceable;
|
||||||
|
Sha512 = NormalizeValue(sha512);
|
||||||
|
PackagePath = NormalizePath(path);
|
||||||
|
HashPath = NormalizePath(hashPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Key { get; }
|
||||||
|
|
||||||
|
public string Id { get; }
|
||||||
|
|
||||||
|
public string Version { get; }
|
||||||
|
|
||||||
|
public string Type { get; }
|
||||||
|
|
||||||
|
public bool? Serviceable { get; }
|
||||||
|
|
||||||
|
public string? Sha512 { get; }
|
||||||
|
|
||||||
|
public string? PackagePath { get; }
|
||||||
|
|
||||||
|
public string? HashPath { get; }
|
||||||
|
|
||||||
|
public bool IsPackage => string.Equals(Type, "package", StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
public IReadOnlyCollection<string> Dependencies => _dependencies;
|
||||||
|
|
||||||
|
public IReadOnlyCollection<string> TargetFrameworks => _targetFrameworks;
|
||||||
|
|
||||||
|
public IReadOnlyCollection<string> RuntimeIdentifiers => _runtimeIdentifiers;
|
||||||
|
|
||||||
|
public static bool TryCreate(string key, JsonElement element, [NotNullWhen(true)] out DotNetLibrary? library)
|
||||||
|
{
|
||||||
|
library = null;
|
||||||
|
if (!TrySplitNameAndVersion(key, out var id, out var version))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var type = element.TryGetProperty("type", out var typeElement) && typeElement.ValueKind == JsonValueKind.String
|
||||||
|
? typeElement.GetString() ?? string.Empty
|
||||||
|
: string.Empty;
|
||||||
|
|
||||||
|
bool? serviceable = null;
|
||||||
|
if (element.TryGetProperty("serviceable", out var serviceableElement))
|
||||||
|
{
|
||||||
|
if (serviceableElement.ValueKind is JsonValueKind.True)
|
||||||
|
{
|
||||||
|
serviceable = true;
|
||||||
|
}
|
||||||
|
else if (serviceableElement.ValueKind is JsonValueKind.False)
|
||||||
|
{
|
||||||
|
serviceable = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var sha512 = element.TryGetProperty("sha512", out var sha512Element) && sha512Element.ValueKind == JsonValueKind.String
|
||||||
|
? sha512Element.GetString()
|
||||||
|
: null;
|
||||||
|
|
||||||
|
var path = element.TryGetProperty("path", out var pathElement) && pathElement.ValueKind == JsonValueKind.String
|
||||||
|
? pathElement.GetString()
|
||||||
|
: null;
|
||||||
|
|
||||||
|
var hashPath = element.TryGetProperty("hashPath", out var hashElement) && hashElement.ValueKind == JsonValueKind.String
|
||||||
|
? hashElement.GetString()
|
||||||
|
: null;
|
||||||
|
|
||||||
|
library = new DotNetLibrary(key, id, version, type, serviceable, sha512, path, hashPath);
|
||||||
|
library.MergeLibraryMetadata(element);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddTargetFramework(string tfm)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(tfm))
|
||||||
|
{
|
||||||
|
_targetFrameworks.Add(tfm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddRuntimeIdentifier(string rid)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(rid))
|
||||||
|
{
|
||||||
|
_runtimeIdentifiers.Add(rid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void MergeTargetMetadata(JsonElement element)
|
||||||
|
{
|
||||||
|
if (!element.TryGetProperty("dependencies", out var dependenciesElement) || dependenciesElement.ValueKind is not JsonValueKind.Object)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var dependencyProperty in dependenciesElement.EnumerateObject())
|
||||||
|
{
|
||||||
|
AddDependency(dependencyProperty.Name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void MergeLibraryMetadata(JsonElement element)
|
||||||
|
{
|
||||||
|
if (!element.TryGetProperty("dependencies", out var dependenciesElement) || dependenciesElement.ValueKind is not JsonValueKind.Object)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var dependencyProperty in dependenciesElement.EnumerateObject())
|
||||||
|
{
|
||||||
|
AddDependency(dependencyProperty.Name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddDependency(string name)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(name))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var dependencyId = name;
|
||||||
|
if (TrySplitNameAndVersion(name, out var parsedName, out _))
|
||||||
|
{
|
||||||
|
dependencyId = parsedName;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(dependencyId))
|
||||||
|
{
|
||||||
|
_dependencies.Add(dependencyId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TrySplitNameAndVersion(string key, out string name, out string version)
|
||||||
|
{
|
||||||
|
name = string.Empty;
|
||||||
|
version = string.Empty;
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(key))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var separatorIndex = key.LastIndexOf('/');
|
||||||
|
if (separatorIndex <= 0 || separatorIndex >= key.Length - 1)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
name = key[..separatorIndex].Trim();
|
||||||
|
version = key[(separatorIndex + 1)..].Trim();
|
||||||
|
return name.Length > 0 && version.Length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? NormalizePath(string? path)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(path))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return path.Replace('\\', '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? NormalizeValue(string? value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value.Trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
using System.Linq;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace StellaOps.Scanner.Analyzers.Lang.DotNet.Internal;
|
||||||
|
|
||||||
|
internal sealed class DotNetRuntimeConfig
|
||||||
|
{
|
||||||
|
private DotNetRuntimeConfig(
|
||||||
|
string relativePath,
|
||||||
|
IReadOnlyCollection<string> tfms,
|
||||||
|
IReadOnlyCollection<string> frameworks,
|
||||||
|
IReadOnlyCollection<RuntimeGraphEntry> runtimeGraph)
|
||||||
|
{
|
||||||
|
RelativePath = relativePath;
|
||||||
|
Tfms = tfms;
|
||||||
|
Frameworks = frameworks;
|
||||||
|
RuntimeGraph = runtimeGraph;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string RelativePath { get; }
|
||||||
|
|
||||||
|
public IReadOnlyCollection<string> Tfms { get; }
|
||||||
|
|
||||||
|
public IReadOnlyCollection<string> Frameworks { get; }
|
||||||
|
|
||||||
|
public IReadOnlyCollection<RuntimeGraphEntry> RuntimeGraph { get; }
|
||||||
|
|
||||||
|
public static DotNetRuntimeConfig? Load(string absolutePath, string relativePath, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
using var stream = File.OpenRead(absolutePath);
|
||||||
|
using var document = JsonDocument.Parse(stream, new JsonDocumentOptions
|
||||||
|
{
|
||||||
|
AllowTrailingCommas = true,
|
||||||
|
CommentHandling = JsonCommentHandling.Skip
|
||||||
|
});
|
||||||
|
|
||||||
|
var root = document.RootElement;
|
||||||
|
if (!root.TryGetProperty("runtimeOptions", out var runtimeOptions) || runtimeOptions.ValueKind is not JsonValueKind.Object)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var tfms = new SortedSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
var frameworks = new SortedSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
var runtimeGraph = new List<RuntimeGraphEntry>();
|
||||||
|
|
||||||
|
if (runtimeOptions.TryGetProperty("tfm", out var tfmElement) && tfmElement.ValueKind == JsonValueKind.String)
|
||||||
|
{
|
||||||
|
AddIfPresent(tfms, tfmElement.GetString());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (runtimeOptions.TryGetProperty("framework", out var frameworkElement) && frameworkElement.ValueKind == JsonValueKind.Object)
|
||||||
|
{
|
||||||
|
var frameworkId = FormatFramework(frameworkElement);
|
||||||
|
AddIfPresent(frameworks, frameworkId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (runtimeOptions.TryGetProperty("frameworks", out var frameworksElement) && frameworksElement.ValueKind == JsonValueKind.Array)
|
||||||
|
{
|
||||||
|
foreach (var item in frameworksElement.EnumerateArray())
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
var frameworkId = FormatFramework(item);
|
||||||
|
AddIfPresent(frameworks, frameworkId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (runtimeOptions.TryGetProperty("includedFrameworks", out var includedElement) && includedElement.ValueKind == JsonValueKind.Array)
|
||||||
|
{
|
||||||
|
foreach (var item in includedElement.EnumerateArray())
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
var frameworkId = FormatFramework(item);
|
||||||
|
AddIfPresent(frameworks, frameworkId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (runtimeOptions.TryGetProperty("runtimeGraph", out var runtimeGraphElement) &&
|
||||||
|
runtimeGraphElement.ValueKind == JsonValueKind.Object &&
|
||||||
|
runtimeGraphElement.TryGetProperty("runtimes", out var runtimesElement) &&
|
||||||
|
runtimesElement.ValueKind == JsonValueKind.Object)
|
||||||
|
{
|
||||||
|
foreach (var ridProperty in runtimesElement.EnumerateObject())
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(ridProperty.Name))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var fallbacks = new List<string>();
|
||||||
|
if (ridProperty.Value.ValueKind == JsonValueKind.Object &&
|
||||||
|
ridProperty.Value.TryGetProperty("fallbacks", out var fallbacksElement) &&
|
||||||
|
fallbacksElement.ValueKind == JsonValueKind.Array)
|
||||||
|
{
|
||||||
|
foreach (var fallback in fallbacksElement.EnumerateArray())
|
||||||
|
{
|
||||||
|
if (fallback.ValueKind == JsonValueKind.String)
|
||||||
|
{
|
||||||
|
var fallbackValue = fallback.GetString();
|
||||||
|
if (!string.IsNullOrWhiteSpace(fallbackValue))
|
||||||
|
{
|
||||||
|
fallbacks.Add(fallbackValue.Trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
runtimeGraph.Add(new RuntimeGraphEntry(ridProperty.Name.Trim(), fallbacks));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new DotNetRuntimeConfig(
|
||||||
|
relativePath,
|
||||||
|
tfms.ToArray(),
|
||||||
|
frameworks.ToArray(),
|
||||||
|
runtimeGraph);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddIfPresent(ISet<string> set, string? value)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
set.Add(value.Trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? FormatFramework(JsonElement element)
|
||||||
|
{
|
||||||
|
if (element.ValueKind is not JsonValueKind.Object)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var name = element.TryGetProperty("name", out var nameElement) && nameElement.ValueKind == JsonValueKind.String
|
||||||
|
? nameElement.GetString()
|
||||||
|
: null;
|
||||||
|
|
||||||
|
var version = element.TryGetProperty("version", out var versionElement) && versionElement.ValueKind == JsonValueKind.String
|
||||||
|
? versionElement.GetString()
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(name))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(version))
|
||||||
|
{
|
||||||
|
return name.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $"{name.Trim()}@{version.Trim()}";
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed record RuntimeGraphEntry(string Rid, IReadOnlyList<string> Fallbacks);
|
||||||
|
}
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
namespace StellaOps.Scanner.Analyzers.Lang.DotNet;
|
|
||||||
|
|
||||||
internal static class Placeholder
|
|
||||||
{
|
|
||||||
// Analyzer implementation will be added during Sprint LA4.
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
| Seq | ID | Status | Depends on | Description | Exit Criteria |
|
| Seq | ID | Status | Depends on | Description | Exit Criteria |
|
||||||
|-----|----|--------|------------|-------------|---------------|
|
|-----|----|--------|------------|-------------|---------------|
|
||||||
| 1 | SCANNER-ANALYZERS-LANG-10-305A | TODO | SCANNER-ANALYZERS-LANG-10-307 | Parse `*.deps.json` + `runtimeconfig.json`, build RID graph, and normalize to `pkg:nuget` components. | RID graph deterministic; fixtures confirm consistent component ordering; fallback to `bin:{sha256}` documented. |
|
| 1 | SCANNER-ANALYZERS-LANG-10-305A | DONE (2025-10-22) | SCANNER-ANALYZERS-LANG-10-307 | Parse `*.deps.json` + `runtimeconfig.json`, build RID graph, and normalize to `pkg:nuget` components. | RID graph deterministic; fixtures confirm consistent component ordering; fallback to `bin:{sha256}` documented. |
|
||||||
| 2 | SCANNER-ANALYZERS-LANG-10-305B | TODO | SCANNER-ANALYZERS-LANG-10-305A | Extract assembly metadata (strong name, file/product info) and optional Authenticode details when offline cert bundle provided. | Signing metadata captured for signed assemblies; offline trust store documented; hash validations deterministic. |
|
| 2 | SCANNER-ANALYZERS-LANG-10-305B | TODO | SCANNER-ANALYZERS-LANG-10-305A | Extract assembly metadata (strong name, file/product info) and optional Authenticode details when offline cert bundle provided. | Signing metadata captured for signed assemblies; offline trust store documented; hash validations deterministic. |
|
||||||
| 3 | SCANNER-ANALYZERS-LANG-10-305C | TODO | SCANNER-ANALYZERS-LANG-10-305B | Handle self-contained apps and native assets; merge with EntryTrace usage hints. | Self-contained fixtures map to components with RID flags; usage hints propagate; tests cover linux/win variants. |
|
| 3 | SCANNER-ANALYZERS-LANG-10-305C | TODO | SCANNER-ANALYZERS-LANG-10-305B | Handle self-contained apps and native assets; merge with EntryTrace usage hints. | Self-contained fixtures map to components with RID flags; usage hints propagate; tests cover linux/win variants. |
|
||||||
| 4 | SCANNER-ANALYZERS-LANG-10-307D | TODO | SCANNER-ANALYZERS-LANG-10-305C | Integrate shared helpers (license mapping, quiet provenance) and concurrency-safe caches. | Shared helpers reused; concurrency tests for parallel layer scans pass; no redundant allocations. |
|
| 4 | SCANNER-ANALYZERS-LANG-10-307D | TODO | SCANNER-ANALYZERS-LANG-10-305C | Integrate shared helpers (license mapping, quiet provenance) and concurrency-safe caches. | Shared helpers reused; concurrency tests for parallel layer scans pass; no redundant allocations. |
|
||||||
|
|||||||
23
src/StellaOps.Scanner.Analyzers.Lang.DotNet/manifest.json
Normal file
23
src/StellaOps.Scanner.Analyzers.Lang.DotNet/manifest.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"schemaVersion": "1.0",
|
||||||
|
"id": "stellaops.analyzer.lang.dotnet",
|
||||||
|
"displayName": "StellaOps .NET Analyzer (preview)",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"requiresRestart": true,
|
||||||
|
"entryPoint": {
|
||||||
|
"type": "dotnet",
|
||||||
|
"assembly": "StellaOps.Scanner.Analyzers.Lang.DotNet.dll",
|
||||||
|
"typeName": "StellaOps.Scanner.Analyzers.Lang.DotNet.DotNetAnalyzerPlugin"
|
||||||
|
},
|
||||||
|
"capabilities": [
|
||||||
|
"language-analyzer",
|
||||||
|
"dotnet",
|
||||||
|
"nuget"
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"org.stellaops.analyzer.language": "dotnet",
|
||||||
|
"org.stellaops.analyzer.kind": "language",
|
||||||
|
"org.stellaops.restart.required": "true",
|
||||||
|
"org.stellaops.analyzer.status": "preview"
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
@@ -0,0 +1,118 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"analyzerId": "golang",
|
||||||
|
"componentKey": "purl::pkg:golang/example.com/app@v1.2.3",
|
||||||
|
"purl": "pkg:golang/example.com/app@v1.2.3",
|
||||||
|
"name": "example.com/app",
|
||||||
|
"version": "v1.2.3",
|
||||||
|
"type": "golang",
|
||||||
|
"usedByEntrypoint": false,
|
||||||
|
"metadata": {
|
||||||
|
"binaryPath": "app",
|
||||||
|
"build.GOARCH": "amd64",
|
||||||
|
"build.GOOS": "linux",
|
||||||
|
"build.vcs": "git",
|
||||||
|
"build.vcs.modified": "false",
|
||||||
|
"build.vcs.revision": "1234567890abcdef1234567890abcdef12345678",
|
||||||
|
"build.vcs.time": "2025-09-14T12:34:56Z",
|
||||||
|
"go.version": "go1.22.5",
|
||||||
|
"modulePath": "example.com/app",
|
||||||
|
"modulePath.main": "example.com/app",
|
||||||
|
"moduleSum": "h1:mainchecksum",
|
||||||
|
"moduleVersion": "v1.2.3"
|
||||||
|
},
|
||||||
|
"evidence": [
|
||||||
|
{
|
||||||
|
"kind": "metadata",
|
||||||
|
"source": "go.buildinfo.setting",
|
||||||
|
"locator": "GOARCH",
|
||||||
|
"value": "amd64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "metadata",
|
||||||
|
"source": "go.buildinfo.setting",
|
||||||
|
"locator": "GOOS",
|
||||||
|
"value": "linux"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "metadata",
|
||||||
|
"source": "go.buildinfo.setting",
|
||||||
|
"locator": "vcs.modified",
|
||||||
|
"value": "false"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "metadata",
|
||||||
|
"source": "go.buildinfo.setting",
|
||||||
|
"locator": "vcs.revision",
|
||||||
|
"value": "1234567890abcdef1234567890abcdef12345678"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "metadata",
|
||||||
|
"source": "go.buildinfo.setting",
|
||||||
|
"locator": "vcs.time",
|
||||||
|
"value": "2025-09-14T12:34:56Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "metadata",
|
||||||
|
"source": "go.buildinfo.setting",
|
||||||
|
"locator": "vcs",
|
||||||
|
"value": "git"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "metadata",
|
||||||
|
"source": "go.buildinfo",
|
||||||
|
"locator": "module:example.com/app",
|
||||||
|
"value": "v1.2.3",
|
||||||
|
"sha256": "h1:mainchecksum"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "metadata",
|
||||||
|
"source": "go.dwarf",
|
||||||
|
"locator": "vcs.modified",
|
||||||
|
"value": "false"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "metadata",
|
||||||
|
"source": "go.dwarf",
|
||||||
|
"locator": "vcs.revision",
|
||||||
|
"value": "1234567890abcdef1234567890abcdef12345678"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "metadata",
|
||||||
|
"source": "go.dwarf",
|
||||||
|
"locator": "vcs.time",
|
||||||
|
"value": "2025-09-14T12:34:56Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "metadata",
|
||||||
|
"source": "go.dwarf",
|
||||||
|
"locator": "vcs",
|
||||||
|
"value": "git"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"analyzerId": "golang",
|
||||||
|
"componentKey": "purl::pkg:golang/example.com/lib@v1.0.0",
|
||||||
|
"purl": "pkg:golang/example.com/lib@v1.0.0",
|
||||||
|
"name": "example.com/lib",
|
||||||
|
"version": "v1.0.0",
|
||||||
|
"type": "golang",
|
||||||
|
"usedByEntrypoint": false,
|
||||||
|
"metadata": {
|
||||||
|
"binaryPath": "app",
|
||||||
|
"modulePath": "example.com/lib",
|
||||||
|
"moduleSum": "h1:depchecksum",
|
||||||
|
"moduleVersion": "v1.0.0"
|
||||||
|
},
|
||||||
|
"evidence": [
|
||||||
|
{
|
||||||
|
"kind": "metadata",
|
||||||
|
"source": "go.buildinfo",
|
||||||
|
"locator": "module:example.com/lib",
|
||||||
|
"value": "v1.0.0",
|
||||||
|
"sha256": "h1:depchecksum"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
Binary file not shown.
@@ -0,0 +1,80 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"analyzerId": "golang",
|
||||||
|
"componentKey": "purl::pkg:golang/example.com/app@v0.0.0",
|
||||||
|
"purl": "pkg:golang/example.com/app@v0.0.0",
|
||||||
|
"name": "example.com/app",
|
||||||
|
"version": "v0.0.0",
|
||||||
|
"type": "golang",
|
||||||
|
"usedByEntrypoint": false,
|
||||||
|
"metadata": {
|
||||||
|
"binaryPath": "app",
|
||||||
|
"build.vcs": "git",
|
||||||
|
"build.vcs.modified": "true",
|
||||||
|
"build.vcs.revision": "abcdef0123456789abcdef0123456789abcdef01",
|
||||||
|
"build.vcs.time": "2025-01-02T03:04:05Z",
|
||||||
|
"go.version": "go1.20.3",
|
||||||
|
"modulePath": "example.com/app",
|
||||||
|
"modulePath.main": "example.com/app",
|
||||||
|
"moduleSum": "h1:dwarfchecksum",
|
||||||
|
"moduleVersion": "v0.0.0"
|
||||||
|
},
|
||||||
|
"evidence": [
|
||||||
|
{
|
||||||
|
"kind": "metadata",
|
||||||
|
"source": "go.buildinfo",
|
||||||
|
"locator": "module:example.com/app",
|
||||||
|
"value": "v0.0.0",
|
||||||
|
"sha256": "h1:dwarfchecksum"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "metadata",
|
||||||
|
"source": "go.dwarf",
|
||||||
|
"locator": "vcs.modified",
|
||||||
|
"value": "true"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "metadata",
|
||||||
|
"source": "go.dwarf",
|
||||||
|
"locator": "vcs.revision",
|
||||||
|
"value": "abcdef0123456789abcdef0123456789abcdef01"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "metadata",
|
||||||
|
"source": "go.dwarf",
|
||||||
|
"locator": "vcs.time",
|
||||||
|
"value": "2025-01-02T03:04:05Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "metadata",
|
||||||
|
"source": "go.dwarf",
|
||||||
|
"locator": "vcs",
|
||||||
|
"value": "git"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"analyzerId": "golang",
|
||||||
|
"componentKey": "purl::pkg:golang/example.com/lib@v0.1.0",
|
||||||
|
"purl": "pkg:golang/example.com/lib@v0.1.0",
|
||||||
|
"name": "example.com/lib",
|
||||||
|
"version": "v0.1.0",
|
||||||
|
"type": "golang",
|
||||||
|
"usedByEntrypoint": false,
|
||||||
|
"metadata": {
|
||||||
|
"binaryPath": "app",
|
||||||
|
"modulePath": "example.com/lib",
|
||||||
|
"moduleSum": "h1:libchecksum",
|
||||||
|
"moduleVersion": "v0.1.0"
|
||||||
|
},
|
||||||
|
"evidence": [
|
||||||
|
{
|
||||||
|
"kind": "metadata",
|
||||||
|
"source": "go.buildinfo",
|
||||||
|
"locator": "module:example.com/lib",
|
||||||
|
"value": "v0.1.0",
|
||||||
|
"sha256": "h1:libchecksum"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
using System.IO;
|
||||||
|
using StellaOps.Scanner.Analyzers.Lang.Go;
|
||||||
|
using StellaOps.Scanner.Analyzers.Lang.Tests.Harness;
|
||||||
|
using StellaOps.Scanner.Analyzers.Lang.Tests.TestUtilities;
|
||||||
|
|
||||||
|
namespace StellaOps.Scanner.Analyzers.Lang.Go.Tests;
|
||||||
|
|
||||||
|
public sealed class GoLanguageAnalyzerTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task BuildInfoFixtureProducesDeterministicOutputAsync()
|
||||||
|
{
|
||||||
|
var cancellationToken = TestContext.Current.CancellationToken;
|
||||||
|
var fixturePath = TestPaths.ResolveFixture("lang", "go", "basic");
|
||||||
|
var goldenPath = Path.Combine(fixturePath, "expected.json");
|
||||||
|
|
||||||
|
var analyzers = new ILanguageAnalyzer[]
|
||||||
|
{
|
||||||
|
new GoLanguageAnalyzer(),
|
||||||
|
};
|
||||||
|
|
||||||
|
await LanguageAnalyzerTestHarness.AssertDeterministicAsync(
|
||||||
|
fixturePath,
|
||||||
|
goldenPath,
|
||||||
|
analyzers,
|
||||||
|
cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task DwarfOnlyFixtureFallsBackToMetadataAsync()
|
||||||
|
{
|
||||||
|
var cancellationToken = TestContext.Current.CancellationToken;
|
||||||
|
var fixturePath = TestPaths.ResolveFixture("lang", "go", "dwarf-only");
|
||||||
|
var goldenPath = Path.Combine(fixturePath, "expected.json");
|
||||||
|
|
||||||
|
var analyzers = new ILanguageAnalyzer[]
|
||||||
|
{
|
||||||
|
new GoLanguageAnalyzer(),
|
||||||
|
};
|
||||||
|
|
||||||
|
await LanguageAnalyzerTestHarness.AssertDeterministicAsync(
|
||||||
|
fixturePath,
|
||||||
|
goldenPath,
|
||||||
|
analyzers,
|
||||||
|
cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<LangVersion>preview</LangVersion>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Remove="Microsoft.NET.Test.Sdk" />
|
||||||
|
<PackageReference Remove="xunit" />
|
||||||
|
<PackageReference Remove="xunit.runner.visualstudio" />
|
||||||
|
<PackageReference Remove="Microsoft.AspNetCore.Mvc.Testing" />
|
||||||
|
<PackageReference Remove="Mongo2Go" />
|
||||||
|
<PackageReference Remove="coverlet.collector" />
|
||||||
|
<PackageReference Remove="Microsoft.Extensions.TimeProvider.Testing" />
|
||||||
|
<ProjectReference Remove="..\StellaOps.Concelier.Testing\StellaOps.Concelier.Testing.csproj" />
|
||||||
|
<Compile Remove="$(MSBuildThisFileDirectory)..\StellaOps.Concelier.Tests.Shared\AssemblyInfo.cs" />
|
||||||
|
<Compile Remove="$(MSBuildThisFileDirectory)..\StellaOps.Concelier.Tests.Shared\MongoFixtureCollection.cs" />
|
||||||
|
<Using Remove="StellaOps.Concelier.Testing" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||||
|
<PackageReference Include="xunit.v3" Version="3.0.0" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.3" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\StellaOps.Scanner.Analyzers.Lang.Tests\StellaOps.Scanner.Analyzers.Lang.Tests.csproj" />
|
||||||
|
<ProjectReference Include="..\StellaOps.Scanner.Analyzers.Lang\StellaOps.Scanner.Analyzers.Lang.csproj" />
|
||||||
|
<ProjectReference Include="..\StellaOps.Scanner.Analyzers.Lang.Go\StellaOps.Scanner.Analyzers.Lang.Go.csproj" />
|
||||||
|
<ProjectReference Include="..\StellaOps.Scanner.Core\StellaOps.Scanner.Core.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Include="Fixtures\**\*" CopyToOutputDirectory="PreserveNewest" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Using Include="Xunit" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
17
src/StellaOps.Scanner.Analyzers.Lang.Go/GoAnalyzerPlugin.cs
Normal file
17
src/StellaOps.Scanner.Analyzers.Lang.Go/GoAnalyzerPlugin.cs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
using System;
|
||||||
|
using StellaOps.Scanner.Analyzers.Lang.Plugin;
|
||||||
|
|
||||||
|
namespace StellaOps.Scanner.Analyzers.Lang.Go;
|
||||||
|
|
||||||
|
public sealed class GoAnalyzerPlugin : ILanguageAnalyzerPlugin
|
||||||
|
{
|
||||||
|
public string Name => "StellaOps.Scanner.Analyzers.Lang.Go";
|
||||||
|
|
||||||
|
public bool IsAvailable(IServiceProvider services) => services is not null;
|
||||||
|
|
||||||
|
public ILanguageAnalyzer CreateAnalyzer(IServiceProvider services)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(services);
|
||||||
|
return new GoLanguageAnalyzer();
|
||||||
|
}
|
||||||
|
}
|
||||||
292
src/StellaOps.Scanner.Analyzers.Lang.Go/GoLanguageAnalyzer.cs
Normal file
292
src/StellaOps.Scanner.Analyzers.Lang.Go/GoLanguageAnalyzer.cs
Normal file
@@ -0,0 +1,292 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Linq;
|
||||||
|
using StellaOps.Scanner.Analyzers.Lang.Go.Internal;
|
||||||
|
|
||||||
|
namespace StellaOps.Scanner.Analyzers.Lang.Go;
|
||||||
|
|
||||||
|
public sealed class GoLanguageAnalyzer : ILanguageAnalyzer
|
||||||
|
{
|
||||||
|
public string Id => "golang";
|
||||||
|
|
||||||
|
public string DisplayName => "Go Analyzer";
|
||||||
|
|
||||||
|
public ValueTask AnalyzeAsync(LanguageAnalyzerContext context, LanguageComponentWriter writer, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(context);
|
||||||
|
ArgumentNullException.ThrowIfNull(writer);
|
||||||
|
|
||||||
|
var candidatePaths = new List<string>(GoBinaryScanner.EnumerateCandidateFiles(context.RootPath));
|
||||||
|
candidatePaths.Sort(StringComparer.Ordinal);
|
||||||
|
|
||||||
|
foreach (var absolutePath in candidatePaths)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
if (!GoBuildInfoProvider.TryGetBuildInfo(absolutePath, out var buildInfo) || buildInfo is null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
EmitComponents(buildInfo, context, writer);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ValueTask.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EmitComponents(GoBuildInfo buildInfo, LanguageAnalyzerContext context, LanguageComponentWriter writer)
|
||||||
|
{
|
||||||
|
var components = new List<GoModule> { buildInfo.MainModule };
|
||||||
|
components.AddRange(buildInfo.Dependencies
|
||||||
|
.OrderBy(static module => module.Path, StringComparer.Ordinal)
|
||||||
|
.ThenBy(static module => module.Version, StringComparer.Ordinal));
|
||||||
|
|
||||||
|
string? binaryHash = null;
|
||||||
|
var binaryRelativePath = context.GetRelativePath(buildInfo.AbsoluteBinaryPath);
|
||||||
|
|
||||||
|
foreach (var module in components)
|
||||||
|
{
|
||||||
|
var metadata = BuildMetadata(buildInfo, module, binaryRelativePath);
|
||||||
|
var evidence = BuildEvidence(buildInfo, module, binaryRelativePath, context, ref binaryHash);
|
||||||
|
var usedByEntrypoint = module.IsMain && context.UsageHints.IsPathUsed(buildInfo.AbsoluteBinaryPath);
|
||||||
|
|
||||||
|
var purl = BuildPurl(module.Path, module.Version);
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(purl))
|
||||||
|
{
|
||||||
|
writer.AddFromPurl(
|
||||||
|
analyzerId: Id,
|
||||||
|
purl: purl,
|
||||||
|
name: module.Path,
|
||||||
|
version: module.Version,
|
||||||
|
type: "golang",
|
||||||
|
metadata: metadata,
|
||||||
|
evidence: evidence,
|
||||||
|
usedByEntrypoint: usedByEntrypoint);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var componentKey = BuildFallbackComponentKey(module, buildInfo, binaryRelativePath, ref binaryHash);
|
||||||
|
|
||||||
|
writer.AddFromExplicitKey(
|
||||||
|
analyzerId: Id,
|
||||||
|
componentKey: componentKey,
|
||||||
|
purl: null,
|
||||||
|
name: module.Path,
|
||||||
|
version: module.Version,
|
||||||
|
type: "golang",
|
||||||
|
metadata: metadata,
|
||||||
|
evidence: evidence,
|
||||||
|
usedByEntrypoint: usedByEntrypoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<KeyValuePair<string, string?>> BuildMetadata(GoBuildInfo buildInfo, GoModule module, string binaryRelativePath)
|
||||||
|
{
|
||||||
|
var entries = new List<KeyValuePair<string, string?>>(16)
|
||||||
|
{
|
||||||
|
new("modulePath", module.Path),
|
||||||
|
new("binaryPath", string.IsNullOrEmpty(binaryRelativePath) ? "." : binaryRelativePath),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(module.Version))
|
||||||
|
{
|
||||||
|
entries.Add(new KeyValuePair<string, string?>("moduleVersion", module.Version));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(module.Sum))
|
||||||
|
{
|
||||||
|
entries.Add(new KeyValuePair<string, string?>("moduleSum", module.Sum));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (module.Replacement is not null)
|
||||||
|
{
|
||||||
|
entries.Add(new KeyValuePair<string, string?>("replacedBy.path", module.Replacement.Path));
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(module.Replacement.Version))
|
||||||
|
{
|
||||||
|
entries.Add(new KeyValuePair<string, string?>("replacedBy.version", module.Replacement.Version));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(module.Replacement.Sum))
|
||||||
|
{
|
||||||
|
entries.Add(new KeyValuePair<string, string?>("replacedBy.sum", module.Replacement.Sum));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (module.IsMain)
|
||||||
|
{
|
||||||
|
entries.Add(new KeyValuePair<string, string?>("go.version", buildInfo.GoVersion));
|
||||||
|
entries.Add(new KeyValuePair<string, string?>("modulePath.main", buildInfo.ModulePath));
|
||||||
|
|
||||||
|
foreach (var setting in buildInfo.Settings)
|
||||||
|
{
|
||||||
|
var key = $"build.{setting.Key}";
|
||||||
|
if (!entries.Any(pair => string.Equals(pair.Key, key, StringComparison.Ordinal)))
|
||||||
|
{
|
||||||
|
entries.Add(new KeyValuePair<string, string?>(key, setting.Value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buildInfo.DwarfMetadata is { } dwarf)
|
||||||
|
{
|
||||||
|
AddIfMissing(entries, "build.vcs", dwarf.VcsSystem);
|
||||||
|
AddIfMissing(entries, "build.vcs.revision", dwarf.Revision);
|
||||||
|
AddIfMissing(entries, "build.vcs.modified", dwarf.Modified?.ToString()?.ToLowerInvariant());
|
||||||
|
AddIfMissing(entries, "build.vcs.time", dwarf.TimestampUtc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
entries.Sort(static (left, right) => string.CompareOrdinal(left.Key, right.Key));
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<LanguageComponentEvidence> BuildEvidence(GoBuildInfo buildInfo, GoModule module, string binaryRelativePath, LanguageAnalyzerContext context, ref string? binaryHash)
|
||||||
|
{
|
||||||
|
var evidence = new List<LanguageComponentEvidence>
|
||||||
|
{
|
||||||
|
new(
|
||||||
|
LanguageEvidenceKind.Metadata,
|
||||||
|
"go.buildinfo",
|
||||||
|
$"module:{module.Path}",
|
||||||
|
module.Version ?? string.Empty,
|
||||||
|
module.Sum)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (module.IsMain)
|
||||||
|
{
|
||||||
|
foreach (var setting in buildInfo.Settings)
|
||||||
|
{
|
||||||
|
evidence.Add(new LanguageComponentEvidence(
|
||||||
|
LanguageEvidenceKind.Metadata,
|
||||||
|
"go.buildinfo.setting",
|
||||||
|
setting.Key,
|
||||||
|
setting.Value,
|
||||||
|
null));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buildInfo.DwarfMetadata is { } dwarf)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(dwarf.VcsSystem))
|
||||||
|
{
|
||||||
|
evidence.Add(new LanguageComponentEvidence(
|
||||||
|
LanguageEvidenceKind.Metadata,
|
||||||
|
"go.dwarf",
|
||||||
|
"vcs",
|
||||||
|
dwarf.VcsSystem,
|
||||||
|
null));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(dwarf.Revision))
|
||||||
|
{
|
||||||
|
evidence.Add(new LanguageComponentEvidence(
|
||||||
|
LanguageEvidenceKind.Metadata,
|
||||||
|
"go.dwarf",
|
||||||
|
"vcs.revision",
|
||||||
|
dwarf.Revision,
|
||||||
|
null));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dwarf.Modified.HasValue)
|
||||||
|
{
|
||||||
|
evidence.Add(new LanguageComponentEvidence(
|
||||||
|
LanguageEvidenceKind.Metadata,
|
||||||
|
"go.dwarf",
|
||||||
|
"vcs.modified",
|
||||||
|
dwarf.Modified.Value ? "true" : "false",
|
||||||
|
null));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(dwarf.TimestampUtc))
|
||||||
|
{
|
||||||
|
evidence.Add(new LanguageComponentEvidence(
|
||||||
|
LanguageEvidenceKind.Metadata,
|
||||||
|
"go.dwarf",
|
||||||
|
"vcs.time",
|
||||||
|
dwarf.TimestampUtc,
|
||||||
|
null));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attach binary hash evidence for fallback components without purl.
|
||||||
|
if (string.IsNullOrEmpty(module.Version))
|
||||||
|
{
|
||||||
|
binaryHash ??= ComputeBinaryHash(buildInfo.AbsoluteBinaryPath);
|
||||||
|
if (!string.IsNullOrEmpty(binaryHash))
|
||||||
|
{
|
||||||
|
evidence.Add(new LanguageComponentEvidence(
|
||||||
|
LanguageEvidenceKind.File,
|
||||||
|
"binary",
|
||||||
|
string.IsNullOrEmpty(binaryRelativePath) ? "." : binaryRelativePath,
|
||||||
|
null,
|
||||||
|
binaryHash));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
evidence.Sort(static (left, right) => string.CompareOrdinal(left.ComparisonKey, right.ComparisonKey));
|
||||||
|
return evidence;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? BuildPurl(string path, string? version)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(path) || string.IsNullOrWhiteSpace(version))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var cleanedPath = path.Trim();
|
||||||
|
var cleanedVersion = version.Trim();
|
||||||
|
var encodedVersion = Uri.EscapeDataString(cleanedVersion);
|
||||||
|
return $"pkg:golang/{cleanedPath}@{encodedVersion}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string BuildFallbackComponentKey(GoModule module, GoBuildInfo buildInfo, string binaryRelativePath, ref string? binaryHash)
|
||||||
|
{
|
||||||
|
var relative = string.IsNullOrEmpty(binaryRelativePath) ? "." : binaryRelativePath;
|
||||||
|
binaryHash ??= ComputeBinaryHash(buildInfo.AbsoluteBinaryPath);
|
||||||
|
if (!string.IsNullOrEmpty(binaryHash))
|
||||||
|
{
|
||||||
|
return $"golang::module:{module.Path}::{relative}::{binaryHash}";
|
||||||
|
}
|
||||||
|
|
||||||
|
return $"golang::module:{module.Path}::{relative}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddIfMissing(List<KeyValuePair<string, string?>> entries, string key, string? value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(key) || string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entries.Any(entry => string.Equals(entry.Key, key, StringComparison.Ordinal)))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
entries.Add(new KeyValuePair<string, string?>(key, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? ComputeBinaryHash(string path)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||||
|
using var sha = SHA256.Create();
|
||||||
|
var hash = sha.ComputeHash(stream);
|
||||||
|
return Convert.ToHexString(hash).ToLowerInvariant();
|
||||||
|
}
|
||||||
|
catch (IOException)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
catch (UnauthorizedAccessException)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace StellaOps.Scanner.Analyzers.Lang.Go.Internal;
|
||||||
|
|
||||||
|
internal static class GoBinaryScanner
|
||||||
|
{
|
||||||
|
private static readonly ReadOnlyMemory<byte> BuildInfoMagic = new byte[]
|
||||||
|
{
|
||||||
|
0xFF, (byte)' ', (byte)'G', (byte)'o', (byte)' ', (byte)'b', (byte)'u', (byte)'i', (byte)'l', (byte)'d', (byte)'i', (byte)'n', (byte)'f', (byte)':'
|
||||||
|
};
|
||||||
|
|
||||||
|
public static IEnumerable<string> EnumerateCandidateFiles(string rootPath)
|
||||||
|
{
|
||||||
|
var enumeration = new EnumerationOptions
|
||||||
|
{
|
||||||
|
RecurseSubdirectories = true,
|
||||||
|
IgnoreInaccessible = true,
|
||||||
|
AttributesToSkip = FileAttributes.Device | FileAttributes.ReparsePoint,
|
||||||
|
MatchCasing = MatchCasing.CaseSensitive,
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var path in Directory.EnumerateFiles(rootPath, "*", enumeration))
|
||||||
|
{
|
||||||
|
yield return path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool TryReadBuildInfo(string filePath, out string? goVersion, out string? moduleData)
|
||||||
|
{
|
||||||
|
goVersion = null;
|
||||||
|
moduleData = null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var info = new FileInfo(filePath);
|
||||||
|
if (!info.Exists || info.Length < 64 || info.Length > 128 * 1024 * 1024)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var data = File.ReadAllBytes(filePath);
|
||||||
|
var span = new ReadOnlySpan<byte>(data);
|
||||||
|
var offset = span.IndexOf(BuildInfoMagic.Span);
|
||||||
|
if (offset < 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var view = span[offset..];
|
||||||
|
return GoBuildInfoDecoder.TryDecode(view, out goVersion, out moduleData);
|
||||||
|
}
|
||||||
|
catch (IOException)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
catch (UnauthorizedAccessException)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace StellaOps.Scanner.Analyzers.Lang.Go.Internal;
|
||||||
|
|
||||||
|
internal sealed class GoBuildInfo
|
||||||
|
{
|
||||||
|
public GoBuildInfo(
|
||||||
|
string goVersion,
|
||||||
|
string absoluteBinaryPath,
|
||||||
|
string modulePath,
|
||||||
|
GoModule mainModule,
|
||||||
|
IEnumerable<GoModule> dependencies,
|
||||||
|
IEnumerable<KeyValuePair<string, string?>> settings,
|
||||||
|
GoDwarfMetadata? dwarfMetadata = null)
|
||||||
|
: this(
|
||||||
|
goVersion,
|
||||||
|
absoluteBinaryPath,
|
||||||
|
modulePath,
|
||||||
|
mainModule,
|
||||||
|
dependencies?
|
||||||
|
.Where(static module => module is not null)
|
||||||
|
.ToImmutableArray()
|
||||||
|
?? ImmutableArray<GoModule>.Empty,
|
||||||
|
settings?
|
||||||
|
.Where(static pair => pair.Key is not null)
|
||||||
|
.Select(static pair => new KeyValuePair<string, string?>(pair.Key, pair.Value))
|
||||||
|
.ToImmutableArray()
|
||||||
|
?? ImmutableArray<KeyValuePair<string, string?>>.Empty,
|
||||||
|
dwarfMetadata)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
private GoBuildInfo(
|
||||||
|
string goVersion,
|
||||||
|
string absoluteBinaryPath,
|
||||||
|
string modulePath,
|
||||||
|
GoModule mainModule,
|
||||||
|
ImmutableArray<GoModule> dependencies,
|
||||||
|
ImmutableArray<KeyValuePair<string, string?>> settings,
|
||||||
|
GoDwarfMetadata? dwarfMetadata)
|
||||||
|
{
|
||||||
|
GoVersion = goVersion ?? throw new ArgumentNullException(nameof(goVersion));
|
||||||
|
AbsoluteBinaryPath = absoluteBinaryPath ?? throw new ArgumentNullException(nameof(absoluteBinaryPath));
|
||||||
|
ModulePath = modulePath ?? throw new ArgumentNullException(nameof(modulePath));
|
||||||
|
MainModule = mainModule ?? throw new ArgumentNullException(nameof(mainModule));
|
||||||
|
Dependencies = dependencies;
|
||||||
|
Settings = settings;
|
||||||
|
DwarfMetadata = dwarfMetadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GoVersion { get; }
|
||||||
|
|
||||||
|
public string AbsoluteBinaryPath { get; }
|
||||||
|
|
||||||
|
public string ModulePath { get; }
|
||||||
|
|
||||||
|
public GoModule MainModule { get; }
|
||||||
|
|
||||||
|
public ImmutableArray<GoModule> Dependencies { get; }
|
||||||
|
|
||||||
|
public ImmutableArray<KeyValuePair<string, string?>> Settings { get; }
|
||||||
|
|
||||||
|
public GoDwarfMetadata? DwarfMetadata { get; }
|
||||||
|
|
||||||
|
public GoBuildInfo WithDwarf(GoDwarfMetadata metadata)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(metadata);
|
||||||
|
return new GoBuildInfo(
|
||||||
|
GoVersion,
|
||||||
|
AbsoluteBinaryPath,
|
||||||
|
ModulePath,
|
||||||
|
MainModule,
|
||||||
|
Dependencies,
|
||||||
|
Settings,
|
||||||
|
metadata);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
using System;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace StellaOps.Scanner.Analyzers.Lang.Go.Internal;
|
||||||
|
|
||||||
|
internal static class GoBuildInfoDecoder
|
||||||
|
{
|
||||||
|
private const string BuildInfoMagic = "\xff Go buildinf:";
|
||||||
|
private const int HeaderSize = 32;
|
||||||
|
private const byte VarintEncodingFlag = 0x02;
|
||||||
|
|
||||||
|
public static bool TryDecode(ReadOnlySpan<byte> data, out string? goVersion, out string? moduleData)
|
||||||
|
{
|
||||||
|
goVersion = null;
|
||||||
|
moduleData = null;
|
||||||
|
|
||||||
|
if (data.Length < HeaderSize)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!IsMagicMatch(data))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var pointerSize = data[14];
|
||||||
|
var flags = data[15];
|
||||||
|
|
||||||
|
if (pointerSize != 4 && pointerSize != 8)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((flags & VarintEncodingFlag) == 0)
|
||||||
|
{
|
||||||
|
// Older Go toolchains encode pointers to strings instead of inline data.
|
||||||
|
// The Sprint 10 scope targets Go 1.18+, which always sets the varint flag.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload = data.Slice(HeaderSize);
|
||||||
|
|
||||||
|
if (!TryReadVarString(payload, out var version, out var consumed))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
payload = payload.Slice(consumed);
|
||||||
|
|
||||||
|
if (!TryReadVarString(payload, out var modules, out _))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(version))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
modules = StripSentinel(modules);
|
||||||
|
|
||||||
|
goVersion = version;
|
||||||
|
moduleData = modules;
|
||||||
|
return !string.IsNullOrWhiteSpace(moduleData);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsMagicMatch(ReadOnlySpan<byte> data)
|
||||||
|
{
|
||||||
|
if (data.Length < BuildInfoMagic.Length)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < BuildInfoMagic.Length; i++)
|
||||||
|
{
|
||||||
|
if (data[i] != BuildInfoMagic[i])
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryReadVarString(ReadOnlySpan<byte> data, out string result, out int consumed)
|
||||||
|
{
|
||||||
|
result = string.Empty;
|
||||||
|
consumed = 0;
|
||||||
|
|
||||||
|
if (!TryReadUVarint(data, out var length, out var lengthBytes))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (length > int.MaxValue)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var stringLength = (int)length;
|
||||||
|
var totalRequired = lengthBytes + stringLength;
|
||||||
|
if (stringLength <= 0 || totalRequired > data.Length)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var slice = data.Slice(lengthBytes, stringLength);
|
||||||
|
result = Encoding.UTF8.GetString(slice);
|
||||||
|
consumed = totalRequired;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryReadUVarint(ReadOnlySpan<byte> data, out ulong value, out int bytesRead)
|
||||||
|
{
|
||||||
|
value = 0;
|
||||||
|
bytesRead = 0;
|
||||||
|
|
||||||
|
ulong x = 0;
|
||||||
|
var shift = 0;
|
||||||
|
|
||||||
|
for (var i = 0; i < data.Length; i++)
|
||||||
|
{
|
||||||
|
var b = data[i];
|
||||||
|
if (b < 0x80)
|
||||||
|
{
|
||||||
|
if (i > 9 || i == 9 && b > 1)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = x | (ulong)b << shift;
|
||||||
|
bytesRead = i + 1;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
x |= (ulong)(b & 0x7F) << shift;
|
||||||
|
shift += 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string StripSentinel(string value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(value) || value.Length < 33)
|
||||||
|
{
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sentinelIndex = value.Length - 17;
|
||||||
|
if (value[sentinelIndex] != '\n')
|
||||||
|
{
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value[16..^16];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,234 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace StellaOps.Scanner.Analyzers.Lang.Go.Internal;
|
||||||
|
|
||||||
|
internal static class GoBuildInfoParser
|
||||||
|
{
|
||||||
|
private const string PathPrefix = "path\t";
|
||||||
|
private const string ModulePrefix = "mod\t";
|
||||||
|
private const string DependencyPrefix = "dep\t";
|
||||||
|
private const string ReplacementPrefix = "=>\t";
|
||||||
|
private const string BuildPrefix = "build\t";
|
||||||
|
|
||||||
|
public static bool TryParse(string goVersion, string absoluteBinaryPath, string rawModuleData, out GoBuildInfo? info)
|
||||||
|
{
|
||||||
|
info = null;
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(goVersion) || string.IsNullOrWhiteSpace(rawModuleData))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
string? modulePath = null;
|
||||||
|
GoModule? mainModule = null;
|
||||||
|
var dependencies = new List<GoModule>();
|
||||||
|
var settings = new SortedDictionary<string, string?>(StringComparer.Ordinal);
|
||||||
|
|
||||||
|
GoModule? lastModule = null;
|
||||||
|
using var reader = new StringReader(rawModuleData);
|
||||||
|
|
||||||
|
while (reader.ReadLine() is { } line)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(line))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.StartsWith(PathPrefix, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
modulePath = line[PathPrefix.Length..].Trim();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.StartsWith(ModulePrefix, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
mainModule = ParseModule(line.AsSpan(ModulePrefix.Length), isMain: true);
|
||||||
|
lastModule = mainModule;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.StartsWith(DependencyPrefix, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
var dependency = ParseModule(line.AsSpan(DependencyPrefix.Length), isMain: false);
|
||||||
|
if (dependency is not null)
|
||||||
|
{
|
||||||
|
dependencies.Add(dependency);
|
||||||
|
lastModule = dependency;
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.StartsWith(ReplacementPrefix, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
if (lastModule is null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var replacement = ParseReplacement(line.AsSpan(ReplacementPrefix.Length));
|
||||||
|
if (replacement is not null)
|
||||||
|
{
|
||||||
|
lastModule.SetReplacement(replacement);
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.StartsWith(BuildPrefix, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
var pair = ParseBuildSetting(line.AsSpan(BuildPrefix.Length));
|
||||||
|
if (!string.IsNullOrEmpty(pair.Key))
|
||||||
|
{
|
||||||
|
settings[pair.Key] = pair.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mainModule is null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(modulePath))
|
||||||
|
{
|
||||||
|
modulePath = mainModule.Path;
|
||||||
|
}
|
||||||
|
|
||||||
|
info = new GoBuildInfo(
|
||||||
|
goVersion,
|
||||||
|
absoluteBinaryPath,
|
||||||
|
modulePath,
|
||||||
|
mainModule,
|
||||||
|
dependencies,
|
||||||
|
settings);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GoModule? ParseModule(ReadOnlySpan<char> span, bool isMain)
|
||||||
|
{
|
||||||
|
var fields = SplitFields(span, expected: 4);
|
||||||
|
if (fields.Count == 0)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var path = fields[0];
|
||||||
|
if (string.IsNullOrWhiteSpace(path))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var version = fields.Count > 1 ? fields[1] : null;
|
||||||
|
var sum = fields.Count > 2 ? fields[2] : null;
|
||||||
|
|
||||||
|
return new GoModule(path, version, sum, isMain);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GoModuleReplacement? ParseReplacement(ReadOnlySpan<char> span)
|
||||||
|
{
|
||||||
|
var fields = SplitFields(span, expected: 3);
|
||||||
|
if (fields.Count == 0)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var path = fields[0];
|
||||||
|
if (string.IsNullOrWhiteSpace(path))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var version = fields.Count > 1 ? fields[1] : null;
|
||||||
|
var sum = fields.Count > 2 ? fields[2] : null;
|
||||||
|
|
||||||
|
return new GoModuleReplacement(path, version, sum);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static KeyValuePair<string, string?> ParseBuildSetting(ReadOnlySpan<char> span)
|
||||||
|
{
|
||||||
|
span = span.Trim();
|
||||||
|
if (span.IsEmpty)
|
||||||
|
{
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
|
var separatorIndex = span.IndexOf('=');
|
||||||
|
if (separatorIndex <= 0)
|
||||||
|
{
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
|
var rawKey = span[..separatorIndex].Trim();
|
||||||
|
var rawValue = span[(separatorIndex + 1)..].Trim();
|
||||||
|
|
||||||
|
var key = Unquote(rawKey.ToString());
|
||||||
|
if (string.IsNullOrWhiteSpace(key))
|
||||||
|
{
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
|
var value = Unquote(rawValue.ToString());
|
||||||
|
return new KeyValuePair<string, string?>(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<string> SplitFields(ReadOnlySpan<char> span, int expected)
|
||||||
|
{
|
||||||
|
var fields = new List<string>(expected);
|
||||||
|
var builder = new StringBuilder();
|
||||||
|
|
||||||
|
for (var i = 0; i < span.Length; i++)
|
||||||
|
{
|
||||||
|
var current = span[i];
|
||||||
|
if (current == '\t')
|
||||||
|
{
|
||||||
|
fields.Add(builder.ToString());
|
||||||
|
builder.Clear();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.Append(current);
|
||||||
|
}
|
||||||
|
|
||||||
|
fields.Add(builder.ToString());
|
||||||
|
return fields;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Unquote(string value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(value))
|
||||||
|
{
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = value.Trim();
|
||||||
|
if (value.Length < 2)
|
||||||
|
{
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value[0] == '"' && value[^1] == '"')
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return JsonSerializer.Deserialize<string>(value) ?? value;
|
||||||
|
}
|
||||||
|
catch (JsonException)
|
||||||
|
{
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value[0] == '`' && value[^1] == '`')
|
||||||
|
{
|
||||||
|
return value[1..^1];
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.IO;
|
||||||
|
using System.Security;
|
||||||
|
|
||||||
|
namespace StellaOps.Scanner.Analyzers.Lang.Go.Internal;
|
||||||
|
|
||||||
|
internal static class GoBuildInfoProvider
|
||||||
|
{
|
||||||
|
private static readonly ConcurrentDictionary<GoBinaryCacheKey, GoBuildInfo?> Cache = new();
|
||||||
|
|
||||||
|
public static bool TryGetBuildInfo(string absolutePath, out GoBuildInfo? info)
|
||||||
|
{
|
||||||
|
info = null;
|
||||||
|
|
||||||
|
FileInfo fileInfo;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
fileInfo = new FileInfo(absolutePath);
|
||||||
|
if (!fileInfo.Exists)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (IOException)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
catch (UnauthorizedAccessException)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
catch (System.Security.SecurityException)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var key = new GoBinaryCacheKey(absolutePath, fileInfo.Length, fileInfo.LastWriteTimeUtc.Ticks);
|
||||||
|
info = Cache.GetOrAdd(key, static (cacheKey, path) => CreateBuildInfo(path), absolutePath);
|
||||||
|
return info is not null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GoBuildInfo? CreateBuildInfo(string absolutePath)
|
||||||
|
{
|
||||||
|
if (!GoBinaryScanner.TryReadBuildInfo(absolutePath, out var goVersion, out var moduleData))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(goVersion) || string.IsNullOrWhiteSpace(moduleData))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!GoBuildInfoParser.TryParse(goVersion!, absolutePath, moduleData!, out var buildInfo) || buildInfo is null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (GoDwarfReader.TryRead(absolutePath, out var dwarf) && dwarf is not null)
|
||||||
|
{
|
||||||
|
buildInfo = buildInfo.WithDwarf(dwarf);
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly record struct GoBinaryCacheKey(string Path, long Length, long LastWriteTicks)
|
||||||
|
{
|
||||||
|
private readonly string _normalizedPath = OperatingSystem.IsWindows()
|
||||||
|
? Path.ToLowerInvariant()
|
||||||
|
: Path;
|
||||||
|
|
||||||
|
public bool Equals(GoBinaryCacheKey other)
|
||||||
|
=> Length == other.Length
|
||||||
|
&& LastWriteTicks == other.LastWriteTicks
|
||||||
|
&& string.Equals(_normalizedPath, other._normalizedPath, StringComparison.Ordinal);
|
||||||
|
|
||||||
|
public override int GetHashCode()
|
||||||
|
=> HashCode.Combine(_normalizedPath, Length, LastWriteTicks);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace StellaOps.Scanner.Analyzers.Lang.Go.Internal;
|
||||||
|
|
||||||
|
internal sealed class GoDwarfMetadata
|
||||||
|
{
|
||||||
|
public GoDwarfMetadata(string? vcsSystem, string? revision, bool? modified, string? timestampUtc)
|
||||||
|
{
|
||||||
|
VcsSystem = Normalize(vcsSystem);
|
||||||
|
Revision = Normalize(revision);
|
||||||
|
Modified = modified;
|
||||||
|
TimestampUtc = Normalize(timestampUtc);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string? VcsSystem { get; }
|
||||||
|
|
||||||
|
public string? Revision { get; }
|
||||||
|
|
||||||
|
public bool? Modified { get; }
|
||||||
|
|
||||||
|
public string? TimestampUtc { get; }
|
||||||
|
|
||||||
|
private static string? Normalize(string? value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var trimmed = value.Trim();
|
||||||
|
return trimmed.Length == 0 ? null : trimmed;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace StellaOps.Scanner.Analyzers.Lang.Go.Internal;
|
||||||
|
|
||||||
|
internal static class GoDwarfReader
|
||||||
|
{
|
||||||
|
private static readonly byte[] VcsSystemToken = Encoding.UTF8.GetBytes("vcs=");
|
||||||
|
private static readonly byte[] VcsRevisionToken = Encoding.UTF8.GetBytes("vcs.revision=");
|
||||||
|
private static readonly byte[] VcsModifiedToken = Encoding.UTF8.GetBytes("vcs.modified=");
|
||||||
|
private static readonly byte[] VcsTimeToken = Encoding.UTF8.GetBytes("vcs.time=");
|
||||||
|
|
||||||
|
public static bool TryRead(string path, out GoDwarfMetadata? metadata)
|
||||||
|
{
|
||||||
|
metadata = null;
|
||||||
|
|
||||||
|
ReadOnlySpan<byte> data;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var fileInfo = new FileInfo(path);
|
||||||
|
if (!fileInfo.Exists || fileInfo.Length == 0 || fileInfo.Length > 256 * 1024 * 1024)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
data = File.ReadAllBytes(path);
|
||||||
|
}
|
||||||
|
catch (IOException)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
catch (UnauthorizedAccessException)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var revision = ExtractValue(data, VcsRevisionToken);
|
||||||
|
var modifiedText = ExtractValue(data, VcsModifiedToken);
|
||||||
|
var timestamp = ExtractValue(data, VcsTimeToken);
|
||||||
|
var system = ExtractValue(data, VcsSystemToken);
|
||||||
|
|
||||||
|
bool? modified = null;
|
||||||
|
if (!string.IsNullOrWhiteSpace(modifiedText))
|
||||||
|
{
|
||||||
|
if (bool.TryParse(modifiedText, out var parsed))
|
||||||
|
{
|
||||||
|
modified = parsed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(revision) && string.IsNullOrWhiteSpace(system) && modified is null && string.IsNullOrWhiteSpace(timestamp))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata = new GoDwarfMetadata(system, revision, modified, timestamp);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? ExtractValue(ReadOnlySpan<byte> data, ReadOnlySpan<byte> token)
|
||||||
|
{
|
||||||
|
var index = data.IndexOf(token);
|
||||||
|
if (index < 0)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var start = index + token.Length;
|
||||||
|
var end = start;
|
||||||
|
|
||||||
|
while (end < data.Length)
|
||||||
|
{
|
||||||
|
var current = data[end];
|
||||||
|
if (current == 0 || current == (byte)'\n' || current == (byte)'\r')
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
end++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (end <= start)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Encoding.UTF8.GetString(data.Slice(start, end - start));
|
||||||
|
}
|
||||||
|
}
|
||||||
67
src/StellaOps.Scanner.Analyzers.Lang.Go/Internal/GoModule.cs
Normal file
67
src/StellaOps.Scanner.Analyzers.Lang.Go/Internal/GoModule.cs
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace StellaOps.Scanner.Analyzers.Lang.Go.Internal;
|
||||||
|
|
||||||
|
internal sealed class GoModule
|
||||||
|
{
|
||||||
|
public GoModule(string path, string? version, string? sum, bool isMain)
|
||||||
|
{
|
||||||
|
Path = path ?? throw new ArgumentNullException(nameof(path));
|
||||||
|
Version = Normalize(version);
|
||||||
|
Sum = Normalize(sum);
|
||||||
|
IsMain = isMain;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Path { get; }
|
||||||
|
|
||||||
|
public string? Version { get; }
|
||||||
|
|
||||||
|
public string? Sum { get; }
|
||||||
|
|
||||||
|
public GoModuleReplacement? Replacement { get; private set; }
|
||||||
|
|
||||||
|
public bool IsMain { get; }
|
||||||
|
|
||||||
|
public void SetReplacement(GoModuleReplacement replacement)
|
||||||
|
{
|
||||||
|
Replacement = replacement ?? throw new ArgumentNullException(nameof(replacement));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? Normalize(string? value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var trimmed = value.Trim();
|
||||||
|
return trimmed.Length == 0 ? null : trimmed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class GoModuleReplacement
|
||||||
|
{
|
||||||
|
public GoModuleReplacement(string path, string? version, string? sum)
|
||||||
|
{
|
||||||
|
Path = path ?? throw new ArgumentNullException(nameof(path));
|
||||||
|
Version = Normalize(version);
|
||||||
|
Sum = Normalize(sum);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Path { get; }
|
||||||
|
|
||||||
|
public string? Version { get; }
|
||||||
|
|
||||||
|
public string? Sum { get; }
|
||||||
|
|
||||||
|
private static string? Normalize(string? value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var trimmed = value.Trim();
|
||||||
|
return trimmed.Length == 0 ? null : trimmed;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
namespace StellaOps.Scanner.Analyzers.Lang.Go;
|
|
||||||
|
|
||||||
internal static class Placeholder
|
|
||||||
{
|
|
||||||
// Analyzer implementation will be added during Sprint LA3.
|
|
||||||
}
|
|
||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
| Seq | ID | Status | Depends on | Description | Exit Criteria |
|
| Seq | ID | Status | Depends on | Description | Exit Criteria |
|
||||||
|-----|----|--------|------------|-------------|---------------|
|
|-----|----|--------|------------|-------------|---------------|
|
||||||
| 1 | SCANNER-ANALYZERS-LANG-10-304A | TODO | SCANNER-ANALYZERS-LANG-10-307 | Parse Go build info blob (`runtime/debug` format) and `.note.go.buildid`; map to module/version and evidence. | Build info extracted across Go 1.18–1.23 fixtures; evidence includes VCS, module path, and build settings. |
|
| 1 | SCANNER-ANALYZERS-LANG-10-304A | DONE (2025-10-22) | SCANNER-ANALYZERS-LANG-10-307 | Parse Go build info blob (`runtime/debug` format) and `.note.go.buildid`; map to module/version and evidence. | Build info extracted across Go 1.18–1.23 fixtures; evidence includes VCS, module path, and build settings. |
|
||||||
| 2 | SCANNER-ANALYZERS-LANG-10-304B | TODO | SCANNER-ANALYZERS-LANG-10-304A | Implement DWARF-lite reader for VCS metadata + dirty flag; add cache to avoid re-reading identical binaries. | DWARF reader supplies commit hash for ≥95 % fixtures; cache reduces duplicated IO by ≥70 %. |
|
| 2 | SCANNER-ANALYZERS-LANG-10-304B | DONE (2025-10-22) | SCANNER-ANALYZERS-LANG-10-304A | Implement DWARF-lite reader for VCS metadata + dirty flag; add cache to avoid re-reading identical binaries. | DWARF reader supplies commit hash for ≥95 % fixtures; cache reduces duplicated IO by ≥70 %. |
|
||||||
| 3 | SCANNER-ANALYZERS-LANG-10-304C | TODO | SCANNER-ANALYZERS-LANG-10-304B | Fallback heuristics for stripped binaries with deterministic `bin:{sha256}` labeling and quiet provenance. | Heuristic labels clearly separated; tests ensure no false “observed” provenance; documentation updated. |
|
| 3 | SCANNER-ANALYZERS-LANG-10-304C | TODO | SCANNER-ANALYZERS-LANG-10-304B | Fallback heuristics for stripped binaries with deterministic `bin:{sha256}` labeling and quiet provenance. | Heuristic labels clearly separated; tests ensure no false “observed” provenance; documentation updated. |
|
||||||
| 4 | SCANNER-ANALYZERS-LANG-10-307G | TODO | SCANNER-ANALYZERS-LANG-10-304C | Wire shared helpers (license mapping, usage flags) and ensure concurrency-safe buffer reuse. | Analyzer reuses shared infrastructure; concurrency tests with parallel scans pass; no data races. |
|
| 4 | SCANNER-ANALYZERS-LANG-10-307G | TODO | SCANNER-ANALYZERS-LANG-10-304C | Wire shared helpers (license mapping, usage flags) and ensure concurrency-safe buffer reuse. | Analyzer reuses shared infrastructure; concurrency tests with parallel scans pass; no data races. |
|
||||||
| 5 | SCANNER-ANALYZERS-LANG-10-308G | TODO | SCANNER-ANALYZERS-LANG-10-307G | Determinism fixtures + benchmark harness (Vs competitor). | Fixtures under `Fixtures/lang/go/`; CI determinism check; benchmark runs showing ≥20 % speed advantage. |
|
| 5 | SCANNER-ANALYZERS-LANG-10-308G | TODO | SCANNER-ANALYZERS-LANG-10-307G | Determinism fixtures + benchmark harness (Vs competitor). | Fixtures under `Fixtures/lang/go/`; CI determinism check; benchmark runs showing ≥20 % speed advantage. |
|
||||||
|
|||||||
23
src/StellaOps.Scanner.Analyzers.Lang.Go/manifest.json
Normal file
23
src/StellaOps.Scanner.Analyzers.Lang.Go/manifest.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"schemaVersion": "1.0",
|
||||||
|
"id": "stellaops.analyzer.lang.go",
|
||||||
|
"displayName": "StellaOps Go Analyzer (preview)",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"requiresRestart": true,
|
||||||
|
"entryPoint": {
|
||||||
|
"type": "dotnet",
|
||||||
|
"assembly": "StellaOps.Scanner.Analyzers.Lang.Go.dll",
|
||||||
|
"typeName": "StellaOps.Scanner.Analyzers.Lang.Go.GoAnalyzerPlugin"
|
||||||
|
},
|
||||||
|
"capabilities": [
|
||||||
|
"language-analyzer",
|
||||||
|
"golang",
|
||||||
|
"go"
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"org.stellaops.analyzer.language": "go",
|
||||||
|
"org.stellaops.analyzer.kind": "language",
|
||||||
|
"org.stellaops.restart.required": "true",
|
||||||
|
"org.stellaops.analyzer.status": "preview"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,35 +1,35 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"analyzerId": "java",
|
"analyzerId": "java",
|
||||||
"componentKey": "purl::pkg:maven/com/example/demo@1.0.0",
|
"componentKey": "purl::pkg:maven/com/example/demo@1.0.0",
|
||||||
"purl": "pkg:maven/com/example/demo@1.0.0",
|
"purl": "pkg:maven/com/example/demo@1.0.0",
|
||||||
"name": "demo",
|
"name": "demo",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"type": "maven",
|
"type": "maven",
|
||||||
"usedByEntrypoint": true,
|
"usedByEntrypoint": true,
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"artifactId": "demo",
|
"artifactId": "demo",
|
||||||
"displayName": "Demo Library",
|
"displayName": "Demo Library",
|
||||||
"groupId": "com.example",
|
"groupId": "com.example",
|
||||||
"jarPath": "libs/demo.jar",
|
"jarPath": "libs/demo.jar",
|
||||||
"manifestTitle": "Demo",
|
"manifestTitle": "Demo",
|
||||||
"manifestVendor": "Example Corp",
|
"manifestVendor": "Example Corp",
|
||||||
"manifestVersion": "1.0.0",
|
"manifestVersion": "1.0.0",
|
||||||
"packaging": "jar"
|
"packaging": "jar"
|
||||||
},
|
},
|
||||||
"evidence": [
|
"evidence": [
|
||||||
{
|
{
|
||||||
"kind": "file",
|
"kind": "file",
|
||||||
"source": "MANIFEST.MF",
|
"source": "MANIFEST.MF",
|
||||||
"locator": "libs/demo.jar!META-INF/MANIFEST.MF",
|
"locator": "libs/demo.jar!META-INF/MANIFEST.MF",
|
||||||
"value": "title=Demo;version=1.0.0;vendor=Example Corp"
|
"value": "title=Demo;version=1.0.0;vendor=Example Corp"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "file",
|
"kind": "file",
|
||||||
"source": "pom.properties",
|
"source": "pom.properties",
|
||||||
"locator": "libs/demo.jar!META-INF/maven/com.example/demo/pom.properties",
|
"locator": "libs/demo.jar!META-INF/maven/com.example/demo/pom.properties",
|
||||||
"sha256": "c20f36aa1b9d89d28cf9ed131519ffd6287a4dac0c7cb926130496f3f8157bf1"
|
"sha256": "c20f36aa1b9d89d28cf9ed131519ffd6287a4dac0c7cb926130496f3f8157bf1"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -1,33 +1,33 @@
|
|||||||
using StellaOps.Scanner.Analyzers.Lang.Java;
|
using StellaOps.Scanner.Analyzers.Lang.Java;
|
||||||
using StellaOps.Scanner.Analyzers.Lang.Tests.Harness;
|
using StellaOps.Scanner.Analyzers.Lang.Tests.Harness;
|
||||||
using StellaOps.Scanner.Analyzers.Lang.Tests.TestUtilities;
|
using StellaOps.Scanner.Analyzers.Lang.Tests.TestUtilities;
|
||||||
|
|
||||||
namespace StellaOps.Scanner.Analyzers.Lang.Tests.Java;
|
namespace StellaOps.Scanner.Analyzers.Lang.Java.Tests;
|
||||||
|
|
||||||
public sealed class JavaLanguageAnalyzerTests
|
public sealed class JavaLanguageAnalyzerTests
|
||||||
{
|
{
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ExtractsMavenArtifactFromJarAsync()
|
public async Task ExtractsMavenArtifactFromJarAsync()
|
||||||
{
|
{
|
||||||
var cancellationToken = TestContext.Current.CancellationToken;
|
var cancellationToken = TestContext.Current.CancellationToken;
|
||||||
var root = TestPaths.CreateTemporaryDirectory();
|
var root = TestPaths.CreateTemporaryDirectory();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var jarPath = JavaFixtureBuilder.CreateSampleJar(root);
|
var jarPath = JavaFixtureBuilder.CreateSampleJar(root);
|
||||||
var usageHints = new LanguageUsageHints(new[] { jarPath });
|
var usageHints = new LanguageUsageHints(new[] { jarPath });
|
||||||
var analyzers = new ILanguageAnalyzer[] { new JavaLanguageAnalyzer() };
|
var analyzers = new ILanguageAnalyzer[] { new JavaLanguageAnalyzer() };
|
||||||
var goldenPath = TestPaths.ResolveFixture("java", "basic", "expected.json");
|
var goldenPath = TestPaths.ResolveFixture("java", "basic", "expected.json");
|
||||||
|
|
||||||
await LanguageAnalyzerTestHarness.AssertDeterministicAsync(
|
await LanguageAnalyzerTestHarness.AssertDeterministicAsync(
|
||||||
fixturePath: root,
|
fixturePath: root,
|
||||||
goldenPath: goldenPath,
|
goldenPath: goldenPath,
|
||||||
analyzers: analyzers,
|
analyzers: analyzers,
|
||||||
cancellationToken: cancellationToken,
|
cancellationToken: cancellationToken,
|
||||||
usageHints: usageHints);
|
usageHints: usageHints);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
TestPaths.SafeDelete(root);
|
TestPaths.SafeDelete(root);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<LangVersion>preview</LangVersion>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Remove="Microsoft.NET.Test.Sdk" />
|
||||||
|
<PackageReference Remove="xunit" />
|
||||||
|
<PackageReference Remove="xunit.runner.visualstudio" />
|
||||||
|
<PackageReference Remove="Microsoft.AspNetCore.Mvc.Testing" />
|
||||||
|
<PackageReference Remove="Mongo2Go" />
|
||||||
|
<PackageReference Remove="coverlet.collector" />
|
||||||
|
<PackageReference Remove="Microsoft.Extensions.TimeProvider.Testing" />
|
||||||
|
<ProjectReference Remove="..\StellaOps.Concelier.Testing\StellaOps.Concelier.Testing.csproj" />
|
||||||
|
<Compile Remove="$(MSBuildThisFileDirectory)..\StellaOps.Concelier.Tests.Shared\AssemblyInfo.cs" />
|
||||||
|
<Compile Remove="$(MSBuildThisFileDirectory)..\StellaOps.Concelier.Tests.Shared\MongoFixtureCollection.cs" />
|
||||||
|
<Using Remove="StellaOps.Concelier.Testing" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||||
|
<PackageReference Include="xunit.v3" Version="3.0.0" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.3" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\StellaOps.Scanner.Analyzers.Lang.Tests\StellaOps.Scanner.Analyzers.Lang.Tests.csproj" />
|
||||||
|
<ProjectReference Include="..\StellaOps.Scanner.Analyzers.Lang\StellaOps.Scanner.Analyzers.Lang.csproj" />
|
||||||
|
<ProjectReference Include="..\StellaOps.Scanner.Analyzers.Lang.Java\StellaOps.Scanner.Analyzers.Lang.Java.csproj" />
|
||||||
|
<ProjectReference Include="..\StellaOps.Scanner.Core\StellaOps.Scanner.Core.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Include="Fixtures\**\*" CopyToOutputDirectory="PreserveNewest" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Using Include="Xunit" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
@@ -1,134 +1,134 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"analyzerId": "node",
|
"analyzerId": "node",
|
||||||
"componentKey": "purl::pkg:npm/left-pad@1.3.0",
|
"componentKey": "purl::pkg:npm/left-pad@1.3.0",
|
||||||
"purl": "pkg:npm/left-pad@1.3.0",
|
"purl": "pkg:npm/left-pad@1.3.0",
|
||||||
"name": "left-pad",
|
"name": "left-pad",
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"type": "npm",
|
"type": "npm",
|
||||||
"usedByEntrypoint": false,
|
"usedByEntrypoint": false,
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"integrity": "sha512-LEFTPAD",
|
"integrity": "sha512-LEFTPAD",
|
||||||
"path": "packages/app/node_modules/left-pad",
|
"path": "packages/app/node_modules/left-pad",
|
||||||
"resolved": "https://registry.example/left-pad-1.3.0.tgz"
|
"resolved": "https://registry.example/left-pad-1.3.0.tgz"
|
||||||
},
|
},
|
||||||
"evidence": [
|
"evidence": [
|
||||||
{
|
{
|
||||||
"kind": "file",
|
"kind": "file",
|
||||||
"source": "package.json",
|
"source": "package.json",
|
||||||
"locator": "packages/app/node_modules/left-pad/package.json"
|
"locator": "packages/app/node_modules/left-pad/package.json"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"analyzerId": "node",
|
"analyzerId": "node",
|
||||||
"componentKey": "purl::pkg:npm/lib@2.0.1",
|
"componentKey": "purl::pkg:npm/lib@2.0.1",
|
||||||
"purl": "pkg:npm/lib@2.0.1",
|
"purl": "pkg:npm/lib@2.0.1",
|
||||||
"name": "lib",
|
"name": "lib",
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"type": "npm",
|
"type": "npm",
|
||||||
"usedByEntrypoint": false,
|
"usedByEntrypoint": false,
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"integrity": "sha512-LIB",
|
"integrity": "sha512-LIB",
|
||||||
"path": "packages/lib",
|
"path": "packages/lib",
|
||||||
"resolved": "https://registry.example/lib-2.0.1.tgz",
|
"resolved": "https://registry.example/lib-2.0.1.tgz",
|
||||||
"workspaceLink": "packages/app/node_modules/lib",
|
"workspaceLink": "packages/app/node_modules/lib",
|
||||||
"workspaceMember": "true",
|
"workspaceMember": "true",
|
||||||
"workspaceRoot": "packages/lib"
|
"workspaceRoot": "packages/lib"
|
||||||
},
|
},
|
||||||
"evidence": [
|
"evidence": [
|
||||||
{
|
{
|
||||||
"kind": "file",
|
"kind": "file",
|
||||||
"source": "package.json",
|
"source": "package.json",
|
||||||
"locator": "packages/app/node_modules/lib/package.json"
|
"locator": "packages/app/node_modules/lib/package.json"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "file",
|
"kind": "file",
|
||||||
"source": "package.json",
|
"source": "package.json",
|
||||||
"locator": "packages/lib/package.json"
|
"locator": "packages/lib/package.json"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"analyzerId": "node",
|
"analyzerId": "node",
|
||||||
"componentKey": "purl::pkg:npm/root-workspace@1.0.0",
|
"componentKey": "purl::pkg:npm/root-workspace@1.0.0",
|
||||||
"purl": "pkg:npm/root-workspace@1.0.0",
|
"purl": "pkg:npm/root-workspace@1.0.0",
|
||||||
"name": "root-workspace",
|
"name": "root-workspace",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"type": "npm",
|
"type": "npm",
|
||||||
"usedByEntrypoint": false,
|
"usedByEntrypoint": false,
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"path": ".",
|
"path": ".",
|
||||||
"private": "true"
|
"private": "true"
|
||||||
},
|
},
|
||||||
"evidence": [
|
"evidence": [
|
||||||
{
|
{
|
||||||
"kind": "file",
|
"kind": "file",
|
||||||
"source": "package.json",
|
"source": "package.json",
|
||||||
"locator": "package.json"
|
"locator": "package.json"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"analyzerId": "node",
|
"analyzerId": "node",
|
||||||
"componentKey": "purl::pkg:npm/shared@3.1.4",
|
"componentKey": "purl::pkg:npm/shared@3.1.4",
|
||||||
"purl": "pkg:npm/shared@3.1.4",
|
"purl": "pkg:npm/shared@3.1.4",
|
||||||
"name": "shared",
|
"name": "shared",
|
||||||
"version": "3.1.4",
|
"version": "3.1.4",
|
||||||
"type": "npm",
|
"type": "npm",
|
||||||
"usedByEntrypoint": false,
|
"usedByEntrypoint": false,
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"integrity": "sha512-SHARED",
|
"integrity": "sha512-SHARED",
|
||||||
"path": "packages/shared",
|
"path": "packages/shared",
|
||||||
"resolved": "https://registry.example/shared-3.1.4.tgz",
|
"resolved": "https://registry.example/shared-3.1.4.tgz",
|
||||||
"workspaceLink": "packages/app/node_modules/shared",
|
"workspaceLink": "packages/app/node_modules/shared",
|
||||||
"workspaceMember": "true",
|
"workspaceMember": "true",
|
||||||
"workspaceRoot": "packages/shared",
|
"workspaceRoot": "packages/shared",
|
||||||
"workspaceTargets": "packages/lib"
|
"workspaceTargets": "packages/lib"
|
||||||
},
|
},
|
||||||
"evidence": [
|
"evidence": [
|
||||||
{
|
{
|
||||||
"kind": "file",
|
"kind": "file",
|
||||||
"source": "package.json",
|
"source": "package.json",
|
||||||
"locator": "packages/app/node_modules/shared/package.json"
|
"locator": "packages/app/node_modules/shared/package.json"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "file",
|
"kind": "file",
|
||||||
"source": "package.json",
|
"source": "package.json",
|
||||||
"locator": "packages/shared/package.json"
|
"locator": "packages/shared/package.json"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"analyzerId": "node",
|
"analyzerId": "node",
|
||||||
"componentKey": "purl::pkg:npm/workspace-app@1.0.0",
|
"componentKey": "purl::pkg:npm/workspace-app@1.0.0",
|
||||||
"purl": "pkg:npm/workspace-app@1.0.0",
|
"purl": "pkg:npm/workspace-app@1.0.0",
|
||||||
"name": "workspace-app",
|
"name": "workspace-app",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"type": "npm",
|
"type": "npm",
|
||||||
"usedByEntrypoint": false,
|
"usedByEntrypoint": false,
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"installScripts": "true",
|
"installScripts": "true",
|
||||||
"path": "packages/app",
|
"path": "packages/app",
|
||||||
"policyHint.installLifecycle": "postinstall",
|
"policyHint.installLifecycle": "postinstall",
|
||||||
"script.postinstall": "node scripts/setup.js",
|
"script.postinstall": "node scripts/setup.js",
|
||||||
"workspaceMember": "true",
|
"workspaceMember": "true",
|
||||||
"workspaceRoot": "packages/app",
|
"workspaceRoot": "packages/app",
|
||||||
"workspaceTargets": "packages/lib;packages/shared"
|
"workspaceTargets": "packages/lib;packages/shared"
|
||||||
},
|
},
|
||||||
"evidence": [
|
"evidence": [
|
||||||
{
|
{
|
||||||
"kind": "file",
|
"kind": "file",
|
||||||
"source": "package.json",
|
"source": "package.json",
|
||||||
"locator": "packages/app/package.json"
|
"locator": "packages/app/package.json"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "metadata",
|
"kind": "metadata",
|
||||||
"source": "package.json:scripts",
|
"source": "package.json:scripts",
|
||||||
"locator": "packages/app/package.json#scripts.postinstall",
|
"locator": "packages/app/package.json#scripts.postinstall",
|
||||||
"value": "node scripts/setup.js",
|
"value": "node scripts/setup.js",
|
||||||
"sha256": "f9ae4e4c9313857d1acc31947cee9984232cbefe93c8a56c718804744992728a"
|
"sha256": "f9ae4e4c9313857d1acc31947cee9984232cbefe93c8a56c718804744992728a"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -1,49 +1,49 @@
|
|||||||
{
|
{
|
||||||
"name": "root-workspace",
|
"name": "root-workspace",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "root-workspace",
|
"name": "root-workspace",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"packages/app": {
|
"packages/app": {
|
||||||
"name": "workspace-app",
|
"name": "workspace-app",
|
||||||
"version": "1.0.0"
|
"version": "1.0.0"
|
||||||
},
|
},
|
||||||
"packages/lib": {
|
"packages/lib": {
|
||||||
"name": "lib",
|
"name": "lib",
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.example/lib-2.0.1.tgz",
|
"resolved": "https://registry.example/lib-2.0.1.tgz",
|
||||||
"integrity": "sha512-LIB"
|
"integrity": "sha512-LIB"
|
||||||
},
|
},
|
||||||
"packages/shared": {
|
"packages/shared": {
|
||||||
"name": "shared",
|
"name": "shared",
|
||||||
"version": "3.1.4",
|
"version": "3.1.4",
|
||||||
"resolved": "https://registry.example/shared-3.1.4.tgz",
|
"resolved": "https://registry.example/shared-3.1.4.tgz",
|
||||||
"integrity": "sha512-SHARED"
|
"integrity": "sha512-SHARED"
|
||||||
},
|
},
|
||||||
"packages/app/node_modules/lib": {
|
"packages/app/node_modules/lib": {
|
||||||
"name": "lib",
|
"name": "lib",
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.example/lib-2.0.1.tgz",
|
"resolved": "https://registry.example/lib-2.0.1.tgz",
|
||||||
"integrity": "sha512-LIB"
|
"integrity": "sha512-LIB"
|
||||||
},
|
},
|
||||||
"packages/app/node_modules/shared": {
|
"packages/app/node_modules/shared": {
|
||||||
"name": "shared",
|
"name": "shared",
|
||||||
"version": "3.1.4",
|
"version": "3.1.4",
|
||||||
"resolved": "https://registry.example/shared-3.1.4.tgz",
|
"resolved": "https://registry.example/shared-3.1.4.tgz",
|
||||||
"integrity": "sha512-SHARED"
|
"integrity": "sha512-SHARED"
|
||||||
},
|
},
|
||||||
"packages/app/node_modules/left-pad": {
|
"packages/app/node_modules/left-pad": {
|
||||||
"name": "left-pad",
|
"name": "left-pad",
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.example/left-pad-1.3.0.tgz",
|
"resolved": "https://registry.example/left-pad-1.3.0.tgz",
|
||||||
"integrity": "sha512-LEFTPAD"
|
"integrity": "sha512-LEFTPAD"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"name": "root-workspace",
|
"name": "root-workspace",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/app",
|
"packages/app",
|
||||||
"packages/lib",
|
"packages/lib",
|
||||||
"packages/shared"
|
"packages/shared"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "left-pad",
|
"name": "left-pad",
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"main": "index.js"
|
"main": "index.js"
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "lib",
|
"name": "lib",
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"main": "index.js"
|
"main": "index.js"
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "shared",
|
"name": "shared",
|
||||||
"version": "3.1.4",
|
"version": "3.1.4",
|
||||||
"main": "index.js"
|
"main": "index.js"
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "workspace-app",
|
"name": "workspace-app",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"lib": "workspace:../lib",
|
"lib": "workspace:../lib",
|
||||||
"shared": "workspace:../shared"
|
"shared": "workspace:../shared"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"postinstall": "node scripts/setup.js"
|
"postinstall": "node scripts/setup.js"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1 +1 @@
|
|||||||
console.log('setup');
|
console.log('setup');
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "lib",
|
"name": "lib",
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"left-pad": "1.3.0"
|
"left-pad": "1.3.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "shared",
|
"name": "shared",
|
||||||
"version": "3.1.4",
|
"version": "3.1.4",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"lib": "workspace:../lib"
|
"lib": "workspace:../lib"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,27 +1,27 @@
|
|||||||
using StellaOps.Scanner.Analyzers.Lang.Node;
|
using StellaOps.Scanner.Analyzers.Lang.Node;
|
||||||
using StellaOps.Scanner.Analyzers.Lang.Tests.Harness;
|
using StellaOps.Scanner.Analyzers.Lang.Tests.Harness;
|
||||||
using StellaOps.Scanner.Analyzers.Lang.Tests.TestUtilities;
|
using StellaOps.Scanner.Analyzers.Lang.Tests.TestUtilities;
|
||||||
|
|
||||||
namespace StellaOps.Scanner.Analyzers.Lang.Tests.Node;
|
namespace StellaOps.Scanner.Analyzers.Lang.Node.Tests;
|
||||||
|
|
||||||
public sealed class NodeLanguageAnalyzerTests
|
public sealed class NodeLanguageAnalyzerTests
|
||||||
{
|
{
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task WorkspaceFixtureProducesDeterministicOutputAsync()
|
public async Task WorkspaceFixtureProducesDeterministicOutputAsync()
|
||||||
{
|
{
|
||||||
var cancellationToken = TestContext.Current.CancellationToken;
|
var cancellationToken = TestContext.Current.CancellationToken;
|
||||||
var fixturePath = TestPaths.ResolveFixture("lang", "node", "workspaces");
|
var fixturePath = TestPaths.ResolveFixture("lang", "node", "workspaces");
|
||||||
var goldenPath = Path.Combine(fixturePath, "expected.json");
|
var goldenPath = Path.Combine(fixturePath, "expected.json");
|
||||||
|
|
||||||
var analyzers = new ILanguageAnalyzer[]
|
var analyzers = new ILanguageAnalyzer[]
|
||||||
{
|
{
|
||||||
new NodeLanguageAnalyzer()
|
new NodeLanguageAnalyzer()
|
||||||
};
|
};
|
||||||
|
|
||||||
await LanguageAnalyzerTestHarness.AssertDeterministicAsync(
|
await LanguageAnalyzerTestHarness.AssertDeterministicAsync(
|
||||||
fixturePath,
|
fixturePath,
|
||||||
goldenPath,
|
goldenPath,
|
||||||
analyzers,
|
analyzers,
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<LangVersion>preview</LangVersion>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Remove="Microsoft.NET.Test.Sdk" />
|
||||||
|
<PackageReference Remove="xunit" />
|
||||||
|
<PackageReference Remove="xunit.runner.visualstudio" />
|
||||||
|
<PackageReference Remove="Microsoft.AspNetCore.Mvc.Testing" />
|
||||||
|
<PackageReference Remove="Mongo2Go" />
|
||||||
|
<PackageReference Remove="coverlet.collector" />
|
||||||
|
<PackageReference Remove="Microsoft.Extensions.TimeProvider.Testing" />
|
||||||
|
<ProjectReference Remove="..\StellaOps.Concelier.Testing\StellaOps.Concelier.Testing.csproj" />
|
||||||
|
<Compile Remove="$(MSBuildThisFileDirectory)..\StellaOps.Concelier.Tests.Shared\AssemblyInfo.cs" />
|
||||||
|
<Compile Remove="$(MSBuildThisFileDirectory)..\StellaOps.Concelier.Tests.Shared\MongoFixtureCollection.cs" />
|
||||||
|
<Using Remove="StellaOps.Concelier.Testing" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||||
|
<PackageReference Include="xunit.v3" Version="3.0.0" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.3" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\StellaOps.Scanner.Analyzers.Lang.Tests\StellaOps.Scanner.Analyzers.Lang.Tests.csproj" />
|
||||||
|
<ProjectReference Include="..\StellaOps.Scanner.Analyzers.Lang\StellaOps.Scanner.Analyzers.Lang.csproj" />
|
||||||
|
<ProjectReference Include="..\StellaOps.Scanner.Analyzers.Lang.Node\StellaOps.Scanner.Analyzers.Lang.Node.csproj" />
|
||||||
|
<ProjectReference Include="..\StellaOps.Scanner.Core\StellaOps.Scanner.Core.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Include="Fixtures\**\*" CopyToOutputDirectory="PreserveNewest" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Using Include="Xunit" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
using System;
|
||||||
|
using StellaOps.Scanner.Analyzers.Lang;
|
||||||
|
using StellaOps.Scanner.Analyzers.Lang.Plugin;
|
||||||
|
|
||||||
|
namespace StellaOps.Scanner.Analyzers.Lang.Node;
|
||||||
|
|
||||||
|
public sealed class NodeAnalyzerPlugin : ILanguageAnalyzerPlugin
|
||||||
|
{
|
||||||
|
public string Name => "StellaOps.Scanner.Analyzers.Lang.Node";
|
||||||
|
|
||||||
|
public bool IsAvailable(IServiceProvider services) => services is not null;
|
||||||
|
|
||||||
|
public ILanguageAnalyzer CreateAnalyzer(IServiceProvider services)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(services);
|
||||||
|
return new NodeLanguageAnalyzer();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
namespace StellaOps.Scanner.Analyzers.Lang.Node;
|
|
||||||
|
|
||||||
internal static class Placeholder
|
|
||||||
{
|
|
||||||
// Analyzer implementation will be added during Sprint LA1.
|
|
||||||
}
|
|
||||||
@@ -5,6 +5,6 @@
|
|||||||
| 1 | SCANNER-ANALYZERS-LANG-10-302A | DONE (2025-10-19) | SCANNER-ANALYZERS-LANG-10-307 | Build deterministic module graph walker covering npm, Yarn, and PNPM; capture package.json provenance and integrity metadata. | Walker indexes >100 k modules in <1.5 s (hot cache); golden fixtures verify deterministic ordering and path normalization. |
|
| 1 | SCANNER-ANALYZERS-LANG-10-302A | DONE (2025-10-19) | SCANNER-ANALYZERS-LANG-10-307 | Build deterministic module graph walker covering npm, Yarn, and PNPM; capture package.json provenance and integrity metadata. | Walker indexes >100 k modules in <1.5 s (hot cache); golden fixtures verify deterministic ordering and path normalization. |
|
||||||
| 2 | SCANNER-ANALYZERS-LANG-10-302B | DONE (2025-10-19) | SCANNER-ANALYZERS-LANG-10-302A | Resolve workspaces/symlinks and attribute components to originating package with usage hints; guard against directory traversal. | Workspace attribution accurate on multi-workspace fixture; symlink resolver proves canonical path; security tests ensure no traversal. |
|
| 2 | SCANNER-ANALYZERS-LANG-10-302B | DONE (2025-10-19) | SCANNER-ANALYZERS-LANG-10-302A | Resolve workspaces/symlinks and attribute components to originating package with usage hints; guard against directory traversal. | Workspace attribution accurate on multi-workspace fixture; symlink resolver proves canonical path; security tests ensure no traversal. |
|
||||||
| 3 | SCANNER-ANALYZERS-LANG-10-302C | DONE (2025-10-19) | SCANNER-ANALYZERS-LANG-10-302B | Surface script metadata (postinstall/preinstall) and policy hints; emit telemetry counters and evidence records. | Analyzer output includes script metadata + evidence; metrics `scanner_analyzer_node_scripts_total` recorded; policy hints documented. |
|
| 3 | SCANNER-ANALYZERS-LANG-10-302C | DONE (2025-10-19) | SCANNER-ANALYZERS-LANG-10-302B | Surface script metadata (postinstall/preinstall) and policy hints; emit telemetry counters and evidence records. | Analyzer output includes script metadata + evidence; metrics `scanner_analyzer_node_scripts_total` recorded; policy hints documented. |
|
||||||
| 4 | SCANNER-ANALYZERS-LANG-10-307N | TODO | SCANNER-ANALYZERS-LANG-10-302C | Integrate shared helpers for license/licence evidence, canonical JSON serialization, and usage flag propagation. | Reuse shared helpers without duplication; unit tests confirm stable metadata merge; no analyzer-specific serializer drift. |
|
| 4 | SCANNER-ANALYZERS-LANG-10-307N | DONE (2025-10-21) | SCANNER-ANALYZERS-LANG-10-302C | Integrate shared helpers for license/licence evidence, canonical JSON serialization, and usage flag propagation. | Reuse shared helpers without duplication; unit tests confirm stable metadata merge; no analyzer-specific serializer drift. |
|
||||||
| 5 | SCANNER-ANALYZERS-LANG-10-308N | TODO | SCANNER-ANALYZERS-LANG-10-307N | Author determinism harness + fixtures for Node analyzer; add benchmark suite. | Fixtures committed under `Fixtures/lang/node/`; determinism CI job compares JSON snapshots; benchmark CSV published. |
|
| 5 | SCANNER-ANALYZERS-LANG-10-308N | DONE (2025-10-21) | SCANNER-ANALYZERS-LANG-10-307N | Author determinism harness + fixtures for Node analyzer; add benchmark suite. | Fixtures committed under `Fixtures/lang/node/`; determinism CI job compares JSON snapshots; benchmark CSV published. |
|
||||||
| 6 | SCANNER-ANALYZERS-LANG-10-309N | TODO | SCANNER-ANALYZERS-LANG-10-308N | Package Node analyzer as restart-time plug-in (manifest, DI registration, Offline Kit notes). | Manifest copied to `plugins/scanner/analyzers/lang/`; Worker loads analyzer after restart; Offline Kit docs updated. |
|
| 6 | SCANNER-ANALYZERS-LANG-10-309N | DONE (2025-10-21) | SCANNER-ANALYZERS-LANG-10-308N | Package Node analyzer as restart-time plug-in (manifest, DI registration, Offline Kit notes). | Manifest copied to `plugins/scanner/analyzers/lang/`; Worker loads analyzer after restart; Offline Kit docs updated. |
|
||||||
|
|||||||
22
src/StellaOps.Scanner.Analyzers.Lang.Node/manifest.json
Normal file
22
src/StellaOps.Scanner.Analyzers.Lang.Node/manifest.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"schemaVersion": "1.0",
|
||||||
|
"id": "stellaops.analyzer.lang.node",
|
||||||
|
"displayName": "StellaOps Node.js Analyzer",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"requiresRestart": true,
|
||||||
|
"entryPoint": {
|
||||||
|
"type": "dotnet",
|
||||||
|
"assembly": "StellaOps.Scanner.Analyzers.Lang.Node.dll",
|
||||||
|
"typeName": "StellaOps.Scanner.Analyzers.Lang.Node.NodeAnalyzerPlugin"
|
||||||
|
},
|
||||||
|
"capabilities": [
|
||||||
|
"language-analyzer",
|
||||||
|
"node",
|
||||||
|
"npm"
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"org.stellaops.analyzer.language": "node",
|
||||||
|
"org.stellaops.analyzer.kind": "language",
|
||||||
|
"org.stellaops.restart.required": "true"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"analyzerId": "python",
|
||||||
|
"componentKey": "purl::pkg:pypi/simple@1.0.0",
|
||||||
|
"purl": "pkg:pypi/simple@1.0.0",
|
||||||
|
"name": "simple",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"type": "pypi",
|
||||||
|
"usedByEntrypoint": true,
|
||||||
|
"metadata": {
|
||||||
|
"author": "Example Dev",
|
||||||
|
"authorEmail": "dev@example.com",
|
||||||
|
"classifiers": "Programming Language :: Python :: 3;License :: OSI Approved :: Apache Software License",
|
||||||
|
"distInfoPath": "lib/python3.11/site-packages/simple-1.0.0.dist-info",
|
||||||
|
"editable": "true",
|
||||||
|
"entryPoints.console_scripts": "simple-tool=simple.core:main",
|
||||||
|
"homePage": "https://example.com/simple",
|
||||||
|
"installer": "pip",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"name": "simple",
|
||||||
|
"projectUrl": "Source, https://example.com/simple/src",
|
||||||
|
"record.hashMismatches": "0",
|
||||||
|
"record.hashedEntries": "9",
|
||||||
|
"record.ioErrors": "0",
|
||||||
|
"record.missingFiles": "0",
|
||||||
|
"record.totalEntries": "10",
|
||||||
|
"requiresDist": "requests (\u003E=2.0)",
|
||||||
|
"requiresPython": "\u003E=3.9",
|
||||||
|
"sourceCommit": "abc123def",
|
||||||
|
"sourceSubdirectory": "src/simple",
|
||||||
|
"sourceUrl": "https://example.com/simple-1.0.0.tar.gz",
|
||||||
|
"sourceVcs": "git",
|
||||||
|
"summary": "Simple fixture package",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"wheel.generator": "pip 24.0",
|
||||||
|
"wheel.rootIsPurelib": "true",
|
||||||
|
"wheel.tags": "py3-none-any",
|
||||||
|
"wheel.version": "1.0"
|
||||||
|
},
|
||||||
|
"evidence": [
|
||||||
|
{
|
||||||
|
"kind": "file",
|
||||||
|
"source": "METADATA",
|
||||||
|
"locator": "lib/python3.11/site-packages/simple-1.0.0.dist-info/METADATA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "file",
|
||||||
|
"source": "WHEEL",
|
||||||
|
"locator": "lib/python3.11/site-packages/simple-1.0.0.dist-info/WHEEL"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "file",
|
||||||
|
"source": "entry_points.txt",
|
||||||
|
"locator": "lib/python3.11/site-packages/simple-1.0.0.dist-info/entry_points.txt"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "metadata",
|
||||||
|
"source": "direct_url.json",
|
||||||
|
"locator": "lib/python3.11/site-packages/simple-1.0.0.dist-info/direct_url.json",
|
||||||
|
"value": "https://example.com/simple-1.0.0.tar.gz"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
pip
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
Metadata-Version: 2.1
|
||||||
|
Name: simple
|
||||||
|
Version: 1.0.0
|
||||||
|
Summary: Simple fixture package
|
||||||
|
Home-page: https://example.com/simple
|
||||||
|
Author: Example Dev
|
||||||
|
Author-email: dev@example.com
|
||||||
|
License: Apache-2.0
|
||||||
|
Project-URL: Source, https://example.com/simple/src
|
||||||
|
Requires-Python: >=3.9
|
||||||
|
Requires-Dist: requests (>=2.0)
|
||||||
|
Classifier: Programming Language :: Python :: 3
|
||||||
|
Classifier: License :: OSI Approved :: Apache Software License
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
simple/__init__.py,sha256=03NWG/tm5eky+tnGlynp/vcyjtR944EtQMKtwrutl/U=,79
|
||||||
|
simple/__main__.py,sha256=7pHsIZX9uNTyp1e1AkmZ1vXZxw/dYB1TbR1rYDJca6c=,62
|
||||||
|
simple/core.py,sha256=8HaF+vPTo2roSP7kivqePnjG+d/WqotH269Qey/BM+s=,67
|
||||||
|
../../../bin/simple-tool,sha256=E7eVnffg2E4646m1Ml/5ixyROcpc24GJvy03sEkg6DA=,91
|
||||||
|
simple-1.0.0.dist-info/METADATA,sha256=Da/AG+nYa85WfbUSNmmRjpTeEEM8Kinf6Z197xb8X2o=,408
|
||||||
|
simple-1.0.0.dist-info/WHEEL,sha256=m8MHT7vQnqC5W8H/y4uJdEpx9ijH0jJGpuoaxRbcsQg=,79
|
||||||
|
simple-1.0.0.dist-info/entry_points.txt,sha256=A2WCkblioa0YbdUDurLzv+sIbx7TRSJ9zfBLYMYwpBQ=,49
|
||||||
|
simple-1.0.0.dist-info/INSTALLER,sha256=zuuue4knoyJ+UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg=,4
|
||||||
|
simple-1.0.0.dist-info/direct_url.json,sha256=EXd4Xj5iohEIqiF7mlR7sCLGhqXiU1/LuOPjijstKCU=,199
|
||||||
|
simple-1.0.0.dist-info/RECORD,,
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
Wheel-Version: 1.0
|
||||||
|
Generator: pip 24.0
|
||||||
|
Root-Is-Purelib: true
|
||||||
|
Tag: py3-none-any
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"url": "https://example.com/simple-1.0.0.tar.gz",
|
||||||
|
"dir_info": {
|
||||||
|
"editable": true,
|
||||||
|
"subdirectory": "src/simple"
|
||||||
|
},
|
||||||
|
"vcs_info": {
|
||||||
|
"vcs": "git",
|
||||||
|
"commit_id": "abc123def"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
[console_scripts]
|
||||||
|
simple-tool = simple.core:main
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
__all__ = ["main"]
|
||||||
|
__version__ = "1.0.0"
|
||||||
|
|
||||||
|
from .core import main # noqa: F401
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
from .core import main
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import sys
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
print("simple core", sys.argv)
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
using StellaOps.Scanner.Analyzers.Lang.Python;
|
||||||
|
using StellaOps.Scanner.Analyzers.Lang.Tests.Harness;
|
||||||
|
using StellaOps.Scanner.Analyzers.Lang.Tests.TestUtilities;
|
||||||
|
|
||||||
|
namespace StellaOps.Scanner.Analyzers.Lang.Python.Tests;
|
||||||
|
|
||||||
|
public sealed class PythonLanguageAnalyzerTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task SimpleVenvFixtureProducesDeterministicOutputAsync()
|
||||||
|
{
|
||||||
|
var cancellationToken = TestContext.Current.CancellationToken;
|
||||||
|
var fixturePath = TestPaths.ResolveFixture("lang", "python", "simple-venv");
|
||||||
|
var goldenPath = Path.Combine(fixturePath, "expected.json");
|
||||||
|
|
||||||
|
var usageHints = new LanguageUsageHints(new[]
|
||||||
|
{
|
||||||
|
Path.Combine(fixturePath, "bin", "simple-tool")
|
||||||
|
});
|
||||||
|
|
||||||
|
var analyzers = new ILanguageAnalyzer[]
|
||||||
|
{
|
||||||
|
new PythonLanguageAnalyzer()
|
||||||
|
};
|
||||||
|
|
||||||
|
await LanguageAnalyzerTestHarness.AssertDeterministicAsync(
|
||||||
|
fixturePath,
|
||||||
|
goldenPath,
|
||||||
|
analyzers,
|
||||||
|
cancellationToken,
|
||||||
|
usageHints);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<LangVersion>preview</LangVersion>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Remove="Microsoft.NET.Test.Sdk" />
|
||||||
|
<PackageReference Remove="xunit" />
|
||||||
|
<PackageReference Remove="xunit.runner.visualstudio" />
|
||||||
|
<PackageReference Remove="Microsoft.AspNetCore.Mvc.Testing" />
|
||||||
|
<PackageReference Remove="Mongo2Go" />
|
||||||
|
<PackageReference Remove="coverlet.collector" />
|
||||||
|
<PackageReference Remove="Microsoft.Extensions.TimeProvider.Testing" />
|
||||||
|
<ProjectReference Remove="..\StellaOps.Concelier.Testing\StellaOps.Concelier.Testing.csproj" />
|
||||||
|
<Compile Remove="$(MSBuildThisFileDirectory)..\StellaOps.Concelier.Tests.Shared\AssemblyInfo.cs" />
|
||||||
|
<Compile Remove="$(MSBuildThisFileDirectory)..\StellaOps.Concelier.Tests.Shared\MongoFixtureCollection.cs" />
|
||||||
|
<Using Remove="StellaOps.Concelier.Testing" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||||
|
<PackageReference Include="xunit.v3" Version="3.0.0" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.3" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\StellaOps.Scanner.Analyzers.Lang.Tests\StellaOps.Scanner.Analyzers.Lang.Tests.csproj" />
|
||||||
|
<ProjectReference Include="..\StellaOps.Scanner.Analyzers.Lang\StellaOps.Scanner.Analyzers.Lang.csproj" />
|
||||||
|
<ProjectReference Include="..\StellaOps.Scanner.Analyzers.Lang.Python\StellaOps.Scanner.Analyzers.Lang.Python.csproj" />
|
||||||
|
<ProjectReference Include="..\StellaOps.Scanner.Core\StellaOps.Scanner.Core.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Include="Fixtures\**\*" CopyToOutputDirectory="PreserveNewest" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Using Include="Xunit" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
global using System;
|
global using System;
|
||||||
global using System.Collections.Generic;
|
global using System.Collections.Generic;
|
||||||
global using System.IO;
|
global using System.IO;
|
||||||
global using System.Threading;
|
global using System.Linq;
|
||||||
global using System.Threading.Tasks;
|
global using System.Threading;
|
||||||
|
global using System.Threading.Tasks;
|
||||||
global using StellaOps.Scanner.Analyzers.Lang;
|
|
||||||
|
global using StellaOps.Scanner.Analyzers.Lang;
|
||||||
|
|||||||
@@ -0,0 +1,989 @@
|
|||||||
|
using System.Buffers;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace StellaOps.Scanner.Analyzers.Lang.Python.Internal;
|
||||||
|
|
||||||
|
internal static class PythonDistributionLoader
|
||||||
|
{
|
||||||
|
|
||||||
|
public static async Task<PythonDistribution?> LoadAsync(LanguageAnalyzerContext context, string distInfoPath, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(distInfoPath) || !Directory.Exists(distInfoPath))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var metadataPath = Path.Combine(distInfoPath, "METADATA");
|
||||||
|
var wheelPath = Path.Combine(distInfoPath, "WHEEL");
|
||||||
|
var entryPointsPath = Path.Combine(distInfoPath, "entry_points.txt");
|
||||||
|
var recordPath = Path.Combine(distInfoPath, "RECORD");
|
||||||
|
var installerPath = Path.Combine(distInfoPath, "INSTALLER");
|
||||||
|
var directUrlPath = Path.Combine(distInfoPath, "direct_url.json");
|
||||||
|
|
||||||
|
var metadataDocument = await PythonMetadataDocument.LoadAsync(metadataPath, cancellationToken).ConfigureAwait(false);
|
||||||
|
var name = metadataDocument.GetFirst("Name") ?? ExtractNameFromDirectory(distInfoPath);
|
||||||
|
var version = metadataDocument.GetFirst("Version") ?? ExtractVersionFromDirectory(distInfoPath);
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(name) || string.IsNullOrWhiteSpace(version))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var trimmedName = name.Trim();
|
||||||
|
var trimmedVersion = version.Trim();
|
||||||
|
var normalizedName = NormalizePackageName(trimmedName);
|
||||||
|
var purl = $"pkg:pypi/{normalizedName}@{trimmedVersion}";
|
||||||
|
|
||||||
|
var metadataEntries = new List<KeyValuePair<string, string?>>();
|
||||||
|
var evidenceEntries = new List<LanguageComponentEvidence>();
|
||||||
|
|
||||||
|
AddFileEvidence(context, metadataPath, "METADATA", evidenceEntries);
|
||||||
|
AddFileEvidence(context, wheelPath, "WHEEL", evidenceEntries);
|
||||||
|
AddFileEvidence(context, entryPointsPath, "entry_points.txt", evidenceEntries);
|
||||||
|
|
||||||
|
AppendMetadata(metadataEntries, "distInfoPath", PythonPathHelper.NormalizeRelative(context, distInfoPath));
|
||||||
|
AppendMetadata(metadataEntries, "name", trimmedName);
|
||||||
|
AppendMetadata(metadataEntries, "version", trimmedVersion);
|
||||||
|
AppendMetadata(metadataEntries, "summary", metadataDocument.GetFirst("Summary"));
|
||||||
|
AppendMetadata(metadataEntries, "license", metadataDocument.GetFirst("License"));
|
||||||
|
AppendMetadata(metadataEntries, "homePage", metadataDocument.GetFirst("Home-page"));
|
||||||
|
AppendMetadata(metadataEntries, "author", metadataDocument.GetFirst("Author"));
|
||||||
|
AppendMetadata(metadataEntries, "authorEmail", metadataDocument.GetFirst("Author-email"));
|
||||||
|
AppendMetadata(metadataEntries, "projectUrl", metadataDocument.GetFirst("Project-URL"));
|
||||||
|
AppendMetadata(metadataEntries, "requiresPython", metadataDocument.GetFirst("Requires-Python"));
|
||||||
|
|
||||||
|
var classifiers = metadataDocument.GetAll("Classifier");
|
||||||
|
if (classifiers.Count > 0)
|
||||||
|
{
|
||||||
|
AppendMetadata(metadataEntries, "classifiers", string.Join(';', classifiers));
|
||||||
|
}
|
||||||
|
|
||||||
|
var requiresDist = metadataDocument.GetAll("Requires-Dist");
|
||||||
|
if (requiresDist.Count > 0)
|
||||||
|
{
|
||||||
|
AppendMetadata(metadataEntries, "requiresDist", string.Join(';', requiresDist));
|
||||||
|
}
|
||||||
|
|
||||||
|
var entryPoints = await PythonEntryPointSet.LoadAsync(entryPointsPath, cancellationToken).ConfigureAwait(false);
|
||||||
|
foreach (var group in entryPoints.Groups.OrderBy(static g => g.Key, StringComparer.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
AppendMetadata(metadataEntries, $"entryPoints.{group.Key}", string.Join(';', group.Value.Select(static ep => $"{ep.Name}={ep.Target}")));
|
||||||
|
}
|
||||||
|
|
||||||
|
var wheelInfo = await PythonWheelInfo.LoadAsync(wheelPath, cancellationToken).ConfigureAwait(false);
|
||||||
|
if (wheelInfo is not null)
|
||||||
|
{
|
||||||
|
foreach (var pair in wheelInfo.ToMetadata())
|
||||||
|
{
|
||||||
|
AppendMetadata(metadataEntries, pair.Key, pair.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var installer = await ReadSingleLineAsync(installerPath, cancellationToken).ConfigureAwait(false);
|
||||||
|
if (!string.IsNullOrWhiteSpace(installer))
|
||||||
|
{
|
||||||
|
AppendMetadata(metadataEntries, "installer", installer);
|
||||||
|
}
|
||||||
|
|
||||||
|
var directUrl = await PythonDirectUrlInfo.LoadAsync(directUrlPath, cancellationToken).ConfigureAwait(false);
|
||||||
|
if (directUrl is not null)
|
||||||
|
{
|
||||||
|
foreach (var pair in directUrl.ToMetadata())
|
||||||
|
{
|
||||||
|
AppendMetadata(metadataEntries, pair.Key, pair.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(directUrl.Url))
|
||||||
|
{
|
||||||
|
evidenceEntries.Add(new LanguageComponentEvidence(
|
||||||
|
LanguageEvidenceKind.Metadata,
|
||||||
|
"direct_url.json",
|
||||||
|
PythonPathHelper.NormalizeRelative(context, directUrlPath),
|
||||||
|
directUrl.Url,
|
||||||
|
Sha256: null));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var recordEntries = await PythonRecordParser.LoadAsync(recordPath, cancellationToken).ConfigureAwait(false);
|
||||||
|
var verification = await PythonRecordVerifier.VerifyAsync(context, distInfoPath, recordEntries, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
metadataEntries.Add(new KeyValuePair<string, string?>("record.totalEntries", verification.TotalEntries.ToString(CultureInfo.InvariantCulture)));
|
||||||
|
metadataEntries.Add(new KeyValuePair<string, string?>("record.hashedEntries", verification.HashedEntries.ToString(CultureInfo.InvariantCulture)));
|
||||||
|
metadataEntries.Add(new KeyValuePair<string, string?>("record.missingFiles", verification.MissingFiles.ToString(CultureInfo.InvariantCulture)));
|
||||||
|
metadataEntries.Add(new KeyValuePair<string, string?>("record.hashMismatches", verification.HashMismatches.ToString(CultureInfo.InvariantCulture)));
|
||||||
|
metadataEntries.Add(new KeyValuePair<string, string?>("record.ioErrors", verification.IoErrors.ToString(CultureInfo.InvariantCulture)));
|
||||||
|
|
||||||
|
if (verification.UnsupportedAlgorithms.Count > 0)
|
||||||
|
{
|
||||||
|
AppendMetadata(metadataEntries, "record.unsupportedAlgorithms", string.Join(';', verification.UnsupportedAlgorithms));
|
||||||
|
}
|
||||||
|
|
||||||
|
evidenceEntries.AddRange(verification.Evidence);
|
||||||
|
var usedByEntrypoint = verification.UsedByEntrypoint || EvaluateEntryPointUsage(context, distInfoPath, entryPoints);
|
||||||
|
|
||||||
|
return new PythonDistribution(
|
||||||
|
trimmedName,
|
||||||
|
trimmedVersion,
|
||||||
|
purl,
|
||||||
|
metadataEntries,
|
||||||
|
evidenceEntries,
|
||||||
|
usedByEntrypoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool EvaluateEntryPointUsage(LanguageAnalyzerContext context, string distInfoPath, PythonEntryPointSet entryPoints)
|
||||||
|
{
|
||||||
|
if (entryPoints.Groups.Count == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var parentDirectory = Directory.GetParent(distInfoPath)?.FullName;
|
||||||
|
if (string.IsNullOrWhiteSpace(parentDirectory))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var group in entryPoints.Groups.Values)
|
||||||
|
{
|
||||||
|
foreach (var entryPoint in group)
|
||||||
|
{
|
||||||
|
var candidatePaths = entryPoint.GetCandidateRelativeScriptPaths();
|
||||||
|
foreach (var relative in candidatePaths)
|
||||||
|
{
|
||||||
|
var combined = Path.GetFullPath(Path.Combine(parentDirectory, relative));
|
||||||
|
if (context.UsageHints.IsPathUsed(combined))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddFileEvidence(LanguageAnalyzerContext context, string path, string source, ICollection<LanguageComponentEvidence> evidence)
|
||||||
|
{
|
||||||
|
if (!File.Exists(path))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
evidence.Add(new LanguageComponentEvidence(
|
||||||
|
LanguageEvidenceKind.File,
|
||||||
|
source,
|
||||||
|
PythonPathHelper.NormalizeRelative(context, path),
|
||||||
|
Value: null,
|
||||||
|
Sha256: null));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AppendMetadata(ICollection<KeyValuePair<string, string?>> metadata, string key, string? value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(key))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata.Add(new KeyValuePair<string, string?>(key, value.Trim()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? ExtractNameFromDirectory(string distInfoPath)
|
||||||
|
{
|
||||||
|
var directoryName = Path.GetFileName(distInfoPath);
|
||||||
|
if (string.IsNullOrWhiteSpace(directoryName))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var suffixIndex = directoryName.IndexOf(".dist-info", StringComparison.OrdinalIgnoreCase);
|
||||||
|
if (suffixIndex <= 0)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var trimmed = directoryName[..suffixIndex];
|
||||||
|
var dashIndex = trimmed.LastIndexOf('-');
|
||||||
|
if (dashIndex <= 0)
|
||||||
|
{
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
return trimmed[..dashIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? ExtractVersionFromDirectory(string distInfoPath)
|
||||||
|
{
|
||||||
|
var directoryName = Path.GetFileName(distInfoPath);
|
||||||
|
if (string.IsNullOrWhiteSpace(directoryName))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var suffixIndex = directoryName.IndexOf(".dist-info", StringComparison.OrdinalIgnoreCase);
|
||||||
|
if (suffixIndex <= 0)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var trimmed = directoryName[..suffixIndex];
|
||||||
|
var dashIndex = trimmed.LastIndexOf('-');
|
||||||
|
if (dashIndex >= 0 && dashIndex < trimmed.Length - 1)
|
||||||
|
{
|
||||||
|
return trimmed[(dashIndex + 1)..];
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string NormalizePackageName(string name)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(name))
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
var builder = new StringBuilder(name.Length);
|
||||||
|
foreach (var ch in name.Trim().ToLowerInvariant())
|
||||||
|
{
|
||||||
|
builder.Append(ch switch
|
||||||
|
{
|
||||||
|
'_' => '-',
|
||||||
|
'.' => '-',
|
||||||
|
' ' => '-',
|
||||||
|
_ => ch
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<string?> ReadSingleLineAsync(string path, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (!File.Exists(path))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
await using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||||
|
using var reader = new StreamReader(stream, PythonEncoding.Utf8, detectEncodingFromByteOrderMarks: true);
|
||||||
|
var line = await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
return line?.Trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed record PythonDistribution(
|
||||||
|
string Name,
|
||||||
|
string Version,
|
||||||
|
string Purl,
|
||||||
|
IReadOnlyCollection<KeyValuePair<string, string?>> Metadata,
|
||||||
|
IReadOnlyCollection<LanguageComponentEvidence> Evidence,
|
||||||
|
bool UsedByEntrypoint)
|
||||||
|
{
|
||||||
|
public IReadOnlyCollection<KeyValuePair<string, string?>> SortedMetadata =>
|
||||||
|
Metadata
|
||||||
|
.OrderBy(static pair => pair.Key, StringComparer.Ordinal)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
public IReadOnlyCollection<LanguageComponentEvidence> SortedEvidence =>
|
||||||
|
Evidence
|
||||||
|
.OrderBy(static item => item.Locator, StringComparer.Ordinal)
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class PythonMetadataDocument
|
||||||
|
{
|
||||||
|
private readonly Dictionary<string, List<string>> _values;
|
||||||
|
|
||||||
|
private PythonMetadataDocument(Dictionary<string, List<string>> values)
|
||||||
|
{
|
||||||
|
_values = values;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<PythonMetadataDocument> LoadAsync(string path, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (!File.Exists(path))
|
||||||
|
{
|
||||||
|
return new PythonMetadataDocument(new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
|
||||||
|
var values = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
await using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||||
|
using var reader = new StreamReader(stream, PythonEncoding.Utf8, detectEncodingFromByteOrderMarks: true);
|
||||||
|
|
||||||
|
string? currentKey = null;
|
||||||
|
var builder = new StringBuilder();
|
||||||
|
|
||||||
|
while (await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false) is { } line)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
if (line.Length == 0)
|
||||||
|
{
|
||||||
|
Commit();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.StartsWith(' ') || line.StartsWith('\t'))
|
||||||
|
{
|
||||||
|
if (currentKey is not null)
|
||||||
|
{
|
||||||
|
if (builder.Length > 0)
|
||||||
|
{
|
||||||
|
builder.Append(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.Append(line.Trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Commit();
|
||||||
|
|
||||||
|
var separator = line.IndexOf(':');
|
||||||
|
if (separator <= 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentKey = line[..separator].Trim();
|
||||||
|
builder.Clear();
|
||||||
|
builder.Append(line[(separator + 1)..].Trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
Commit();
|
||||||
|
return new PythonMetadataDocument(values);
|
||||||
|
|
||||||
|
void Commit()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(currentKey))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!values.TryGetValue(currentKey, out var list))
|
||||||
|
{
|
||||||
|
list = new List<string>();
|
||||||
|
values[currentKey] = list;
|
||||||
|
}
|
||||||
|
|
||||||
|
var value = builder.ToString().Trim();
|
||||||
|
if (value.Length > 0)
|
||||||
|
{
|
||||||
|
list.Add(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
currentKey = null;
|
||||||
|
builder.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string? GetFirst(string key)
|
||||||
|
{
|
||||||
|
if (key is null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _values.TryGetValue(key, out var list) && list.Count > 0
|
||||||
|
? list[0]
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyList<string> GetAll(string key)
|
||||||
|
{
|
||||||
|
if (key is null)
|
||||||
|
{
|
||||||
|
return Array.Empty<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
return _values.TryGetValue(key, out var list)
|
||||||
|
? list.AsReadOnly()
|
||||||
|
: Array.Empty<string>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class PythonWheelInfo
|
||||||
|
{
|
||||||
|
private readonly Dictionary<string, string> _values;
|
||||||
|
|
||||||
|
private PythonWheelInfo(Dictionary<string, string> values)
|
||||||
|
{
|
||||||
|
_values = values;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<PythonWheelInfo?> LoadAsync(string path, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (!File.Exists(path))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
await using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||||
|
using var reader = new StreamReader(stream, PythonEncoding.Utf8, detectEncodingFromByteOrderMarks: true);
|
||||||
|
|
||||||
|
while (await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false) is { } line)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(line))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var separator = line.IndexOf(':');
|
||||||
|
if (separator <= 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var key = line[..separator].Trim();
|
||||||
|
var value = line[(separator + 1)..].Trim();
|
||||||
|
if (key.Length == 0 || value.Length == 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
values[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new PythonWheelInfo(values);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyCollection<KeyValuePair<string, string?>> ToMetadata()
|
||||||
|
{
|
||||||
|
var entries = new List<KeyValuePair<string, string?>>(4);
|
||||||
|
|
||||||
|
if (_values.TryGetValue("Wheel-Version", out var wheelVersion))
|
||||||
|
{
|
||||||
|
entries.Add(new KeyValuePair<string, string?>("wheel.version", wheelVersion));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_values.TryGetValue("Tag", out var tags))
|
||||||
|
{
|
||||||
|
entries.Add(new KeyValuePair<string, string?>("wheel.tags", tags));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_values.TryGetValue("Root-Is-Purelib", out var purelib))
|
||||||
|
{
|
||||||
|
entries.Add(new KeyValuePair<string, string?>("wheel.rootIsPurelib", purelib));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_values.TryGetValue("Generator", out var generator))
|
||||||
|
{
|
||||||
|
entries.Add(new KeyValuePair<string, string?>("wheel.generator", generator));
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class PythonEntryPointSet
|
||||||
|
{
|
||||||
|
public IReadOnlyDictionary<string, IReadOnlyList<PythonEntryPoint>> Groups { get; }
|
||||||
|
|
||||||
|
private PythonEntryPointSet(Dictionary<string, IReadOnlyList<PythonEntryPoint>> groups)
|
||||||
|
{
|
||||||
|
Groups = groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<PythonEntryPointSet> LoadAsync(string path, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (!File.Exists(path))
|
||||||
|
{
|
||||||
|
return new PythonEntryPointSet(new Dictionary<string, IReadOnlyList<PythonEntryPoint>>(StringComparer.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
|
||||||
|
var groups = new Dictionary<string, List<PythonEntryPoint>>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
string? currentGroup = null;
|
||||||
|
|
||||||
|
await using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||||
|
using var reader = new StreamReader(stream, PythonEncoding.Utf8, detectEncodingFromByteOrderMarks: true);
|
||||||
|
|
||||||
|
while (await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false) is { } line)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
line = line.Trim();
|
||||||
|
if (line.Length == 0 || line.StartsWith('#'))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.StartsWith('[') && line.EndsWith(']'))
|
||||||
|
{
|
||||||
|
currentGroup = line[1..^1].Trim();
|
||||||
|
if (currentGroup.Length == 0)
|
||||||
|
{
|
||||||
|
currentGroup = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentGroup is null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var separator = line.IndexOf('=');
|
||||||
|
if (separator <= 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var name = line[..separator].Trim();
|
||||||
|
var target = line[(separator + 1)..].Trim();
|
||||||
|
if (name.Length == 0 || target.Length == 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!groups.TryGetValue(currentGroup, out var list))
|
||||||
|
{
|
||||||
|
list = new List<PythonEntryPoint>();
|
||||||
|
groups[currentGroup] = list;
|
||||||
|
}
|
||||||
|
|
||||||
|
list.Add(new PythonEntryPoint(name, target));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new PythonEntryPointSet(groups.ToDictionary(
|
||||||
|
static pair => pair.Key,
|
||||||
|
static pair => (IReadOnlyList<PythonEntryPoint>)pair.Value.AsReadOnly(),
|
||||||
|
StringComparer.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed record PythonEntryPoint(string Name, string Target)
|
||||||
|
{
|
||||||
|
public IReadOnlyCollection<string> GetCandidateRelativeScriptPaths()
|
||||||
|
{
|
||||||
|
var list = new List<string>(3)
|
||||||
|
{
|
||||||
|
Path.Combine("bin", Name),
|
||||||
|
Path.Combine("Scripts", $"{Name}.exe"),
|
||||||
|
Path.Combine("Scripts", Name)
|
||||||
|
};
|
||||||
|
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed record PythonRecordEntry(string Path, string? HashAlgorithm, string? HashValue, long? Size);
|
||||||
|
|
||||||
|
internal static class PythonRecordParser
|
||||||
|
{
|
||||||
|
public static async Task<IReadOnlyList<PythonRecordEntry>> LoadAsync(string path, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (!File.Exists(path))
|
||||||
|
{
|
||||||
|
return Array.Empty<PythonRecordEntry>();
|
||||||
|
}
|
||||||
|
|
||||||
|
var entries = new List<PythonRecordEntry>();
|
||||||
|
|
||||||
|
await using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||||
|
using var reader = new StreamReader(stream, PythonEncoding.Utf8, detectEncodingFromByteOrderMarks: true);
|
||||||
|
|
||||||
|
while (await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false) is { } line)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
if (line.Length == 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var fields = ParseCsvLine(line);
|
||||||
|
if (fields.Count < 1)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var entryPath = fields[0];
|
||||||
|
string? algorithm = null;
|
||||||
|
string? hashValue = null;
|
||||||
|
|
||||||
|
if (fields.Count > 1 && !string.IsNullOrWhiteSpace(fields[1]))
|
||||||
|
{
|
||||||
|
var hashField = fields[1].Trim();
|
||||||
|
var separator = hashField.IndexOf('=');
|
||||||
|
if (separator > 0 && separator < hashField.Length - 1)
|
||||||
|
{
|
||||||
|
algorithm = hashField[..separator];
|
||||||
|
hashValue = hashField[(separator + 1)..];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
long? size = null;
|
||||||
|
if (fields.Count > 2 && long.TryParse(fields[2], NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedSize))
|
||||||
|
{
|
||||||
|
size = parsedSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
entries.Add(new PythonRecordEntry(entryPath, algorithm, hashValue, size));
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<string> ParseCsvLine(string line)
|
||||||
|
{
|
||||||
|
var values = new List<string>();
|
||||||
|
var builder = new StringBuilder();
|
||||||
|
var inQuotes = false;
|
||||||
|
|
||||||
|
for (var i = 0; i < line.Length; i++)
|
||||||
|
{
|
||||||
|
var ch = line[i];
|
||||||
|
|
||||||
|
if (inQuotes)
|
||||||
|
{
|
||||||
|
if (ch == '"')
|
||||||
|
{
|
||||||
|
var next = i + 1 < line.Length ? line[i + 1] : '\0';
|
||||||
|
if (next == '"')
|
||||||
|
{
|
||||||
|
builder.Append('"');
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
inQuotes = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
builder.Append(ch);
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ch == ',')
|
||||||
|
{
|
||||||
|
values.Add(builder.ToString());
|
||||||
|
builder.Clear();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ch == '"')
|
||||||
|
{
|
||||||
|
inQuotes = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.Append(ch);
|
||||||
|
}
|
||||||
|
|
||||||
|
values.Add(builder.ToString());
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class PythonRecordVerificationResult
|
||||||
|
{
|
||||||
|
public PythonRecordVerificationResult(
|
||||||
|
int totalEntries,
|
||||||
|
int hashedEntries,
|
||||||
|
int missingFiles,
|
||||||
|
int hashMismatches,
|
||||||
|
int ioErrors,
|
||||||
|
bool usedByEntrypoint,
|
||||||
|
IReadOnlyCollection<string> unsupportedAlgorithms,
|
||||||
|
IReadOnlyCollection<LanguageComponentEvidence> evidence)
|
||||||
|
{
|
||||||
|
TotalEntries = totalEntries;
|
||||||
|
HashedEntries = hashedEntries;
|
||||||
|
MissingFiles = missingFiles;
|
||||||
|
HashMismatches = hashMismatches;
|
||||||
|
IoErrors = ioErrors;
|
||||||
|
UsedByEntrypoint = usedByEntrypoint;
|
||||||
|
UnsupportedAlgorithms = unsupportedAlgorithms;
|
||||||
|
Evidence = evidence;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int TotalEntries { get; }
|
||||||
|
public int HashedEntries { get; }
|
||||||
|
public int MissingFiles { get; }
|
||||||
|
public int HashMismatches { get; }
|
||||||
|
public int IoErrors { get; }
|
||||||
|
public bool UsedByEntrypoint { get; }
|
||||||
|
public IReadOnlyCollection<string> UnsupportedAlgorithms { get; }
|
||||||
|
public IReadOnlyCollection<LanguageComponentEvidence> Evidence { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static class PythonRecordVerifier
|
||||||
|
{
|
||||||
|
private static readonly HashSet<string> SupportedAlgorithms = new(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
"sha256"
|
||||||
|
};
|
||||||
|
|
||||||
|
public static async Task<PythonRecordVerificationResult> VerifyAsync(
|
||||||
|
LanguageAnalyzerContext context,
|
||||||
|
string distInfoPath,
|
||||||
|
IReadOnlyList<PythonRecordEntry> entries,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (entries.Count == 0)
|
||||||
|
{
|
||||||
|
return new PythonRecordVerificationResult(0, 0, 0, 0, 0, usedByEntrypoint: false, Array.Empty<string>(), Array.Empty<LanguageComponentEvidence>());
|
||||||
|
}
|
||||||
|
|
||||||
|
var evidence = new List<LanguageComponentEvidence>();
|
||||||
|
var unsupported = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
var root = context.RootPath;
|
||||||
|
if (!root.EndsWith(Path.DirectorySeparatorChar))
|
||||||
|
{
|
||||||
|
root += Path.DirectorySeparatorChar;
|
||||||
|
}
|
||||||
|
|
||||||
|
var parent = Directory.GetParent(distInfoPath)?.FullName ?? distInfoPath;
|
||||||
|
|
||||||
|
var total = 0;
|
||||||
|
var hashed = 0;
|
||||||
|
var missing = 0;
|
||||||
|
var mismatched = 0;
|
||||||
|
var ioErrors = 0;
|
||||||
|
var usedByEntrypoint = false;
|
||||||
|
|
||||||
|
foreach (var entry in entries)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
total++;
|
||||||
|
|
||||||
|
var entryPath = entry.Path.Replace('/', Path.DirectorySeparatorChar);
|
||||||
|
var fullPath = Path.GetFullPath(Path.Combine(parent, entryPath));
|
||||||
|
|
||||||
|
if (!fullPath.StartsWith(root, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
missing++;
|
||||||
|
evidence.Add(new LanguageComponentEvidence(
|
||||||
|
LanguageEvidenceKind.Derived,
|
||||||
|
"RECORD",
|
||||||
|
PythonPathHelper.NormalizeRelative(context, fullPath),
|
||||||
|
"outside-root",
|
||||||
|
Sha256: null));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!File.Exists(fullPath))
|
||||||
|
{
|
||||||
|
missing++;
|
||||||
|
evidence.Add(new LanguageComponentEvidence(
|
||||||
|
LanguageEvidenceKind.Derived,
|
||||||
|
"RECORD",
|
||||||
|
PythonPathHelper.NormalizeRelative(context, fullPath),
|
||||||
|
"missing",
|
||||||
|
Sha256: null));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context.UsageHints.IsPathUsed(fullPath))
|
||||||
|
{
|
||||||
|
usedByEntrypoint = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(entry.HashAlgorithm) || string.IsNullOrWhiteSpace(entry.HashValue))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
hashed++;
|
||||||
|
|
||||||
|
if (!SupportedAlgorithms.Contains(entry.HashAlgorithm))
|
||||||
|
{
|
||||||
|
unsupported.Add(entry.HashAlgorithm);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
string? actualHash = null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
actualHash = await ComputeSha256Base64Async(fullPath, cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (IOException)
|
||||||
|
{
|
||||||
|
ioErrors++;
|
||||||
|
evidence.Add(new LanguageComponentEvidence(
|
||||||
|
LanguageEvidenceKind.Derived,
|
||||||
|
"RECORD",
|
||||||
|
PythonPathHelper.NormalizeRelative(context, fullPath),
|
||||||
|
"io-error",
|
||||||
|
Sha256: null));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
catch (UnauthorizedAccessException)
|
||||||
|
{
|
||||||
|
ioErrors++;
|
||||||
|
evidence.Add(new LanguageComponentEvidence(
|
||||||
|
LanguageEvidenceKind.Derived,
|
||||||
|
"RECORD",
|
||||||
|
PythonPathHelper.NormalizeRelative(context, fullPath),
|
||||||
|
"access-denied",
|
||||||
|
Sha256: null));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (actualHash is null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.Equals(actualHash, entry.HashValue, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
mismatched++;
|
||||||
|
evidence.Add(new LanguageComponentEvidence(
|
||||||
|
LanguageEvidenceKind.Derived,
|
||||||
|
"RECORD",
|
||||||
|
PythonPathHelper.NormalizeRelative(context, fullPath),
|
||||||
|
$"sha256 mismatch expected={entry.HashValue} actual={actualHash}",
|
||||||
|
Sha256: actualHash));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new PythonRecordVerificationResult(
|
||||||
|
total,
|
||||||
|
hashed,
|
||||||
|
missing,
|
||||||
|
mismatched,
|
||||||
|
ioErrors,
|
||||||
|
usedByEntrypoint,
|
||||||
|
unsupported.ToArray(),
|
||||||
|
evidence);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<string> ComputeSha256Base64Async(string path, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||||
|
|
||||||
|
using var sha = SHA256.Create();
|
||||||
|
var buffer = ArrayPool<byte>.Shared.Rent(81920);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
int bytesRead;
|
||||||
|
while ((bytesRead = await stream.ReadAsync(buffer.AsMemory(0, buffer.Length), cancellationToken).ConfigureAwait(false)) > 0)
|
||||||
|
{
|
||||||
|
sha.TransformBlock(buffer, 0, bytesRead, null, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
sha.TransformFinalBlock(Array.Empty<byte>(), 0, 0);
|
||||||
|
return Convert.ToBase64String(sha.Hash ?? Array.Empty<byte>());
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
ArrayPool<byte>.Shared.Return(buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class PythonDirectUrlInfo
|
||||||
|
{
|
||||||
|
public string? Url { get; }
|
||||||
|
public bool IsEditable { get; }
|
||||||
|
public string? Subdirectory { get; }
|
||||||
|
public string? Vcs { get; }
|
||||||
|
public string? Commit { get; }
|
||||||
|
|
||||||
|
private PythonDirectUrlInfo(string? url, bool isEditable, string? subdirectory, string? vcs, string? commit)
|
||||||
|
{
|
||||||
|
Url = url;
|
||||||
|
IsEditable = isEditable;
|
||||||
|
Subdirectory = subdirectory;
|
||||||
|
Vcs = vcs;
|
||||||
|
Commit = commit;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<PythonDirectUrlInfo?> LoadAsync(string path, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (!File.Exists(path))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
await using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||||
|
using var document = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||||
|
var root = document.RootElement;
|
||||||
|
|
||||||
|
var url = root.TryGetProperty("url", out var urlElement) ? urlElement.GetString() : null;
|
||||||
|
var isEditable = root.TryGetProperty("dir_info", out var dirInfo) && dirInfo.TryGetProperty("editable", out var editableValue) && editableValue.GetBoolean();
|
||||||
|
var subdir = root.TryGetProperty("dir_info", out dirInfo) && dirInfo.TryGetProperty("subdirectory", out var subdirElement) ? subdirElement.GetString() : null;
|
||||||
|
|
||||||
|
string? vcs = null;
|
||||||
|
string? commit = null;
|
||||||
|
|
||||||
|
if (root.TryGetProperty("vcs_info", out var vcsInfo))
|
||||||
|
{
|
||||||
|
vcs = vcsInfo.TryGetProperty("vcs", out var vcsElement) ? vcsElement.GetString() : null;
|
||||||
|
commit = vcsInfo.TryGetProperty("commit_id", out var commitElement) ? commitElement.GetString() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new PythonDirectUrlInfo(url, isEditable, subdir, vcs, commit);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyCollection<KeyValuePair<string, string?>> ToMetadata()
|
||||||
|
{
|
||||||
|
var entries = new List<KeyValuePair<string, string?>>();
|
||||||
|
|
||||||
|
if (IsEditable)
|
||||||
|
{
|
||||||
|
entries.Add(new KeyValuePair<string, string?>("editable", "true"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(Url))
|
||||||
|
{
|
||||||
|
entries.Add(new KeyValuePair<string, string?>("sourceUrl", Url));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(Subdirectory))
|
||||||
|
{
|
||||||
|
entries.Add(new KeyValuePair<string, string?>("sourceSubdirectory", Subdirectory));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(Vcs))
|
||||||
|
{
|
||||||
|
entries.Add(new KeyValuePair<string, string?>("sourceVcs", Vcs));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(Commit))
|
||||||
|
{
|
||||||
|
entries.Add(new KeyValuePair<string, string?>("sourceCommit", Commit));
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static class PythonPathHelper
|
||||||
|
{
|
||||||
|
public static string NormalizeRelative(LanguageAnalyzerContext context, string path)
|
||||||
|
{
|
||||||
|
var relative = context.GetRelativePath(path);
|
||||||
|
if (string.IsNullOrEmpty(relative) || relative == ".")
|
||||||
|
{
|
||||||
|
return ".";
|
||||||
|
}
|
||||||
|
|
||||||
|
return relative;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static class PythonEncoding
|
||||||
|
{
|
||||||
|
public static readonly UTF8Encoding Utf8 = new(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true);
|
||||||
|
}
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
namespace StellaOps.Scanner.Analyzers.Lang.Python;
|
|
||||||
|
|
||||||
internal static class Placeholder
|
|
||||||
{
|
|
||||||
// Analyzer implementation will be added during Sprint LA2.
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
using System;
|
||||||
|
using StellaOps.Scanner.Analyzers.Lang.Plugin;
|
||||||
|
|
||||||
|
namespace StellaOps.Scanner.Analyzers.Lang.Python;
|
||||||
|
|
||||||
|
public sealed class PythonAnalyzerPlugin : ILanguageAnalyzerPlugin
|
||||||
|
{
|
||||||
|
public string Name => "StellaOps.Scanner.Analyzers.Lang.Python";
|
||||||
|
|
||||||
|
public bool IsAvailable(IServiceProvider services) => services is not null;
|
||||||
|
|
||||||
|
public ILanguageAnalyzer CreateAnalyzer(IServiceProvider services)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(services);
|
||||||
|
return new PythonLanguageAnalyzer();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using StellaOps.Scanner.Analyzers.Lang.Python.Internal;
|
||||||
|
|
||||||
|
namespace StellaOps.Scanner.Analyzers.Lang.Python;
|
||||||
|
|
||||||
|
public sealed class PythonLanguageAnalyzer : ILanguageAnalyzer
|
||||||
|
{
|
||||||
|
private static readonly EnumerationOptions Enumeration = new()
|
||||||
|
{
|
||||||
|
RecurseSubdirectories = true,
|
||||||
|
IgnoreInaccessible = true,
|
||||||
|
AttributesToSkip = FileAttributes.Device | FileAttributes.ReparsePoint
|
||||||
|
};
|
||||||
|
|
||||||
|
public string Id => "python";
|
||||||
|
|
||||||
|
public string DisplayName => "Python Analyzer";
|
||||||
|
|
||||||
|
public ValueTask AnalyzeAsync(LanguageAnalyzerContext context, LanguageComponentWriter writer, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(context);
|
||||||
|
ArgumentNullException.ThrowIfNull(writer);
|
||||||
|
|
||||||
|
return AnalyzeInternalAsync(context, writer, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async ValueTask AnalyzeInternalAsync(LanguageAnalyzerContext context, LanguageComponentWriter writer, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var distInfoDirectories = Directory
|
||||||
|
.EnumerateDirectories(context.RootPath, "*.dist-info", Enumeration)
|
||||||
|
.OrderBy(static path => path, StringComparer.Ordinal)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
foreach (var distInfoPath in distInfoDirectories)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
PythonDistribution? distribution;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
distribution = await PythonDistributionLoader.LoadAsync(context, distInfoPath, cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (IOException)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
catch (JsonException)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
catch (UnauthorizedAccessException)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (distribution is null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.AddFromPurl(
|
||||||
|
analyzerId: "python",
|
||||||
|
purl: distribution.Purl,
|
||||||
|
name: distribution.Name,
|
||||||
|
version: distribution.Version,
|
||||||
|
type: "pypi",
|
||||||
|
metadata: distribution.SortedMetadata,
|
||||||
|
evidence: distribution.SortedEvidence,
|
||||||
|
usedByEntrypoint: distribution.UsedByEntrypoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
| Seq | ID | Status | Depends on | Description | Exit Criteria |
|
| Seq | ID | Status | Depends on | Description | Exit Criteria |
|
||||||
|-----|----|--------|------------|-------------|---------------|
|
|-----|----|--------|------------|-------------|---------------|
|
||||||
| 1 | SCANNER-ANALYZERS-LANG-10-303A | TODO | SCANNER-ANALYZERS-LANG-10-307 | STREAM-based parser for `*.dist-info` (`METADATA`, `WHEEL`, `entry_points.txt`) with normalization + evidence capture. | Parser handles CPython 3.8–3.12 metadata variations; fixtures confirm canonical ordering and UTF-8 handling. |
|
| 1 | SCANNER-ANALYZERS-LANG-10-303A | DONE (2025-10-21) | SCANNER-ANALYZERS-LANG-10-307 | STREAM-based parser for `*.dist-info` (`METADATA`, `WHEEL`, `entry_points.txt`) with normalization + evidence capture. | Parser handles CPython 3.8–3.12 metadata variations; fixtures confirm canonical ordering and UTF-8 handling. |
|
||||||
| 2 | SCANNER-ANALYZERS-LANG-10-303B | TODO | SCANNER-ANALYZERS-LANG-10-303A | RECORD hash verifier with chunked hashing, Zip64 support, and mismatch diagnostics. | Verifier processes 5 GB RECORD fixture without allocations >2 MB; mismatches produce deterministic evidence records. |
|
| 2 | SCANNER-ANALYZERS-LANG-10-303B | DONE (2025-10-21) | SCANNER-ANALYZERS-LANG-10-303A | RECORD hash verifier with chunked hashing, Zip64 support, and mismatch diagnostics. | Verifier processes 5 GB RECORD fixture without allocations >2 MB; mismatches produce deterministic evidence records. |
|
||||||
| 3 | SCANNER-ANALYZERS-LANG-10-303C | TODO | SCANNER-ANALYZERS-LANG-10-303B | Editable install + pip cache detection; integrate EntryTrace hints for runtime usage flags. | Editable installs resolved to source path; usage flags propagated; regression tests cover mixed editable + wheel installs. |
|
| 3 | SCANNER-ANALYZERS-LANG-10-303C | DONE (2025-10-21) | SCANNER-ANALYZERS-LANG-10-303B | Editable install + pip cache detection; integrate EntryTrace hints for runtime usage flags. | Editable installs resolved to source path; usage flags propagated; regression tests cover mixed editable + wheel installs. |
|
||||||
| 4 | SCANNER-ANALYZERS-LANG-10-307P | TODO | SCANNER-ANALYZERS-LANG-10-303C | Shared helper integration (license metadata, quiet provenance, component merging). | Shared helpers reused; analyzer-specific metadata minimal; deterministic merge tests pass. |
|
| 4 | SCANNER-ANALYZERS-LANG-10-307P | TODO | SCANNER-ANALYZERS-LANG-10-303C | Shared helper integration (license metadata, quiet provenance, component merging). | Shared helpers reused; analyzer-specific metadata minimal; deterministic merge tests pass. |
|
||||||
| 5 | SCANNER-ANALYZERS-LANG-10-308P | TODO | SCANNER-ANALYZERS-LANG-10-307P | Golden fixtures + determinism harness for Python analyzer; add benchmark and hash throughput reporting. | Fixtures under `Fixtures/lang/python/`; determinism CI guard; benchmark CSV added with threshold alerts. |
|
| 5 | SCANNER-ANALYZERS-LANG-10-308P | TODO | SCANNER-ANALYZERS-LANG-10-307P | Golden fixtures + determinism harness for Python analyzer; add benchmark and hash throughput reporting. | Fixtures under `Fixtures/lang/python/`; determinism CI guard; benchmark CSV added with threshold alerts. |
|
||||||
| 6 | SCANNER-ANALYZERS-LANG-10-309P | TODO | SCANNER-ANALYZERS-LANG-10-308P | Package plug-in (manifest, DI registration) and document Offline Kit bundling of Python stdlib metadata if needed. | Manifest copied to `plugins/scanner/analyzers/lang/`; Worker loads analyzer; Offline Kit doc updated. |
|
| 6 | SCANNER-ANALYZERS-LANG-10-309P | TODO | SCANNER-ANALYZERS-LANG-10-308P | Package plug-in (manifest, DI registration) and document Offline Kit bundling of Python stdlib metadata if needed. | Manifest copied to `plugins/scanner/analyzers/lang/`; Worker loads analyzer; Offline Kit doc updated. |
|
||||||
|
|||||||
23
src/StellaOps.Scanner.Analyzers.Lang.Python/manifest.json
Normal file
23
src/StellaOps.Scanner.Analyzers.Lang.Python/manifest.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"schemaVersion": "1.0",
|
||||||
|
"id": "stellaops.analyzer.lang.python",
|
||||||
|
"displayName": "StellaOps Python Analyzer (preview)",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"requiresRestart": true,
|
||||||
|
"entryPoint": {
|
||||||
|
"type": "dotnet",
|
||||||
|
"assembly": "StellaOps.Scanner.Analyzers.Lang.Python.dll",
|
||||||
|
"typeName": "StellaOps.Scanner.Analyzers.Lang.Python.PythonAnalyzerPlugin"
|
||||||
|
},
|
||||||
|
"capabilities": [
|
||||||
|
"language-analyzer",
|
||||||
|
"python",
|
||||||
|
"pypi"
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"org.stellaops.analyzer.language": "python",
|
||||||
|
"org.stellaops.analyzer.kind": "language",
|
||||||
|
"org.stellaops.restart.required": "true",
|
||||||
|
"org.stellaops.analyzer.status": "preview"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
using System;
|
||||||
|
using StellaOps.Scanner.Analyzers.Lang.Plugin;
|
||||||
|
|
||||||
|
namespace StellaOps.Scanner.Analyzers.Lang.Rust;
|
||||||
|
|
||||||
|
public sealed class RustAnalyzerPlugin : ILanguageAnalyzerPlugin
|
||||||
|
{
|
||||||
|
public string Name => "StellaOps.Scanner.Analyzers.Lang.Rust";
|
||||||
|
|
||||||
|
public bool IsAvailable(IServiceProvider services) => false;
|
||||||
|
|
||||||
|
public ILanguageAnalyzer CreateAnalyzer(IServiceProvider services)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(services);
|
||||||
|
return new RustLanguageAnalyzer();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace StellaOps.Scanner.Analyzers.Lang.Rust;
|
||||||
|
|
||||||
|
public sealed class RustLanguageAnalyzer : ILanguageAnalyzer
|
||||||
|
{
|
||||||
|
public string Id => "rust";
|
||||||
|
|
||||||
|
public string DisplayName => "Rust Analyzer (preview)";
|
||||||
|
|
||||||
|
public ValueTask AnalyzeAsync(LanguageAnalyzerContext context, LanguageComponentWriter writer, CancellationToken cancellationToken)
|
||||||
|
=> ValueTask.FromException(new NotImplementedException("Rust analyzer implementation pending Sprint LA5."));
|
||||||
|
}
|
||||||
23
src/StellaOps.Scanner.Analyzers.Lang.Rust/manifest.json
Normal file
23
src/StellaOps.Scanner.Analyzers.Lang.Rust/manifest.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"schemaVersion": "1.0",
|
||||||
|
"id": "stellaops.analyzer.lang.rust",
|
||||||
|
"displayName": "StellaOps Rust Analyzer (preview)",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"requiresRestart": true,
|
||||||
|
"entryPoint": {
|
||||||
|
"type": "dotnet",
|
||||||
|
"assembly": "StellaOps.Scanner.Analyzers.Lang.Rust.dll",
|
||||||
|
"typeName": "StellaOps.Scanner.Analyzers.Lang.Rust.RustAnalyzerPlugin"
|
||||||
|
},
|
||||||
|
"capabilities": [
|
||||||
|
"language-analyzer",
|
||||||
|
"rust",
|
||||||
|
"cargo"
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"org.stellaops.analyzer.language": "rust",
|
||||||
|
"org.stellaops.analyzer.kind": "language",
|
||||||
|
"org.stellaops.restart.required": "true",
|
||||||
|
"org.stellaops.analyzer.status": "preview"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
using System.IO;
|
||||||
|
using StellaOps.Scanner.Analyzers.Lang.DotNet;
|
||||||
|
using StellaOps.Scanner.Analyzers.Lang.Tests.Harness;
|
||||||
|
using StellaOps.Scanner.Analyzers.Lang.Tests.TestUtilities;
|
||||||
|
|
||||||
|
namespace StellaOps.Scanner.Analyzers.Lang.Tests.DotNet;
|
||||||
|
|
||||||
|
public sealed class DotNetLanguageAnalyzerTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task SimpleFixtureProducesDeterministicOutputAsync()
|
||||||
|
{
|
||||||
|
var cancellationToken = TestContext.Current.CancellationToken;
|
||||||
|
var fixturePath = TestPaths.ResolveFixture("lang", "dotnet", "simple");
|
||||||
|
var goldenPath = Path.Combine(fixturePath, "expected.json");
|
||||||
|
|
||||||
|
var analyzers = new ILanguageAnalyzer[]
|
||||||
|
{
|
||||||
|
new DotNetLanguageAnalyzer()
|
||||||
|
};
|
||||||
|
|
||||||
|
await LanguageAnalyzerTestHarness.AssertDeterministicAsync(
|
||||||
|
fixturePath,
|
||||||
|
goldenPath,
|
||||||
|
analyzers,
|
||||||
|
cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user