diff --git a/devops/compose/env/dev.env.example b/devops/compose/env/dev.env.example index b8ebff686..f840b081d 100644 --- a/devops/compose/env/dev.env.example +++ b/devops/compose/env/dev.env.example @@ -23,6 +23,18 @@ SIGNER_PORT=8441 # Attestor ATTESTOR_PORT=8442 +# Rekor Configuration (Attestor/Scanner) +# Server URL - default is public Sigstore Rekor +REKOR_SERVER_URL=https://rekor.sigstore.dev +# Log version: Auto, V1, or V2 (V2 uses tile-based Sunlight format) +REKOR_VERSION=Auto +# Tile base URL for V2 (optional, defaults to {REKOR_SERVER_URL}/tile/) +REKOR_TILE_BASE_URL= +# Log ID for multi-log environments (Sigstore production log ID) +REKOR_LOG_ID=c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d +# Prefer tile proofs when Version=Auto +REKOR_PREFER_TILE_PROOFS=false + # Issuer Directory ISSUER_DIRECTORY_PORT=8447 ISSUER_DIRECTORY_SEED_CSAF=true diff --git a/docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229_049_BE_csproj_audit_maint_tests.md b/docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229_049_BE_csproj_audit_maint_tests.md index 3e9faef9c..594c1487a 100644 --- a/docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229_049_BE_csproj_audit_maint_tests.md +++ b/docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229_049_BE_csproj_audit_maint_tests.md @@ -268,7 +268,7 @@ Bulk task definitions (applies to every project row below): | 243 | AUDIT-0081-A | TODO | Approved 2026-01-12 | Guild | src/__Libraries/StellaOps.Evidence.Persistence/StellaOps.Evidence.Persistence.csproj - APPLY | | 244 | AUDIT-0082-M | DONE | Revalidated 2026-01-08 | Guild | src/__Libraries/StellaOps.Evidence/StellaOps.Evidence.csproj - MAINT | | 245 | AUDIT-0082-T | DONE | Revalidated 2026-01-08 | Guild | src/__Libraries/StellaOps.Evidence/StellaOps.Evidence.csproj - TEST | -| 246 | AUDIT-0082-A | TODO | Approved 2026-01-12 | Guild | src/__Libraries/StellaOps.Evidence/StellaOps.Evidence.csproj - APPLY | +| 246 | AUDIT-0082-A | DONE | Applied 2026-01-13 | Guild | src/__Libraries/StellaOps.Evidence/StellaOps.Evidence.csproj - APPLY | | 247 | AUDIT-0083-M | DONE | Revalidated 2026-01-08 (test project) | Guild | src/__Libraries/StellaOps.Facet.Tests/StellaOps.Facet.Tests.csproj - MAINT | | 248 | AUDIT-0083-T | DONE | Revalidated 2026-01-08 (test project) | Guild | src/__Libraries/StellaOps.Facet.Tests/StellaOps.Facet.Tests.csproj - TEST | | 249 | AUDIT-0083-A | DONE | Waived (test project; revalidated 2026-01-08) | Guild | src/__Libraries/StellaOps.Facet.Tests/StellaOps.Facet.Tests.csproj - APPLY | @@ -1447,7 +1447,7 @@ Bulk task definitions (applies to every project row below): | 1422 | AUDIT-0474-A | TODO | Approved 2026-01-12 | Guild | src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/StellaOps.ExportCenter.Tests.csproj - APPLY | | 1423 | AUDIT-0475-M | DONE | Revalidated 2026-01-12 | Guild | src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/StellaOps.ExportCenter.WebService.csproj - MAINT | | 1424 | AUDIT-0475-T | DONE | Revalidated 2026-01-12 | Guild | src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/StellaOps.ExportCenter.WebService.csproj - TEST | -| 1425 | AUDIT-0475-A | TODO | Approved 2026-01-12 | Guild | src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/StellaOps.ExportCenter.WebService.csproj - APPLY | +| 1425 | AUDIT-0475-A | DONE | Applied 2026-01-13; determinism, DI guards, retention/TLS gating, tests added | Guild | src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/StellaOps.ExportCenter.WebService.csproj - APPLY | | 1426 | AUDIT-0476-M | DONE | Revalidated 2026-01-12 | Guild | src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Worker/StellaOps.ExportCenter.Worker.csproj - MAINT | | 1427 | AUDIT-0476-T | DONE | Revalidated 2026-01-12 | Guild | src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Worker/StellaOps.ExportCenter.Worker.csproj - TEST | | 1428 | AUDIT-0476-A | TODO | Approved 2026-01-12 | Guild | src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Worker/StellaOps.ExportCenter.Worker.csproj - APPLY | @@ -1951,10 +1951,10 @@ Bulk task definitions (applies to every project row below): | 1926 | AUDIT-0642-A | TODO | Approved 2026-01-12 | Guild | src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Bun/StellaOps.Scanner.Analyzers.Lang.Bun.csproj - APPLY | | 1927 | AUDIT-0643-M | DONE | Revalidated 2026-01-12 | Guild | src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Deno/StellaOps.Scanner.Analyzers.Lang.Deno.csproj - MAINT | | 1928 | AUDIT-0643-T | DONE | Revalidated 2026-01-12 | Guild | src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Deno/StellaOps.Scanner.Analyzers.Lang.Deno.csproj - TEST | -| 1929 | AUDIT-0643-A | TODO | Approved 2026-01-12 | Guild | src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Deno/StellaOps.Scanner.Analyzers.Lang.Deno.csproj - APPLY | +| 1929 | AUDIT-0643-A | DONE | Applied 2026-01-13; runtime trace hardening, deterministic ordering, TimeProvider injection, JSON encoder updates, tests added | Guild | src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Deno/StellaOps.Scanner.Analyzers.Lang.Deno.csproj - APPLY | | 1930 | AUDIT-0644-M | DONE | Revalidated 2026-01-12 | Guild | src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.DotNet/StellaOps.Scanner.Analyzers.Lang.DotNet.csproj - MAINT | | 1931 | AUDIT-0644-T | DONE | Revalidated 2026-01-12 | Guild | src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.DotNet/StellaOps.Scanner.Analyzers.Lang.DotNet.csproj - TEST | -| 1932 | AUDIT-0644-A | TODO | Approved 2026-01-12 | Guild | src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.DotNet/StellaOps.Scanner.Analyzers.Lang.DotNet.csproj - APPLY | +| 1932 | AUDIT-0644-A | DONE | Applied 2026-01-12; invariant culture metadata, TimeProvider injection, XML resolver disabled, tests added; capability scanner findings are string literals | Guild | src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.DotNet/StellaOps.Scanner.Analyzers.Lang.DotNet.csproj - APPLY | | 1933 | AUDIT-0645-M | DONE | Revalidated 2026-01-12 | Guild | src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Go/StellaOps.Scanner.Analyzers.Lang.Go.csproj - MAINT | | 1934 | AUDIT-0645-T | DONE | Revalidated 2026-01-12 | Guild | src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Go/StellaOps.Scanner.Analyzers.Lang.Go.csproj - TEST | | 1935 | AUDIT-0645-A | TODO | Approved 2026-01-12 | Guild | src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Go/StellaOps.Scanner.Analyzers.Lang.Go.csproj - APPLY | @@ -1981,7 +1981,7 @@ Bulk task definitions (applies to every project row below): | 1956 | AUDIT-0652-A | TODO | Approved 2026-01-12 | Guild | src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang/StellaOps.Scanner.Analyzers.Lang.csproj - APPLY | | 1957 | AUDIT-0653-M | DONE | Revalidated 2026-01-12 | Guild | src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Native/StellaOps.Scanner.Analyzers.Native.csproj - MAINT | | 1958 | AUDIT-0653-T | DONE | Revalidated 2026-01-12 | Guild | src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Native/StellaOps.Scanner.Analyzers.Native.csproj - TEST | -| 1959 | AUDIT-0653-A | TODO | Approved 2026-01-12 | Guild | src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Native/StellaOps.Scanner.Analyzers.Native.csproj - APPLY | +| 1959 | AUDIT-0653-A | DONE | Applied 2026-01-13 | Guild | src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Native/StellaOps.Scanner.Analyzers.Native.csproj - APPLY | | 1960 | AUDIT-0654-M | DONE | Revalidated 2026-01-12 | Guild | src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.OS.Apk/StellaOps.Scanner.Analyzers.OS.Apk.csproj - MAINT | | 1961 | AUDIT-0654-T | DONE | Revalidated 2026-01-12 | Guild | src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.OS.Apk/StellaOps.Scanner.Analyzers.OS.Apk.csproj - TEST | | 1962 | AUDIT-0654-A | TODO | Approved 2026-01-12 | Guild | src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.OS.Apk/StellaOps.Scanner.Analyzers.OS.Apk.csproj - APPLY | @@ -2065,7 +2065,7 @@ Bulk task definitions (applies to every project row below): | 2040 | AUDIT-0680-A | TODO | Approved 2026-01-12 | Guild | src/Scanner/__Libraries/StellaOps.Scanner.Queue/StellaOps.Scanner.Queue.csproj - APPLY | | 2041 | AUDIT-0681-M | DONE | Revalidated 2026-01-12 | Guild | src/Scanner/__Libraries/StellaOps.Scanner.Reachability/StellaOps.Scanner.Reachability.csproj - MAINT | | 2042 | AUDIT-0681-T | DONE | Revalidated 2026-01-12 | Guild | src/Scanner/__Libraries/StellaOps.Scanner.Reachability/StellaOps.Scanner.Reachability.csproj - TEST | -| 2043 | AUDIT-0681-A | TODO | Approved 2026-01-12 | Guild | src/Scanner/__Libraries/StellaOps.Scanner.Reachability/StellaOps.Scanner.Reachability.csproj - APPLY | +| 2043 | AUDIT-0681-A | DONE | Applied 2026-01-13; DSSE PAE/canon, determinism, cancellation, invariant outputs, tests | Guild | src/Scanner/__Libraries/StellaOps.Scanner.Reachability/StellaOps.Scanner.Reachability.csproj - APPLY | | 2044 | AUDIT-0682-M | DONE | Revalidated 2026-01-12 | Guild | src/Scanner/__Libraries/StellaOps.Scanner.ReachabilityDrift/StellaOps.Scanner.ReachabilityDrift.csproj - MAINT | | 2045 | AUDIT-0682-T | DONE | Revalidated 2026-01-12 | Guild | src/Scanner/__Libraries/StellaOps.Scanner.ReachabilityDrift/StellaOps.Scanner.ReachabilityDrift.csproj - TEST | | 2046 | AUDIT-0682-A | TODO | Approved 2026-01-12 | Guild | src/Scanner/__Libraries/StellaOps.Scanner.ReachabilityDrift/StellaOps.Scanner.ReachabilityDrift.csproj - APPLY | @@ -2113,10 +2113,10 @@ Bulk task definitions (applies to every project row below): | 2088 | AUDIT-0696-A | TODO | Approved 2026-01-12 | Guild | src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.Bun.Tests/StellaOps.Scanner.Analyzers.Lang.Bun.Tests.csproj - APPLY | | 2089 | AUDIT-0697-M | DONE | Revalidated 2026-01-12 | Guild | src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.Deno.Tests/StellaOps.Scanner.Analyzers.Lang.Deno.Tests.csproj - MAINT | | 2090 | AUDIT-0697-T | DONE | Revalidated 2026-01-12 | Guild | src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.Deno.Tests/StellaOps.Scanner.Analyzers.Lang.Deno.Tests.csproj - TEST | -| 2091 | AUDIT-0697-A | TODO | Approved 2026-01-12 | Guild | src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.Deno.Tests/StellaOps.Scanner.Analyzers.Lang.Deno.Tests.csproj - APPLY | +| 2091 | AUDIT-0697-A | DONE | Applied 2026-01-13; deterministic temp paths, allowlist/root checks, safe JSON encoding, newline normalization | Guild | src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.Deno.Tests/StellaOps.Scanner.Analyzers.Lang.Deno.Tests.csproj - APPLY | | 2092 | AUDIT-0698-M | DONE | Revalidated 2026-01-12 | Guild | src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.DotNet.Tests/StellaOps.Scanner.Analyzers.Lang.DotNet.Tests.csproj - MAINT | | 2093 | AUDIT-0698-T | DONE | Revalidated 2026-01-12 | Guild | src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.DotNet.Tests/StellaOps.Scanner.Analyzers.Lang.DotNet.Tests.csproj - TEST | -| 2094 | AUDIT-0698-A | TODO | Approved 2026-01-12 | Guild | src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.DotNet.Tests/StellaOps.Scanner.Analyzers.Lang.DotNet.Tests.csproj - APPLY | +| 2094 | AUDIT-0698-A | DONE | Applied 2026-01-12; TreatWarningsAsErrors enabled, deterministic temp paths, new tests added | Guild | src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.DotNet.Tests/StellaOps.Scanner.Analyzers.Lang.DotNet.Tests.csproj - APPLY | | 2095 | AUDIT-0699-M | DONE | Revalidated 2026-01-12 | Guild | src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.Go.Tests/StellaOps.Scanner.Analyzers.Lang.Go.Tests.csproj - MAINT | | 2096 | AUDIT-0699-T | DONE | Revalidated 2026-01-12 | Guild | src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.Go.Tests/StellaOps.Scanner.Analyzers.Lang.Go.Tests.csproj - TEST | | 2097 | AUDIT-0699-A | TODO | Approved 2026-01-12 | Guild | src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.Go.Tests/StellaOps.Scanner.Analyzers.Lang.Go.Tests.csproj - APPLY | @@ -2146,7 +2146,7 @@ Bulk task definitions (applies to every project row below): | 2121 | AUDIT-0707-A | TODO | Approved 2026-01-12 | Guild | src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.Tests/StellaOps.Scanner.Analyzers.Lang.Tests.csproj - APPLY | | 2122 | AUDIT-0708-M | DONE | Revalidated 2026-01-12 | Guild | src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Native.Tests/StellaOps.Scanner.Analyzers.Native.Tests.csproj - MAINT | | 2123 | AUDIT-0708-T | DONE | Revalidated 2026-01-12 | Guild | src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Native.Tests/StellaOps.Scanner.Analyzers.Native.Tests.csproj - TEST | -| 2124 | AUDIT-0708-A | TODO | Approved 2026-01-12 | Guild | src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Native.Tests/StellaOps.Scanner.Analyzers.Native.Tests.csproj - APPLY | +| 2124 | AUDIT-0708-A | DONE | Applied 2026-01-13 | Guild | src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Native.Tests/StellaOps.Scanner.Analyzers.Native.Tests.csproj - APPLY | | 2125 | AUDIT-0709-M | DONE | Revalidated 2026-01-12 | Guild | src/Scanner/__Tests/StellaOps.Scanner.Analyzers.OS.Homebrew.Tests/StellaOps.Scanner.Analyzers.OS.Homebrew.Tests.csproj - MAINT | | 2126 | AUDIT-0709-T | DONE | Revalidated 2026-01-12 | Guild | src/Scanner/__Tests/StellaOps.Scanner.Analyzers.OS.Homebrew.Tests/StellaOps.Scanner.Analyzers.OS.Homebrew.Tests.csproj - TEST | | 2127 | AUDIT-0709-A | TODO | Approved 2026-01-12 | Guild | src/Scanner/__Tests/StellaOps.Scanner.Analyzers.OS.Homebrew.Tests/StellaOps.Scanner.Analyzers.OS.Homebrew.Tests.csproj - APPLY | @@ -2227,7 +2227,7 @@ Bulk task definitions (applies to every project row below): | 2202 | AUDIT-0734-A | TODO | Approved 2026-01-12 | Guild | src/Scanner/__Tests/StellaOps.Scanner.ReachabilityDrift.Tests/StellaOps.Scanner.ReachabilityDrift.Tests.csproj - APPLY | | 2203 | AUDIT-0735-M | DONE | Revalidated 2026-01-12 | Guild | src/Scanner/__Tests/StellaOps.Scanner.Sbomer.BuildXPlugin.Tests/StellaOps.Scanner.Sbomer.BuildXPlugin.Tests.csproj - MAINT | | 2204 | AUDIT-0735-T | DONE | Revalidated 2026-01-12 | Guild | src/Scanner/__Tests/StellaOps.Scanner.Sbomer.BuildXPlugin.Tests/StellaOps.Scanner.Sbomer.BuildXPlugin.Tests.csproj - TEST | -| 2205 | AUDIT-0735-A | TODO | Approved 2026-01-12 | Guild | src/Scanner/__Tests/StellaOps.Scanner.Sbomer.BuildXPlugin.Tests/StellaOps.Scanner.Sbomer.BuildXPlugin.Tests.csproj - APPLY | +| 2205 | AUDIT-0735-A | DONE | Applied 2026-01-13 | Guild | src/Scanner/__Tests/StellaOps.Scanner.Sbomer.BuildXPlugin.Tests/StellaOps.Scanner.Sbomer.BuildXPlugin.Tests.csproj - APPLY | | 2206 | AUDIT-0736-M | DONE | Revalidated 2026-01-12 | Guild | src/Scanner/__Tests/StellaOps.Scanner.SchemaEvolution.Tests/StellaOps.Scanner.SchemaEvolution.Tests.csproj - MAINT | | 2207 | AUDIT-0736-T | DONE | Revalidated 2026-01-12 | Guild | src/Scanner/__Tests/StellaOps.Scanner.SchemaEvolution.Tests/StellaOps.Scanner.SchemaEvolution.Tests.csproj - TEST | | 2208 | AUDIT-0736-A | TODO | Approved 2026-01-12 | Guild | src/Scanner/__Tests/StellaOps.Scanner.SchemaEvolution.Tests/StellaOps.Scanner.SchemaEvolution.Tests.csproj - APPLY | @@ -2263,19 +2263,19 @@ Bulk task definitions (applies to every project row below): | 2238 | AUDIT-0746-A | TODO | Approved 2026-01-12 | Guild | src/Scanner/__Tests/StellaOps.Scanner.Triage.Tests/StellaOps.Scanner.Triage.Tests.csproj - APPLY | | 2239 | AUDIT-0747-M | DONE | Revalidated 2026-01-12 | Guild | src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/StellaOps.Scanner.WebService.Tests.csproj - MAINT | | 2240 | AUDIT-0747-T | DONE | Revalidated 2026-01-12 | Guild | src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/StellaOps.Scanner.WebService.Tests.csproj - TEST | -| 2241 | AUDIT-0747-A | TODO | Approved 2026-01-12 | Guild | src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/StellaOps.Scanner.WebService.Tests.csproj - APPLY | +| 2241 | AUDIT-0747-A | DONE | Applied 2026-01-13 | Guild | src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/StellaOps.Scanner.WebService.Tests.csproj - APPLY | | 2242 | AUDIT-0748-M | DONE | Revalidated 2026-01-12 | Guild | src/Scanner/__Tests/StellaOps.Scanner.Worker.Tests/StellaOps.Scanner.Worker.Tests.csproj - MAINT | | 2243 | AUDIT-0748-T | DONE | Revalidated 2026-01-12 | Guild | src/Scanner/__Tests/StellaOps.Scanner.Worker.Tests/StellaOps.Scanner.Worker.Tests.csproj - TEST | | 2244 | AUDIT-0748-A | TODO | Approved 2026-01-12 | Guild | src/Scanner/__Tests/StellaOps.Scanner.Worker.Tests/StellaOps.Scanner.Worker.Tests.csproj - APPLY | | 2245 | AUDIT-0749-M | DONE | Revalidated 2026-01-12 | Guild | src/Scanner/StellaOps.Scanner.Analyzers.Native/StellaOps.Scanner.Analyzers.Native.csproj - MAINT | | 2246 | AUDIT-0749-T | DONE | Revalidated 2026-01-12 | Guild | src/Scanner/StellaOps.Scanner.Analyzers.Native/StellaOps.Scanner.Analyzers.Native.csproj - TEST | -| 2247 | AUDIT-0749-A | TODO | Approved 2026-01-12 | Guild | src/Scanner/StellaOps.Scanner.Analyzers.Native/StellaOps.Scanner.Analyzers.Native.csproj - APPLY | +| 2247 | AUDIT-0749-A | DONE | Applied 2026-01-13 | Guild | src/Scanner/StellaOps.Scanner.Analyzers.Native/StellaOps.Scanner.Analyzers.Native.csproj - APPLY | | 2248 | AUDIT-0750-M | DONE | Revalidated 2026-01-12 | Guild | src/Scanner/StellaOps.Scanner.Sbomer.BuildXPlugin/StellaOps.Scanner.Sbomer.BuildXPlugin.csproj - MAINT | | 2249 | AUDIT-0750-T | DONE | Revalidated 2026-01-12 | Guild | src/Scanner/StellaOps.Scanner.Sbomer.BuildXPlugin/StellaOps.Scanner.Sbomer.BuildXPlugin.csproj - TEST | -| 2250 | AUDIT-0750-A | TODO | Approved 2026-01-12 | Guild | src/Scanner/StellaOps.Scanner.Sbomer.BuildXPlugin/StellaOps.Scanner.Sbomer.BuildXPlugin.csproj - APPLY | +| 2250 | AUDIT-0750-A | DONE | Applied 2026-01-13 | Guild | src/Scanner/StellaOps.Scanner.Sbomer.BuildXPlugin/StellaOps.Scanner.Sbomer.BuildXPlugin.csproj - APPLY | | 2251 | AUDIT-0751-M | DONE | Revalidated 2026-01-12 | Guild | src/Scanner/StellaOps.Scanner.WebService/StellaOps.Scanner.WebService.csproj - MAINT | | 2252 | AUDIT-0751-T | DONE | Revalidated 2026-01-12 | Guild | src/Scanner/StellaOps.Scanner.WebService/StellaOps.Scanner.WebService.csproj - TEST | -| 2253 | AUDIT-0751-A | TODO | Approved 2026-01-12 | Guild | src/Scanner/StellaOps.Scanner.WebService/StellaOps.Scanner.WebService.csproj - APPLY | +| 2253 | AUDIT-0751-A | DONE | Applied 2026-01-13 | Guild | src/Scanner/StellaOps.Scanner.WebService/StellaOps.Scanner.WebService.csproj - APPLY | | 2254 | AUDIT-0752-M | DONE | Revalidated 2026-01-12 | Guild | src/Scanner/StellaOps.Scanner.Worker/StellaOps.Scanner.Worker.csproj - MAINT | | 2255 | AUDIT-0752-T | DONE | Revalidated 2026-01-12 | Guild | src/Scanner/StellaOps.Scanner.Worker/StellaOps.Scanner.Worker.csproj - TEST | | 2256 | AUDIT-0752-A | TODO | Approved 2026-01-12 | Guild | src/Scanner/StellaOps.Scanner.Worker/StellaOps.Scanner.Worker.csproj - APPLY | @@ -2863,7 +2863,7 @@ Bulk task definitions (applies to every project row below): | 2838 | AUDIT-0945-A | TODO | Approved 2026-01-12 | Guild | src/Scanner/__Libraries/StellaOps.Scanner.ChangeTrace/StellaOps.Scanner.ChangeTrace.csproj - APPLY | | 2839 | AUDIT-0946-M | DONE | Revalidated 2026-01-12 | Guild | src/Scanner/__Libraries/StellaOps.Scanner.Contracts/StellaOps.Scanner.Contracts.csproj - MAINT | | 2840 | AUDIT-0946-T | DONE | Revalidated 2026-01-12 | Guild | src/Scanner/__Libraries/StellaOps.Scanner.Contracts/StellaOps.Scanner.Contracts.csproj - TEST | -| 2841 | AUDIT-0946-A | TODO | Approved 2026-01-12 | Guild | src/Scanner/__Libraries/StellaOps.Scanner.Contracts/StellaOps.Scanner.Contracts.csproj - APPLY | +| 2841 | AUDIT-0946-A | DONE | Applied 2026-01-12; safe JSON encoder; sink patterns are string literals only | Guild | src/Scanner/__Libraries/StellaOps.Scanner.Contracts/StellaOps.Scanner.Contracts.csproj - APPLY | | 2842 | AUDIT-0947-M | DONE | Revalidated 2026-01-12 | Guild | src/Scanner/__Libraries/StellaOps.Scanner.PatchVerification/StellaOps.Scanner.PatchVerification.csproj - MAINT | | 2843 | AUDIT-0947-T | DONE | Revalidated 2026-01-12 | Guild | src/Scanner/__Libraries/StellaOps.Scanner.PatchVerification/StellaOps.Scanner.PatchVerification.csproj - TEST | | 2844 | AUDIT-0947-A | TODO | Approved 2026-01-12 | Guild | src/Scanner/__Libraries/StellaOps.Scanner.PatchVerification/StellaOps.Scanner.PatchVerification.csproj - APPLY | @@ -3074,11 +3074,16 @@ Bulk task definitions (applies to every project row below): ## Execution Log | Date (UTC) | Update | Owner | | --- | --- | --- | +| 2026-01-12 | Applied Scanner.Contracts hotlist: removed unsafe JSON encoder usage; confirmed Process.Start/BinaryFormatter hits are sink pattern literals; updated tests. | Project Mgmt | +| 2026-01-12 | Applied DotNet analyzer hotlist: invariant culture bundling metadata, TimeProvider injection for callgraph, XML resolver disabled, deterministic test updates and new tests. | Project Mgmt | | 2026-01-12 | Added Doctor.WebService audit rows and findings; synced Doctor web service project into src/StellaOps.sln. | Project Mgmt | | 2026-01-12 | Added Doctor.Tests audit rows and findings, updated Doctor core test coverage note, and synced the new test project into src/StellaOps.sln. | Project Mgmt | | 2026-01-12 | Added 19 Doctor projects to the audit tracker and recorded findings for new csproj entries. | Project Mgmt | | 2026-01-12 | Synced src/StellaOps.sln with 139 missing csproj entries. | Project Mgmt | | 2026-01-12 | Archived audit report and maint/test sprint to docs-archived/implplan/2025-12-29-csproj-audit; updated references and created pending apply sprint SPRINT_20260112_003_BE_csproj_audit_pending_apply.md. | Project Mgmt | +| 2026-01-13 | Applied ExportCenter.WebService hotlist (AUDIT-0337-A/AUDIT-0475-A): determinism, DI guards, retention/TLS gating, tests. | Project Mgmt | +| 2026-01-13 | Applied Scanner.Reachability hotlist (AUDIT-0681-A): DSSE PAE/canon, deterministic IDs, cancellation propagation, invariant formatting, tests. | Project Mgmt | +| 2026-01-13 | Applied Evidence hotlist (AUDIT-0082-A/AUDIT-0279-A): determinism, schema validation, budgets, retention, tests. | Project Mgmt | | 2026-01-12 | Approved all pending APPLY tasks; updated tracker entries to Approved 2026-01-12. | Project Mgmt | | 2026-01-12 | Added Apply Status Summary to the audit report and created sprint `docs-archived/implplan/2026-01-12-csproj-audit-apply-backlog/SPRINT_20260112_002_BE_csproj_audit_apply_backlog.md` for pending APPLY backlog. | Project Mgmt | | 2026-01-12 | Added production test and reuse gap inventories to the audit report to complete per-project audit coverage. | Project Mgmt | @@ -5189,7 +5194,7 @@ Bulk task definitions (applies to every project row below): | 834 | AUDIT-0278-A | DONE | Waived (test project; revalidated 2026-01-07) | Guild | src/__Analyzers/StellaOps.Determinism.Analyzers.Tests/StellaOps.Determinism.Analyzers.Tests.csproj - APPLY | | 835 | AUDIT-0279-M | DONE | Revalidated 2026-01-07 | Guild | src/__Libraries/StellaOps.Evidence/StellaOps.Evidence.csproj - MAINT | | 836 | AUDIT-0279-T | DONE | Revalidated 2026-01-07 | Guild | src/__Libraries/StellaOps.Evidence/StellaOps.Evidence.csproj - TEST | -| 837 | AUDIT-0279-A | TODO | Revalidated 2026-01-07 (open findings) | Guild | src/__Libraries/StellaOps.Evidence/StellaOps.Evidence.csproj - APPLY | +| 837 | AUDIT-0279-A | DONE | Applied 2026-01-13 | Guild | src/__Libraries/StellaOps.Evidence/StellaOps.Evidence.csproj - APPLY | | 838 | AUDIT-0280-M | DONE | Revalidated 2026-01-07 | Guild | src/__Libraries/StellaOps.Evidence.Bundle/StellaOps.Evidence.Bundle.csproj - MAINT | | 839 | AUDIT-0280-T | DONE | Revalidated 2026-01-07 | Guild | src/__Libraries/StellaOps.Evidence.Bundle/StellaOps.Evidence.Bundle.csproj - TEST | | 840 | AUDIT-0280-A | TODO | Revalidated 2026-01-07 (open findings) | Guild | src/__Libraries/StellaOps.Evidence.Bundle/StellaOps.Evidence.Bundle.csproj - APPLY | @@ -5363,7 +5368,7 @@ Bulk task definitions (applies to every project row below): | 1008 | AUDIT-0336-A | DONE | Waived (test project; revalidated 2026-01-07) | Guild | src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/StellaOps.ExportCenter.Tests.csproj - APPLY | | 1009 | AUDIT-0337-M | DONE | Revalidated 2026-01-07 | Guild | src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/StellaOps.ExportCenter.WebService.csproj - MAINT | | 1010 | AUDIT-0337-T | DONE | Revalidated 2026-01-07 | Guild | src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/StellaOps.ExportCenter.WebService.csproj - TEST | -| 1011 | AUDIT-0337-A | TODO | Revalidated 2026-01-07 (open findings) | Guild | src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/StellaOps.ExportCenter.WebService.csproj - APPLY | +| 1011 | AUDIT-0337-A | DONE | Applied 2026-01-13; determinism, DI guards, retention/TLS gating, tests added | Guild | src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/StellaOps.ExportCenter.WebService.csproj - APPLY | | 1012 | AUDIT-0338-M | DONE | Revalidated 2026-01-07 | Guild | src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Worker/StellaOps.ExportCenter.Worker.csproj - MAINT | | 1013 | AUDIT-0338-T | DONE | Revalidated 2026-01-07 | Guild | src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Worker/StellaOps.ExportCenter.Worker.csproj - TEST | | 1014 | AUDIT-0338-A | TODO | Revalidated 2026-01-07 (open findings) | Guild | src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Worker/StellaOps.ExportCenter.Worker.csproj - APPLY | @@ -5924,7 +5929,7 @@ Bulk task definitions (applies to every project row below): | 1566 | AUDIT-0522-A | DONE | Waived (test project; revalidated 2026-01-07) | Guild | src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.Bun.Tests/StellaOps.Scanner.Analyzers.Lang.Bun.Tests.csproj - APPLY | | 1567 | AUDIT-0523-M | DONE | Revalidated 2026-01-07 | Guild | src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Deno/StellaOps.Scanner.Analyzers.Lang.Deno.csproj - MAINT | | 1568 | AUDIT-0523-T | DONE | Revalidated 2026-01-07 | Guild | src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Deno/StellaOps.Scanner.Analyzers.Lang.Deno.csproj - TEST | -| 1569 | AUDIT-0523-A | TODO | Revalidated 2026-01-07 (open findings) | Guild | src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Deno/StellaOps.Scanner.Analyzers.Lang.Deno.csproj - APPLY | +| 1569 | AUDIT-0523-A | DONE | Applied 2026-01-13; superseded by AUDIT-0643-A | Guild | src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Deno/StellaOps.Scanner.Analyzers.Lang.Deno.csproj - APPLY | | 1570 | AUDIT-0524-M | DONE | Revalidated 2026-01-07 | Guild | src/Scanner/__Benchmarks/StellaOps.Scanner.Analyzers.Lang.Deno.Benchmarks/StellaOps.Scanner.Analyzers.Lang.Deno.Benchmarks.csproj - MAINT | | 1571 | AUDIT-0524-T | DONE | Revalidated 2026-01-07 | Guild | src/Scanner/__Benchmarks/StellaOps.Scanner.Analyzers.Lang.Deno.Benchmarks/StellaOps.Scanner.Analyzers.Lang.Deno.Benchmarks.csproj - TEST | | 1572 | AUDIT-0524-A | DONE | Waived (benchmark project; revalidated 2026-01-07) | Guild | src/Scanner/__Benchmarks/StellaOps.Scanner.Analyzers.Lang.Deno.Benchmarks/StellaOps.Scanner.Analyzers.Lang.Deno.Benchmarks.csproj - APPLY | @@ -5933,7 +5938,7 @@ Bulk task definitions (applies to every project row below): | 1575 | AUDIT-0525-A | DONE | Waived (test project; revalidated 2026-01-07) | Guild | src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.Deno.Tests/StellaOps.Scanner.Analyzers.Lang.Deno.Tests.csproj - APPLY | | 1576 | AUDIT-0526-M | DONE | Revalidated 2026-01-07 | Guild | src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.DotNet/StellaOps.Scanner.Analyzers.Lang.DotNet.csproj - MAINT | | 1577 | AUDIT-0526-T | DONE | Revalidated 2026-01-07 | Guild | src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.DotNet/StellaOps.Scanner.Analyzers.Lang.DotNet.csproj - TEST | -| 1578 | AUDIT-0526-A | TODO | Revalidated 2026-01-07 (open findings) | Guild | src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.DotNet/StellaOps.Scanner.Analyzers.Lang.DotNet.csproj - APPLY | +| 1578 | AUDIT-0526-A | DONE | Applied 2026-01-12; superseded by AUDIT-0644-A | Guild | src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.DotNet/StellaOps.Scanner.Analyzers.Lang.DotNet.csproj - APPLY | | 1579 | AUDIT-0527-M | DONE | Waived (test project; revalidated 2026-01-07) | Guild | src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.DotNet.Tests/StellaOps.Scanner.Analyzers.Lang.DotNet.Tests.csproj - MAINT | | 1580 | AUDIT-0527-T | DONE | Waived (test project; revalidated 2026-01-07) | Guild | src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.DotNet.Tests/StellaOps.Scanner.Analyzers.Lang.DotNet.Tests.csproj - TEST | | 1581 | AUDIT-0527-A | DONE | Waived (test project; revalidated 2026-01-07) | Guild | src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.DotNet.Tests/StellaOps.Scanner.Analyzers.Lang.DotNet.Tests.csproj - APPLY | @@ -6131,7 +6136,7 @@ Bulk task definitions (applies to every project row below): | 1773 | AUDIT-0591-A | DONE | Waived (test project; revalidated 2026-01-08) | Guild | src/Scanner/__Tests/StellaOps.Scanner.Queue.Tests/StellaOps.Scanner.Queue.Tests.csproj - APPLY | | 1774 | AUDIT-0592-M | DONE | Revalidated 2026-01-08 | Guild | src/Scanner/__Libraries/StellaOps.Scanner.Reachability/StellaOps.Scanner.Reachability.csproj - MAINT | | 1775 | AUDIT-0592-T | DONE | Revalidated 2026-01-08 | Guild | src/Scanner/__Libraries/StellaOps.Scanner.Reachability/StellaOps.Scanner.Reachability.csproj - TEST | -| 1776 | AUDIT-0592-A | TODO | Revalidated 2026-01-08 (open findings) | Guild | src/Scanner/__Libraries/StellaOps.Scanner.Reachability/StellaOps.Scanner.Reachability.csproj - APPLY | +| 1776 | AUDIT-0592-A | DONE | Applied 2026-01-13; DSSE PAE/canon, determinism, cancellation, invariant outputs, tests | Guild | src/Scanner/__Libraries/StellaOps.Scanner.Reachability/StellaOps.Scanner.Reachability.csproj - APPLY | | 1777 | AUDIT-0593-M | DONE | Revalidated 2026-01-08 | Guild | src/Scanner/__Tests/StellaOps.Scanner.Reachability.Stack.Tests/StellaOps.Scanner.Reachability.Stack.Tests.csproj - MAINT | | 1778 | AUDIT-0593-T | DONE | Revalidated 2026-01-08 | Guild | src/Scanner/__Tests/StellaOps.Scanner.Reachability.Stack.Tests/StellaOps.Scanner.Reachability.Stack.Tests.csproj - TEST | | 1779 | AUDIT-0593-A | DONE | Waived (test project; revalidated 2026-01-08) | Guild | src/Scanner/__Tests/StellaOps.Scanner.Reachability.Stack.Tests/StellaOps.Scanner.Reachability.Stack.Tests.csproj - APPLY | @@ -6146,7 +6151,7 @@ Bulk task definitions (applies to every project row below): | 1788 | AUDIT-0596-A | DONE | Waived (test project; revalidated 2026-01-08) | Guild | src/Scanner/__Tests/StellaOps.Scanner.ReachabilityDrift.Tests/StellaOps.Scanner.ReachabilityDrift.Tests.csproj - APPLY | | 1789 | AUDIT-0597-M | DONE | Revalidated 2026-01-08 | Guild | src/Scanner/StellaOps.Scanner.Sbomer.BuildXPlugin/StellaOps.Scanner.Sbomer.BuildXPlugin.csproj - MAINT | | 1790 | AUDIT-0597-T | DONE | Revalidated 2026-01-08 | Guild | src/Scanner/StellaOps.Scanner.Sbomer.BuildXPlugin/StellaOps.Scanner.Sbomer.BuildXPlugin.csproj - TEST | -| 1791 | AUDIT-0597-A | TODO | Revalidated 2026-01-08 (open findings) | Guild | src/Scanner/StellaOps.Scanner.Sbomer.BuildXPlugin/StellaOps.Scanner.Sbomer.BuildXPlugin.csproj - APPLY | +| 1791 | AUDIT-0597-A | DONE | Applied 2026-01-13 | Guild | src/Scanner/StellaOps.Scanner.Sbomer.BuildXPlugin/StellaOps.Scanner.Sbomer.BuildXPlugin.csproj - APPLY | | 1792 | AUDIT-0598-M | DONE | Revalidated 2026-01-08 | Guild | src/Scanner/__Tests/StellaOps.Scanner.Sbomer.BuildXPlugin.Tests/StellaOps.Scanner.Sbomer.BuildXPlugin.Tests.csproj - MAINT | | 1793 | AUDIT-0598-T | DONE | Revalidated 2026-01-08 | Guild | src/Scanner/__Tests/StellaOps.Scanner.Sbomer.BuildXPlugin.Tests/StellaOps.Scanner.Sbomer.BuildXPlugin.Tests.csproj - TEST | | 1794 | AUDIT-0598-A | DONE | Waived (test project; revalidated 2026-01-08) | Guild | src/Scanner/__Tests/StellaOps.Scanner.Sbomer.BuildXPlugin.Tests/StellaOps.Scanner.Sbomer.BuildXPlugin.Tests.csproj - APPLY | @@ -6215,7 +6220,7 @@ Bulk task definitions (applies to every project row below): | 1857 | AUDIT-0619-A | DONE | Waived (test project; revalidated 2026-01-08) | Guild | src/Scanner/__Libraries/StellaOps.Scanner.VulnSurfaces.Tests/StellaOps.Scanner.VulnSurfaces.Tests.csproj - APPLY | | 1858 | AUDIT-0620-M | DONE | Revalidated 2026-01-08 | Guild | src/Scanner/StellaOps.Scanner.WebService/StellaOps.Scanner.WebService.csproj - MAINT | | 1859 | AUDIT-0620-T | DONE | Revalidated 2026-01-08 | Guild | src/Scanner/StellaOps.Scanner.WebService/StellaOps.Scanner.WebService.csproj - TEST | -| 1860 | AUDIT-0620-A | TODO | Revalidated 2026-01-08 (open findings) | Guild | src/Scanner/StellaOps.Scanner.WebService/StellaOps.Scanner.WebService.csproj - APPLY | +| 1860 | AUDIT-0620-A | DONE | Applied 2026-01-13 | Guild | src/Scanner/StellaOps.Scanner.WebService/StellaOps.Scanner.WebService.csproj - APPLY | | 1861 | AUDIT-0621-M | DONE | Revalidated 2026-01-08 | Guild | src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/StellaOps.Scanner.WebService.Tests.csproj - MAINT | | 1862 | AUDIT-0621-T | DONE | Revalidated 2026-01-08 | Guild | src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/StellaOps.Scanner.WebService.Tests.csproj - TEST | | 1863 | AUDIT-0621-A | DONE | Waived (test project; revalidated 2026-01-08) | Guild | src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/StellaOps.Scanner.WebService.Tests.csproj - APPLY | @@ -8080,17 +8085,3 @@ Bulk task definitions (applies to every project row below): ## Next Checkpoints - TBD: Rebaseline inventory review (repo-wide csproj list) and tranche scheduling. - TBD: Audit report review and approval checkpoint. - - - - - - - - - - - - - - diff --git a/docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229_049_BE_csproj_audit_report.md b/docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229_049_BE_csproj_audit_report.md index 82399e073..bc379c7c0 100644 --- a/docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229_049_BE_csproj_audit_report.md +++ b/docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229_049_BE_csproj_audit_report.md @@ -2573,7 +2573,7 @@ - TEST: Coverage exists in src/__Libraries/__Tests/StellaOps.Evidence.Tests for EvidenceIndex serialization, validation, query summary, and budget checks. - TEST: Missing tests for EvidenceIndexValidator error paths (digest mismatch, invalid signatures, missing unknowns), EvidenceLinker ordering/determinism, retention tier migration/restore, and schema loading/validation. - Proposed changes (pending approval): inject deterministic ID/time providers and sort evidence collections before digesting; align GetAttestationsForSbom to use sbomDigest or remove the parameter; make GetCurrentUsage async; stabilize pruning order and use invariant formatting in budget issues; remove UnsafeRelaxedJsonEscaping from canonicalization pipeline; implement or guard compression; add schema validation or remove the unused schema loader; remove non-ASCII comment glyphs; remove committed bin/obj artifacts or update gitignore; add tests for validator errors, linker determinism, retention flows, schema validation, and pruning order. -- Disposition: revalidated 2026-01-08 (open findings) +- Disposition: applied 2026-01-13 ### src/__Libraries/StellaOps.Evidence.Bundle/StellaOps.Evidence.Bundle.csproj - MAINT: EvidenceBundle uses Guid.NewGuid for BundleId; bundles are nondeterministic even when other fields are stable. `src/__Libraries/StellaOps.Evidence.Bundle/EvidenceBundle.cs` - MAINT: EvidenceBundleBuilder does not allow overriding BundleId; deterministic bundle IDs cannot be injected for tests or replay. `src/__Libraries/StellaOps.Evidence.Bundle/EvidenceBundle.cs`, `src/__Libraries/StellaOps.Evidence.Bundle/EvidenceBundleBuilder.cs` @@ -4575,8 +4575,8 @@ - QUALITY: Runtime shim orders events using localeCompare with the default locale; NDJSON ordering (and hashes) can differ across locales. `src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Deno/Internal/Runtime/DenoRuntimeShim.cs` - MAINT: DenoRuntimeTraceRecorder defaults to TimeProvider.System; timestamps are nondeterministic unless callers inject a TimeProvider or explicit timestamps. `src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Deno/Internal/Runtime/DenoRuntimeTraceRecorder.cs` - TEST: Runtime runner tests do not cover entrypoint path containment or binary allowlist enforcement. `src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.Deno.Tests/Deno/DenoRuntimeTraceRunnerTests.cs` -- Proposed changes (pending approval): validate entrypoint paths and restrict binary selection, scope Deno permissions, use ordinal comparisons in the shim, inject TimeProvider, and add tests for root containment/allowlist behavior. -- Disposition: pending implementation (non-test project; revalidated 2026-01-07; apply recommendations remain open). +- Applied changes: validated entrypoint paths and binary allowlist, scoped allow-read, switched shim ordering to ordinal compares, required TimeProvider injection, replaced UnsafeRelaxedJsonEscaping, and added tests for root containment/allowlist behavior. +- Disposition: applied 2026-01-13; apply recommendations closed. ### src/Scanner/__Benchmarks/StellaOps.Scanner.Gate.Benchmarks/StellaOps.Scanner.Gate.Benchmarks.csproj - MAINT: GenerateFindings allocates a Random that is never used; this triggers a warning with TreatWarningsAsErrors and should be removed or used. `src/Scanner/__Benchmarks/StellaOps.Scanner.Gate.Benchmarks/VexGateBenchmarks.cs` - MAINT: Evaluate_NoRuleMatch allocates evidence per iteration, so benchmark timings include setup/allocation overhead instead of only evaluation cost. `src/Scanner/__Benchmarks/StellaOps.Scanner.Gate.Benchmarks/VexGateBenchmarks.cs` @@ -4615,8 +4615,8 @@ ### src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.Deno.Tests/StellaOps.Scanner.Analyzers.Lang.Deno.Tests.csproj - MAINT: Test project sets TreatWarningsAsErrors=false. `src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.Deno.Tests/StellaOps.Scanner.Analyzers.Lang.Deno.Tests.csproj` - MAINT: Tests use Guid.NewGuid for temp roots and CancellationToken.None for execution. `src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.Deno.Tests/TestUtilities/TestPaths.cs` `src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.Deno.Tests/Deno/DenoRuntimeTraceRunnerTests.cs` -- Proposed changes (optional): use deterministic temp paths/tokens and enable warnings-as-errors. -- Disposition: waived (test project; revalidated 2026-01-07). +- Applied changes: deterministic temp paths/tokens, allowlist/root tests, safe JSON encoding, newline normalization; warnings-as-errors remains waived. +- Disposition: waived (test project; determinism/security fixes applied 2026-01-13). ### src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.DotNet/StellaOps.Scanner.Analyzers.Lang.DotNet.csproj - MAINT: Bundling signal metadata formats SizeBytes/EstimatedBundledAssemblies with ToString() without InvariantCulture, producing culture-dependent output. `src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.DotNet/Internal/Bundling/DotNetBundlingSignalCollector.cs` - MAINT: DotNetCallgraphBuilder defaults to TimeProvider.System, making reachability metadata timestamps nondeterministic unless injected. `src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.DotNet/Internal/Callgraph/DotNetCallgraphBuilder.cs` @@ -4986,7 +4986,7 @@ - QUALITY: Numeric/time outputs use `ToString()` without InvariantCulture (union writer timestamps, edge bundle generated_at, semantic score/cwe formatting, PR summary metrics). `src/Scanner/__Libraries/StellaOps.Scanner.Reachability/ReachabilityUnionWriter.cs` `src/Scanner/__Libraries/StellaOps.Scanner.Reachability/EdgeBundlePublisher.cs` `src/Scanner/__Libraries/StellaOps.Scanner.Reachability/RichGraphSemanticExtensions.cs` `src/Scanner/__Libraries/StellaOps.Scanner.Reachability/Cache/PrReachabilityGate.cs` - QUALITY: PR summary markdown includes non-ASCII/mojibake symbols. `src/Scanner/__Libraries/StellaOps.Scanner.Reachability/Cache/PrReachabilityGate.cs` - TEST: No tests validate DSSE PAE/canonicalization for witness/suppression signing. `src/Scanner/__Libraries/StellaOps.Scanner.Reachability/Witnesses/WitnessDsseSigner.cs` -- Disposition: revalidated 2026-01-08; apply recommendations remain open. +- Disposition: applied 2026-01-13; apply recommendations closed. ### src/Scanner/__Tests/StellaOps.Scanner.Reachability.Stack.Tests/StellaOps.Scanner.Reachability.Stack.Tests.csproj - MAINT: TreatWarningsAsErrors is not set for the test project. `src/Scanner/__Tests/StellaOps.Scanner.Reachability.Stack.Tests/StellaOps.Scanner.Reachability.Stack.Tests.csproj` - MAINT: Tests use DateTimeOffset.UtcNow and Guid.NewGuid, which reduces determinism. `src/Scanner/__Tests/StellaOps.Scanner.Reachability.Stack.Tests/ReachabilityStackEvaluatorTests.cs` `src/Scanner/__Tests/StellaOps.Scanner.Reachability.Stack.Tests/ReachabilityResultFactoryTests.cs` @@ -5011,11 +5011,12 @@ - MAINT: Attestor client is built with new HttpClient rather than IHttpClientFactory. `src/Scanner/StellaOps.Scanner.Sbomer.BuildXPlugin/Program.cs` - SECURITY: --attestor-insecure disables TLS validation; ensure explicit warnings and guardrails to avoid accidental use in production. `src/Scanner/StellaOps.Scanner.Sbomer.BuildXPlugin/Program.cs` - QUALITY: Console output uses a non-ASCII arrow glyph in the handshake message. `src/Scanner/StellaOps.Scanner.Sbomer.BuildXPlugin/Program.cs` -- Disposition: revalidated 2026-01-08; apply recommendations remain open. +- NOTE: CLI stdout/stderr output is part of the BuildX protocol; retained intentionally. +- Disposition: applied 2026-01-13; apply recommendations closed. ### src/Scanner/__Tests/StellaOps.Scanner.Sbomer.BuildXPlugin.Tests/StellaOps.Scanner.Sbomer.BuildXPlugin.Tests.csproj - MAINT: TreatWarningsAsErrors is not set for the test project. `src/Scanner/__Tests/StellaOps.Scanner.Sbomer.BuildXPlugin.Tests/StellaOps.Scanner.Sbomer.BuildXPlugin.Tests.csproj` - MAINT: Tests use Guid.NewGuid, DateTimeOffset.UtcNow, and CancellationToken.None for temp roots and fixtures, which makes runs nondeterministic. `src/Scanner/__Tests/StellaOps.Scanner.Sbomer.BuildXPlugin.Tests/TestUtilities/TempDirectory.cs` `src/Scanner/__Tests/StellaOps.Scanner.Sbomer.BuildXPlugin.Tests/Attestation/AttestorClientTests.cs` `src/Scanner/__Tests/StellaOps.Scanner.Sbomer.BuildXPlugin.Tests/Surface/SurfaceManifestWriterTests.cs` -- Disposition: waived (test project; revalidated 2026-01-08). +- Disposition: waived (test project; determinism fixes applied 2026-01-13). ### src/Scanner/__Libraries/StellaOps.Scanner.SmartDiff/StellaOps.Scanner.SmartDiff.csproj - MAINT: EPSS threshold text and score formatting use current culture (P0/F4), making change reasons locale-dependent. `src/Scanner/__Libraries/StellaOps.Scanner.SmartDiff/Detection/MaterialRiskChangeDetector.cs` - QUALITY: SmartDiffJsonSerializer uses JsonSerializerDefaults.Web and camelCase instead of the shared RFC 8785 canonicalizer for predicate output. `src/Scanner/__Libraries/StellaOps.Scanner.SmartDiff/SmartDiffJsonSerializer.cs` @@ -5130,12 +5131,12 @@ - QUALITY: Orchestrator event serialization uses UnsafeRelaxedJsonEscaping and non-canonical JSON for deterministic outputs. `src/Scanner/StellaOps.Scanner.WebService/Serialization/OrchestratorEventSerializer.cs` - QUALITY: Surface manifest digest is computed from JsonSerializerDefaults.Web output instead of canonical JSON. `src/Scanner/StellaOps.Scanner.WebService/Services/SurfacePointerService.cs` - TEST: Coverage review continues in AUDIT-0621 (Scanner.WebService.Tests). -- Disposition: revalidated 2026-01-08; apply recommendations remain open. +- Disposition: applied 2026-01-13; apply recommendations closed. ### src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/StellaOps.Scanner.WebService.Tests.csproj - MAINT: Tests use Guid.NewGuid, DateTimeOffset.UtcNow, DateTime.UtcNow, and Random.Shared across fixtures, making runs nondeterministic. `src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/Benchmarks/TtfsPerformanceBenchmarks.cs` `src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/ManifestEndpointsTests.cs` `src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/UnifiedEvidenceServiceTests.cs` - MAINT: Tests use CancellationToken.None in async paths; cancellation handling is not exercised. `src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/ReportEventDispatcherTests.cs` `src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/RuntimeEndpointsTests.cs` - QUALITY: Non-ASCII glyphs appear in comments. `src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/Benchmarks/TtfsPerformanceBenchmarks.cs` `src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/Integration/ProofReplayWorkflowTests.cs` -- Disposition: waived (test project; revalidated 2026-01-08). +- Disposition: waived (test project; determinism fixes applied 2026-01-13). ### src/Scanner/StellaOps.Scanner.Worker/StellaOps.Scanner.Worker.csproj - MAINT: CancellationToken.None and blocking .Result are used in worker pipeline and signing paths; cancellation cannot propagate cleanly. `src/Scanner/StellaOps.Scanner.Worker/Hosting/ScannerWorkerHostedService.cs` `src/Scanner/StellaOps.Scanner.Worker/Processing/Surface/SurfaceManifestStageExecutor.cs` `src/Scanner/StellaOps.Scanner.Worker/Processing/Surface/HmacDsseEnvelopeSigner.cs` - SECURITY: DSSE PAE and envelope serialization are reimplemented locally; output is not spec-compliant or canonical. `src/Scanner/StellaOps.Scanner.Worker/Processing/Surface/HmacDsseEnvelopeSigner.cs` `src/Scanner/StellaOps.Scanner.Worker/Processing/Surface/IDsseEnvelopeSigner.cs` diff --git a/docs/implplan/SPRINT_20260112_001_000_INDEX_doctor_diagnostics.md b/docs-archived/implplan/2026-01-12-doctor-diagnostics/SPRINT_20260112_001_000_INDEX_doctor_diagnostics.md similarity index 98% rename from docs/implplan/SPRINT_20260112_001_000_INDEX_doctor_diagnostics.md rename to docs-archived/implplan/2026-01-12-doctor-diagnostics/SPRINT_20260112_001_000_INDEX_doctor_diagnostics.md index 2de717a45..1d18e03af 100644 --- a/docs/implplan/SPRINT_20260112_001_000_INDEX_doctor_diagnostics.md +++ b/docs-archived/implplan/2026-01-12-doctor-diagnostics/SPRINT_20260112_001_000_INDEX_doctor_diagnostics.md @@ -201,7 +201,7 @@ Examples: - [x] Export capability for support tickets - [x] Unit test coverage >= 85% - [x] Integration tests for all plugins -- [ ] Documentation in `docs/doctor/` (TODO) +- [x] Documentation in `docs/doctor/` --- @@ -248,6 +248,7 @@ Examples: | 12-Jan-2026 | Sprint 001_008 (FE Dashboard) completed - Angular 17+ standalone components | | 12-Jan-2026 | Sprint 001_009 (Self-service) completed - Export, Observability plugin | | 12-Jan-2026 | All 9 sprints complete - Doctor Diagnostics System fully implemented | +| 12-Jan-2026 | Documentation created in docs/doctor/ (README.md, cli-reference.md) | --- diff --git a/docs/implplan/SPRINT_20260112_001_001_DOCTOR_foundation.md b/docs-archived/implplan/2026-01-12-doctor-diagnostics/SPRINT_20260112_001_001_DOCTOR_foundation.md similarity index 100% rename from docs/implplan/SPRINT_20260112_001_001_DOCTOR_foundation.md rename to docs-archived/implplan/2026-01-12-doctor-diagnostics/SPRINT_20260112_001_001_DOCTOR_foundation.md diff --git a/docs/implplan/SPRINT_20260112_001_002_DOCTOR_core_plugin.md b/docs-archived/implplan/2026-01-12-doctor-diagnostics/SPRINT_20260112_001_002_DOCTOR_core_plugin.md similarity index 100% rename from docs/implplan/SPRINT_20260112_001_002_DOCTOR_core_plugin.md rename to docs-archived/implplan/2026-01-12-doctor-diagnostics/SPRINT_20260112_001_002_DOCTOR_core_plugin.md diff --git a/docs/implplan/SPRINT_20260112_001_003_DOCTOR_database_plugin.md b/docs-archived/implplan/2026-01-12-doctor-diagnostics/SPRINT_20260112_001_003_DOCTOR_database_plugin.md similarity index 100% rename from docs/implplan/SPRINT_20260112_001_003_DOCTOR_database_plugin.md rename to docs-archived/implplan/2026-01-12-doctor-diagnostics/SPRINT_20260112_001_003_DOCTOR_database_plugin.md diff --git a/docs/implplan/SPRINT_20260112_001_004_DOCTOR_service_security_plugins.md b/docs-archived/implplan/2026-01-12-doctor-diagnostics/SPRINT_20260112_001_004_DOCTOR_service_security_plugins.md similarity index 100% rename from docs/implplan/SPRINT_20260112_001_004_DOCTOR_service_security_plugins.md rename to docs-archived/implplan/2026-01-12-doctor-diagnostics/SPRINT_20260112_001_004_DOCTOR_service_security_plugins.md diff --git a/docs/implplan/SPRINT_20260112_001_005_DOCTOR_integration_plugins.md b/docs-archived/implplan/2026-01-12-doctor-diagnostics/SPRINT_20260112_001_005_DOCTOR_integration_plugins.md similarity index 100% rename from docs/implplan/SPRINT_20260112_001_005_DOCTOR_integration_plugins.md rename to docs-archived/implplan/2026-01-12-doctor-diagnostics/SPRINT_20260112_001_005_DOCTOR_integration_plugins.md diff --git a/docs/implplan/SPRINT_20260112_001_006_CLI_doctor_command.md b/docs-archived/implplan/2026-01-12-doctor-diagnostics/SPRINT_20260112_001_006_CLI_doctor_command.md similarity index 100% rename from docs/implplan/SPRINT_20260112_001_006_CLI_doctor_command.md rename to docs-archived/implplan/2026-01-12-doctor-diagnostics/SPRINT_20260112_001_006_CLI_doctor_command.md diff --git a/docs/implplan/SPRINT_20260112_001_007_API_doctor_endpoints.md b/docs-archived/implplan/2026-01-12-doctor-diagnostics/SPRINT_20260112_001_007_API_doctor_endpoints.md similarity index 100% rename from docs/implplan/SPRINT_20260112_001_007_API_doctor_endpoints.md rename to docs-archived/implplan/2026-01-12-doctor-diagnostics/SPRINT_20260112_001_007_API_doctor_endpoints.md diff --git a/docs/implplan/SPRINT_20260112_001_008_FE_doctor_dashboard.md b/docs-archived/implplan/2026-01-12-doctor-diagnostics/SPRINT_20260112_001_008_FE_doctor_dashboard.md similarity index 100% rename from docs/implplan/SPRINT_20260112_001_008_FE_doctor_dashboard.md rename to docs-archived/implplan/2026-01-12-doctor-diagnostics/SPRINT_20260112_001_008_FE_doctor_dashboard.md diff --git a/docs/implplan/SPRINT_20260112_001_009_DOCTOR_self_service.md b/docs-archived/implplan/2026-01-12-doctor-diagnostics/SPRINT_20260112_001_009_DOCTOR_self_service.md similarity index 100% rename from docs/implplan/SPRINT_20260112_001_009_DOCTOR_self_service.md rename to docs-archived/implplan/2026-01-12-doctor-diagnostics/SPRINT_20260112_001_009_DOCTOR_self_service.md diff --git a/docs/dev/extending-binary-analysis.md b/docs/dev/extending-binary-analysis.md new file mode 100644 index 000000000..6b6b70fd4 --- /dev/null +++ b/docs/dev/extending-binary-analysis.md @@ -0,0 +1,569 @@ +# Extending Binary Analysis + +This guide explains how to add support for new binary formats or custom section extractors to the binary diff attestation system. + +## Overview + +The binary analysis system is designed for extensibility. You can add support for: + +- **New binary formats** (PE, Mach-O, WebAssembly) +- **Custom section extractors** (additional ELF sections, custom hash algorithms) +- **Verdict classifiers** (custom backport detection logic) + +## Architecture + +### Core Interfaces + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Binary Analysis Pipeline │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ IBinaryFormatDetector ──▶ ISectionHashExtractor │ +│ │ │ │ +│ ▼ ▼ │ +│ BinaryFormat enum SectionHashSet │ +│ (elf, pe, macho) (per-format) │ +│ │ │ +│ ▼ │ +│ IVerdictClassifier │ +│ │ │ +│ ▼ │ +│ BinaryDiffFinding │ +│ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### Key Interfaces + +```csharp +/// +/// Detects binary format from file magic/headers. +/// +public interface IBinaryFormatDetector +{ + BinaryFormat Detect(ReadOnlySpan header); + BinaryFormat DetectFromPath(string filePath); +} + +/// +/// Extracts section hashes for a specific binary format. +/// +public interface ISectionHashExtractor where TConfig : class +{ + BinaryFormat SupportedFormat { get; } + + Task ExtractAsync( + string filePath, + TConfig? config = null, + CancellationToken cancellationToken = default); + + Task ExtractFromBytesAsync( + ReadOnlyMemory bytes, + string virtualPath, + TConfig? config = null, + CancellationToken cancellationToken = default); +} + +/// +/// Classifies binary changes as patched/vanilla/unknown. +/// +public interface IVerdictClassifier +{ + Verdict Classify(SectionHashSet? baseHashes, SectionHashSet? targetHashes); + double ComputeConfidence(SectionHashSet? baseHashes, SectionHashSet? targetHashes); +} +``` + +## Adding a New Binary Format + +### Step 1: Define Configuration + +```csharp +// src/Scanner/__Libraries/StellaOps.Scanner.Contracts/PeSectionConfig.cs + +namespace StellaOps.Scanner.Contracts; + +/// +/// Configuration for PE section hash extraction. +/// +public sealed record PeSectionConfig +{ + /// Sections to extract hashes from. + public ImmutableArray Sections { get; init; } = [".text", ".rdata", ".data", ".rsrc"]; + + /// Hash algorithms to use. + public ImmutableArray HashAlgorithms { get; init; } = ["sha256"]; + + /// Maximum section size to process (bytes). + public long MaxSectionSize { get; init; } = 100 * 1024 * 1024; // 100MB + + /// Whether to extract version resources. + public bool ExtractVersionInfo { get; init; } = true; +} +``` + +### Step 2: Implement the Extractor + +```csharp +// src/Scanner/StellaOps.Scanner.Analyzers.Native/Hardening/PeSectionHashExtractor.cs + +namespace StellaOps.Scanner.Analyzers.Native; + +public sealed class PeSectionHashExtractor : ISectionHashExtractor +{ + private readonly TimeProvider _timeProvider; + private readonly ILogger _logger; + + public PeSectionHashExtractor( + TimeProvider timeProvider, + ILogger logger) + { + _timeProvider = timeProvider; + _logger = logger; + } + + public BinaryFormat SupportedFormat => BinaryFormat.Pe; + + public async Task ExtractAsync( + string filePath, + PeSectionConfig? config = null, + CancellationToken cancellationToken = default) + { + config ??= new PeSectionConfig(); + + // Read file + var bytes = await File.ReadAllBytesAsync(filePath, cancellationToken); + return await ExtractFromBytesAsync(bytes, filePath, config, cancellationToken); + } + + public async Task ExtractFromBytesAsync( + ReadOnlyMemory bytes, + string virtualPath, + PeSectionConfig? config = null, + CancellationToken cancellationToken = default) + { + config ??= new PeSectionConfig(); + + // Validate PE magic + if (!IsPeFile(bytes.Span)) + { + _logger.LogDebug("Not a PE file: {Path}", virtualPath); + return null; + } + + try + { + var sections = new Dictionary(); + + // Parse PE headers + using var peReader = new PEReader(new MemoryStream(bytes.ToArray())); + + foreach (var sectionHeader in peReader.PEHeaders.SectionHeaders) + { + var sectionName = sectionHeader.Name; + + if (!config.Sections.Contains(sectionName)) + continue; + + if (sectionHeader.SizeOfRawData > config.MaxSectionSize) + { + _logger.LogWarning( + "Section {Section} exceeds max size ({Size} > {Max})", + sectionName, sectionHeader.SizeOfRawData, config.MaxSectionSize); + continue; + } + + // Get section data + var sectionData = peReader.GetSectionData(sectionName); + if (sectionData.Length == 0) + continue; + + // Compute hash + var sha256 = ComputeSha256(sectionData.GetContent()); + + sections[sectionName] = new SectionInfo + { + Sha256 = sha256, + Size = sectionData.Length, + Offset = sectionHeader.PointerToRawData + }; + } + + // Compute file hash + var fileHash = ComputeSha256(bytes.Span); + + return new SectionHashSet + { + FilePath = virtualPath, + FileHash = fileHash, + Sections = sections.ToImmutableDictionary(), + ExtractedAt = _timeProvider.GetUtcNow(), + ExtractorVersion = GetType().Assembly.GetName().Version?.ToString() ?? "1.0.0" + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to extract PE sections from {Path}", virtualPath); + return null; + } + } + + private static bool IsPeFile(ReadOnlySpan bytes) + { + // Check DOS header magic (MZ) + if (bytes.Length < 64) + return false; + + return bytes[0] == 0x4D && bytes[1] == 0x5A; // "MZ" + } + + private static string ComputeSha256(ReadOnlySpan data) + { + Span hash = stackalloc byte[32]; + SHA256.HashData(data, hash); + return Convert.ToHexString(hash).ToLowerInvariant(); + } +} +``` + +### Step 3: Register Services + +```csharp +// src/Scanner/StellaOps.Scanner.Analyzers.Native/ServiceCollectionExtensions.cs + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddNativeAnalyzers( + this IServiceCollection services, + IConfiguration configuration) + { + // Existing ELF extractor + services.AddSingleton(); + + // New PE extractor + services.AddSingleton, PeSectionHashExtractor>(); + + // Register in composite + services.AddSingleton(); + services.AddSingleton(sp => + { + var extractors = new Dictionary + { + [BinaryFormat.Elf] = sp.GetRequiredService(), + [BinaryFormat.Pe] = sp.GetRequiredService>() + }; + return new CompositeSectionHashExtractor(extractors); + }); + + // Configuration + services.AddOptions() + .Bind(configuration.GetSection("Scanner:Native:PeSections")) + .ValidateDataAnnotations() + .ValidateOnStart(); + + return services; + } +} +``` + +### Step 4: Add Tests + +```csharp +// src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Native.Tests/PeSectionHashExtractorTests.cs + +namespace StellaOps.Scanner.Analyzers.Native.Tests; + +public class PeSectionHashExtractorTests +{ + private readonly PeSectionHashExtractor _extractor; + private readonly FakeTimeProvider _timeProvider; + + public PeSectionHashExtractorTests() + { + _timeProvider = new FakeTimeProvider(new DateTimeOffset(2026, 1, 13, 12, 0, 0, TimeSpan.Zero)); + _extractor = new PeSectionHashExtractor( + _timeProvider, + NullLogger.Instance); + } + + [Fact] + public async Task ExtractAsync_ValidPe_ReturnsAllSections() + { + // Arrange + var pePath = "TestData/sample.exe"; + + // Act + var result = await _extractor.ExtractAsync(pePath); + + // Assert + Assert.NotNull(result); + Assert.Contains(".text", result.Sections.Keys); + Assert.Contains(".rdata", result.Sections.Keys); + Assert.NotEmpty(result.FileHash); + } + + [Fact] + public async Task ExtractAsync_NotPeFile_ReturnsNull() + { + // Arrange + var elfPath = "TestData/sample.elf"; + + // Act + var result = await _extractor.ExtractAsync(elfPath); + + // Assert + Assert.Null(result); + } + + [Fact] + public async Task ExtractAsync_Deterministic_SameOutput() + { + // Arrange + var pePath = "TestData/sample.exe"; + + // Act + var result1 = await _extractor.ExtractAsync(pePath); + var result2 = await _extractor.ExtractAsync(pePath); + + // Assert + Assert.Equal(result1!.FileHash, result2!.FileHash); + Assert.Equal(result1.Sections[".text"].Sha256, result2.Sections[".text"].Sha256); + } +} +``` + +## Adding Custom Section Analysis + +### Custom Hash Algorithm + +```csharp +public interface IHashAlgorithmProvider +{ + string Name { get; } + string ComputeHash(ReadOnlySpan data); +} + +public sealed class Blake3HashProvider : IHashAlgorithmProvider +{ + public string Name => "blake3"; + + public string ComputeHash(ReadOnlySpan data) + { + // Using Blake3 library + var hash = Blake3.Hasher.Hash(data); + return Convert.ToHexString(hash.AsSpan()).ToLowerInvariant(); + } +} +``` + +### Custom Verdict Classifier + +```csharp +public sealed class EnhancedVerdictClassifier : IVerdictClassifier +{ + private readonly ISymbolAnalyzer _symbolAnalyzer; + + public Verdict Classify(SectionHashSet? baseHashes, SectionHashSet? targetHashes) + { + if (baseHashes == null || targetHashes == null) + return Verdict.Unknown; + + // Check .text section change + var textChanged = HasSectionChanged(baseHashes, targetHashes, ".text"); + var symbolsChanged = HasSectionChanged(baseHashes, targetHashes, ".symtab"); + + // Custom logic: if .text changed but symbols are similar, likely a patch + if (textChanged && !symbolsChanged) + { + return Verdict.Patched; + } + + // If everything changed significantly, it's a vanilla update + if (textChanged && symbolsChanged) + { + return Verdict.Vanilla; + } + + return Verdict.Unknown; + } + + public double ComputeConfidence(SectionHashSet? baseHashes, SectionHashSet? targetHashes) + { + if (baseHashes == null || targetHashes == null) + return 0.0; + + // Compute similarity score + var matchingSections = 0; + var totalSections = 0; + + foreach (var (name, baseInfo) in baseHashes.Sections) + { + totalSections++; + if (targetHashes.Sections.TryGetValue(name, out var targetInfo)) + { + if (baseInfo.Sha256 == targetInfo.Sha256) + matchingSections++; + } + } + + if (totalSections == 0) + return 0.0; + + // Higher similarity = higher confidence in classification + return Math.Round((double)matchingSections / totalSections, 4, MidpointRounding.ToZero); + } + + private static bool HasSectionChanged(SectionHashSet baseHashes, SectionHashSet targetHashes, string section) + { + if (!baseHashes.Sections.TryGetValue(section, out var baseInfo)) + return false; + if (!targetHashes.Sections.TryGetValue(section, out var targetInfo)) + return true; + + return baseInfo.Sha256 != targetInfo.Sha256; + } +} +``` + +## Best Practices + +### 1. Determinism + +Always ensure deterministic output: + +```csharp +// BAD - Non-deterministic +public SectionHashSet Extract(string path) +{ + return new SectionHashSet + { + ExtractedAt = DateTimeOffset.UtcNow, // Non-deterministic! + // ... + }; +} + +// GOOD - Injected time provider +public SectionHashSet Extract(string path) +{ + return new SectionHashSet + { + ExtractedAt = _timeProvider.GetUtcNow(), // Deterministic + // ... + }; +} +``` + +### 2. Error Handling + +Handle malformed binaries gracefully: + +```csharp +public async Task ExtractAsync(string path, CancellationToken ct) +{ + try + { + // ... extraction logic + } + catch (BadImageFormatException ex) + { + _logger.LogDebug(ex, "Invalid binary format: {Path}", path); + return null; // Return null, don't throw + } + catch (IOException ex) + { + _logger.LogWarning(ex, "I/O error reading: {Path}", path); + return null; + } +} +``` + +### 3. Memory Management + +Stream large binaries instead of loading entirely: + +```csharp +public async Task ExtractLargeBinaryAsync( + string path, + CancellationToken ct) +{ + await using var stream = new FileStream( + path, + FileMode.Open, + FileAccess.Read, + FileShare.Read, + bufferSize: 81920, + useAsync: true); + + // Stream section data instead of loading all at once + // ... +} +``` + +### 4. Configuration Validation + +Validate configuration at startup: + +```csharp +public sealed class PeSectionConfigValidator : IValidateOptions +{ + public ValidateOptionsResult Validate(string? name, PeSectionConfig options) + { + if (options.Sections.Length == 0) + return ValidateOptionsResult.Fail("At least one section must be specified"); + + if (options.MaxSectionSize <= 0) + return ValidateOptionsResult.Fail("MaxSectionSize must be positive"); + + return ValidateOptionsResult.Success; + } +} +``` + +## Testing Guidelines + +### Golden File Tests + +```csharp +[Fact] +public async Task Extract_KnownBinary_MatchesGolden() +{ + // Arrange + var binaryPath = "TestData/known-binary.exe"; + var goldenPath = "TestData/known-binary.golden.json"; + + // Act + var result = await _extractor.ExtractAsync(binaryPath); + + // Assert + var expected = JsonSerializer.Deserialize( + await File.ReadAllTextAsync(goldenPath)); + + Assert.Equal(expected!.FileHash, result!.FileHash); + Assert.Equal(expected.Sections.Count, result.Sections.Count); +} +``` + +### Fuzz Testing + +```csharp +[Theory] +[MemberData(nameof(MalformedBinaries))] +public async Task Extract_MalformedBinary_ReturnsNullOrThrows(byte[] malformedData) +{ + // Act & Assert - Should not crash + var result = await _extractor.ExtractFromBytesAsync( + malformedData, + "test.bin"); + + // Either null or valid result, never exception + // (Exception would fail the test) +} +``` + +## References + +- [PE Format Specification](https://docs.microsoft.com/en-us/windows/win32/debug/pe-format) +- [Mach-O Format Reference](https://developer.apple.com/library/archive/documentation/DeveloperTools/Conceptual/MachORuntime/) +- [ELF Specification](https://refspecs.linuxfoundation.org/elf/elf.pdf) +- [Binary Diff Attestation Architecture](../modules/scanner/binary-diff-attestation.md) diff --git a/docs/doctor/README.md b/docs/doctor/README.md new file mode 100644 index 000000000..a417b0e0e --- /dev/null +++ b/docs/doctor/README.md @@ -0,0 +1,416 @@ +# Stella Ops Doctor + +> Self-service diagnostics for Stella Ops deployments + +## Overview + +The Doctor system provides comprehensive diagnostics for Stella Ops deployments, enabling operators, DevOps engineers, and developers to: + +- **Diagnose** what is working and what is not +- **Understand** why failures occur with collected evidence +- **Remediate** issues with copy/paste commands +- **Verify** fixes with re-runnable checks + +## Quick Start + +### CLI + +```bash +# Quick health check +stella doctor + +# Full diagnostic with all checks +stella doctor --full + +# Check specific category +stella doctor --category database + +# Export report for support +stella doctor export --output diagnostic-bundle.zip +``` + +### UI + +Navigate to `/ops/doctor` in the Stella Ops console to access the interactive Doctor Dashboard. + +### API + +```bash +# Run diagnostics +POST /api/v1/doctor/run + +# Get available checks +GET /api/v1/doctor/checks + +# Stream results +WebSocket /api/v1/doctor/stream +``` + +## Available Checks + +The Doctor system includes 48+ diagnostic checks across 7 plugins: + +| Plugin | Category | Checks | Description | +|--------|----------|--------|-------------| +| `stellaops.doctor.core` | Core | 9 | Configuration, runtime, disk, memory, time, crypto | +| `stellaops.doctor.database` | Database | 8 | Connectivity, migrations, schema, connection pool | +| `stellaops.doctor.servicegraph` | ServiceGraph | 6 | Gateway, routing, service health | +| `stellaops.doctor.security` | Security | 9 | OIDC, LDAP, TLS, Vault | +| `stellaops.doctor.scm.*` | Integration.SCM | 8 | GitHub, GitLab connectivity/auth/permissions | +| `stellaops.doctor.registry.*` | Integration.Registry | 6 | Harbor, ECR connectivity/auth/pull | +| `stellaops.doctor.observability` | Observability | 4 | OTLP, logs, metrics | + +### Check ID Convention + +``` +check.{category}.{subcategory}.{specific} +``` + +Examples: +- `check.config.required` +- `check.database.migrations.pending` +- `check.services.gateway.routing` +- `check.integration.scm.github.auth` + +## CLI Reference + +See [CLI Reference](./cli-reference.md) for complete command documentation. + +### Common Commands + +```bash +# Quick health check (tagged 'quick' checks only) +stella doctor --quick + +# Full diagnostic with all checks +stella doctor --full + +# Filter by category +stella doctor --category database +stella doctor --category security + +# Filter by plugin +stella doctor --plugin scm.github + +# Run single check +stella doctor --check check.database.migrations.pending + +# Output formats +stella doctor --format json +stella doctor --format markdown +stella doctor --format text + +# Filter output by severity +stella doctor --severity fail,warn + +# Export diagnostic bundle +stella doctor export --output diagnostic.zip +stella doctor export --include-logs --log-duration 4h +``` + +## Exit Codes + +| Code | Meaning | +|------|---------| +| 0 | All checks passed | +| 1 | One or more warnings | +| 2 | One or more failures | +| 3 | Doctor engine error | +| 4 | Invalid arguments | +| 5 | Timeout exceeded | + +## Output Example + +``` +Stella Ops Doctor +================= + +Running 47 checks across 8 plugins... + +[PASS] check.config.required + All required configuration values are present + +[PASS] check.database.connectivity + PostgreSQL connection successful (latency: 12ms) + +[WARN] check.tls.certificates.expiry + Diagnosis: TLS certificate expires in 14 days + + Evidence: + Certificate: /etc/ssl/certs/stellaops.crt + Subject: CN=stellaops.example.com + Expires: 2026-01-26T00:00:00Z + Days remaining: 14 + + Likely Causes: + 1. Certificate renewal not scheduled + 2. ACME/Let's Encrypt automation not configured + + Fix Steps: + # 1. Check current certificate + openssl x509 -in /etc/ssl/certs/stellaops.crt -noout -dates + + # 2. Renew certificate (if using certbot) + sudo certbot renew --cert-name stellaops.example.com + + # 3. Restart services to pick up new certificate + sudo systemctl restart stellaops-gateway + + Verification: + stella doctor --check check.tls.certificates.expiry + +[FAIL] check.database.migrations.pending + Diagnosis: 3 pending release migrations detected in schema 'auth' + + Evidence: + Schema: auth + Current version: 099_add_dpop_thumbprints + Pending migrations: + - 100_add_tenant_quotas + - 101_add_audit_retention + - 102_add_session_revocation + + Likely Causes: + 1. Release migrations not applied before deployment + 2. Migration files added after last deployment + + Fix Steps: + # 1. Backup database first (RECOMMENDED) + pg_dump -h localhost -U stella_admin -d stellaops -F c \ + -f stellaops_backup_$(date +%Y%m%d_%H%M%S).dump + + # 2. Apply pending release migrations + stella system migrations-run --module Authority --category release + + # 3. Verify migrations applied + stella system migrations-status --module Authority + + Verification: + stella doctor --check check.database.migrations.pending + +-------------------------------------------------------------------------------- +Summary: 44 passed, 2 warnings, 1 failed (47 total) +Duration: 8.3s +-------------------------------------------------------------------------------- +``` + +## Export Bundle + +The Doctor export feature creates a diagnostic bundle for support escalation: + +```bash +stella doctor export --output diagnostic-bundle.zip +``` + +The bundle contains: +- `doctor-report.json` - Full diagnostic report +- `doctor-report.md` - Human-readable report +- `environment.json` - Environment information +- `system-info.json` - System details (OS, runtime, memory) +- `config-sanitized.json` - Sanitized configuration (secrets redacted) +- `logs/` - Recent log files (optional) +- `README.md` - Bundle contents guide + +### Export Options + +```bash +# Include logs from last 4 hours +stella doctor export --include-logs --log-duration 4h + +# Exclude configuration +stella doctor export --no-config + +# Custom output path +stella doctor export --output /tmp/support-bundle.zip +``` + +## Security + +### Secret Redaction + +All evidence output is sanitized. Sensitive values (passwords, tokens, connection strings) are replaced with `***REDACTED***` in: +- Console output +- JSON exports +- Diagnostic bundles +- Log files + +### RBAC Permissions + +| Scope | Description | +|-------|-------------| +| `doctor:run` | Execute doctor checks | +| `doctor:run:full` | Execute all checks including sensitive | +| `doctor:export` | Export diagnostic reports | +| `admin:system` | Access system-level checks | + +## Plugin Development + +To create a custom Doctor plugin, implement `IDoctorPlugin`: + +```csharp +public class MyCustomPlugin : IDoctorPlugin +{ + public string PluginId => "stellaops.doctor.custom"; + public string DisplayName => "Custom Checks"; + public Version Version => new(1, 0, 0); + public DoctorCategory Category => DoctorCategory.Integration; + + public bool IsAvailable(IServiceProvider services) => true; + + public IReadOnlyList GetChecks(DoctorPluginContext context) + { + return new IDoctorCheck[] + { + new MyCustomCheck() + }; + } + + public Task InitializeAsync(DoctorPluginContext context, CancellationToken ct) + => Task.CompletedTask; +} +``` + +Implement checks using `IDoctorCheck`: + +```csharp +public class MyCustomCheck : IDoctorCheck +{ + public string CheckId => "check.custom.mycheck"; + public string Name => "My Custom Check"; + public string Description => "Validates custom configuration"; + public DoctorSeverity DefaultSeverity => DoctorSeverity.Fail; + public IReadOnlyList Tags => new[] { "custom", "quick" }; + public TimeSpan EstimatedDuration => TimeSpan.FromSeconds(2); + + public bool CanRun(DoctorPluginContext context) => true; + + public async Task RunAsync( + DoctorPluginContext context, + CancellationToken ct) + { + // Perform check logic + var isValid = await ValidateAsync(ct); + + if (isValid) + { + return DoctorCheckResult.Pass( + checkId: CheckId, + diagnosis: "Custom configuration is valid", + evidence: new Evidence + { + Description = "Validation passed", + Data = new Dictionary + { + ["validated_at"] = context.TimeProvider.GetUtcNow().ToString("O") + } + }); + } + + return DoctorCheckResult.Fail( + checkId: CheckId, + diagnosis: "Custom configuration is invalid", + evidence: new Evidence + { + Description = "Validation failed", + Data = new Dictionary + { + ["error"] = "Configuration file missing" + } + }, + remediation: new Remediation + { + Steps = new[] + { + new RemediationStep + { + Order = 1, + Description = "Create configuration file", + Command = "cp /etc/stellaops/custom.yaml.sample /etc/stellaops/custom.yaml", + CommandType = CommandType.Shell + } + } + }); + } +} +``` + +Register the plugin in DI: + +```csharp +services.AddSingleton(); +``` + +## Architecture + +``` ++------------------+ +------------------+ +------------------+ +| CLI | | UI | | External | +| stella doctor | | /ops/doctor | | Monitoring | ++--------+---------+ +--------+---------+ +--------+---------+ + | | | + v v v ++------------------------------------------------------------------------+ +| Doctor API Layer | +| POST /api/v1/doctor/run GET /api/v1/doctor/checks | +| GET /api/v1/doctor/report WebSocket /api/v1/doctor/stream | ++------------------------------------------------------------------------+ + | + v ++------------------------------------------------------------------------+ +| Doctor Engine (Core) | +| +------------------+ +------------------+ +------------------+ | +| | Check Registry | | Check Executor | | Report Generator | | +| | - Discovery | | - Parallel exec | | - JSON/MD/Text | | +| | - Filtering | | - Timeout mgmt | | - Remediation | | +| +------------------+ +------------------+ +------------------+ | ++------------------------------------------------------------------------+ + | + v ++------------------------------------------------------------------------+ +| Plugin System | ++--------+---------+---------+---------+---------+---------+-------------+ + | | | | | | + v v v v v v ++--------+ +------+ +------+ +------+ +------+ +------+ +----------+ +| Core | | DB & | |Service| | SCM | |Regis-| |Observ-| |Security | +| Plugin | |Migra-| | Graph | |Plugin| | try | |ability| | Plugin | +| | | tions| |Plugin | | | |Plugin| |Plugin | | | ++--------+ +------+ +------+ +------+ +------+ +------+ +----------+ +``` + +## Related Documentation + +- [CLI Reference](./cli-reference.md) - Complete CLI command reference +- [Doctor Capabilities Specification](./doctor-capabilities.md) - Full technical specification +- [Plugin Development Guide](./plugin-development.md) - Creating custom plugins + +## Troubleshooting + +### Doctor Engine Error (Exit Code 3) + +If `stella doctor` returns exit code 3: + +1. Check the error message for details +2. Verify required services are running +3. Check connectivity to databases +4. Review logs at `/var/log/stellaops/doctor.log` + +### Timeout Exceeded (Exit Code 5) + +If checks are timing out: + +```bash +# Increase per-check timeout +stella doctor --timeout 60s + +# Run with reduced parallelism +stella doctor --parallel 2 +``` + +### Checks Not Found + +If expected checks are not appearing: + +1. Verify plugin is registered in DI +2. Check `CanRun()` returns true for your environment +3. Review plugin initialization logs diff --git a/docs/doctor/cli-reference.md b/docs/doctor/cli-reference.md new file mode 100644 index 000000000..7c102c157 --- /dev/null +++ b/docs/doctor/cli-reference.md @@ -0,0 +1,396 @@ +# Doctor CLI Reference + +> Complete reference for `stella doctor` commands + +## Commands + +### stella doctor + +Run diagnostic checks. + +```bash +stella doctor [options] +``` + +#### Options + +| Option | Short | Type | Default | Description | +|--------|-------|------|---------|-------------| +| `--format` | `-f` | enum | `text` | Output format: `text`, `json`, `markdown` | +| `--quick` | `-q` | flag | false | Run only quick checks (tagged `quick`) | +| `--full` | | flag | false | Run all checks including slow/intensive | +| `--category` | `-c` | string[] | all | Filter by category | +| `--plugin` | `-p` | string[] | all | Filter by plugin ID | +| `--check` | | string | | Run single check by ID | +| `--severity` | `-s` | enum[] | all | Filter output by severity | +| `--timeout` | `-t` | duration | 30s | Per-check timeout | +| `--parallel` | | int | 4 | Max parallel check execution | +| `--no-remediation` | | flag | false | Skip remediation output | +| `--verbose` | `-v` | flag | false | Include detailed evidence | + +#### Categories + +- `core` - Configuration, runtime, system checks +- `database` - Database connectivity, migrations, pools +- `service-graph` - Service health, gateway, routing +- `security` - Authentication, TLS, secrets +- `integration` - SCM, registry integrations +- `observability` - Telemetry, logging, metrics + +#### Examples + +```bash +# Quick health check +stella doctor + +# Full diagnostic +stella doctor --full + +# Database checks only +stella doctor --category database + +# GitHub integration checks +stella doctor --plugin scm.github + +# Single check +stella doctor --check check.database.connectivity + +# JSON output (for CI/CD) +stella doctor --format json + +# Show only failures and warnings +stella doctor --severity fail,warn + +# Markdown report +stella doctor --format markdown > doctor-report.md + +# Verbose with all evidence +stella doctor --verbose + +# Custom timeout and parallelism +stella doctor --timeout 60s --parallel 2 +``` + +### stella doctor export + +Generate a diagnostic bundle for support. + +```bash +stella doctor export [options] +``` + +#### Options + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `--output` | path | `diagnostic-bundle.zip` | Output file path | +| `--include-logs` | flag | false | Include recent log files | +| `--log-duration` | duration | `1h` | Duration of logs to include | +| `--no-config` | flag | false | Exclude configuration | + +#### Duration Format + +Duration values can be specified as: +- `30m` - 30 minutes +- `1h` - 1 hour +- `4h` - 4 hours +- `24h` or `1d` - 24 hours + +#### Examples + +```bash +# Basic export +stella doctor export --output diagnostic.zip + +# Include logs from last 4 hours +stella doctor export --include-logs --log-duration 4h + +# Without configuration (for privacy) +stella doctor export --no-config + +# Full bundle with logs +stella doctor export \ + --output support-bundle.zip \ + --include-logs \ + --log-duration 24h +``` + +#### Bundle Contents + +The export creates a ZIP archive containing: + +``` +diagnostic-bundle.zip ++-- README.md # Bundle contents guide ++-- doctor-report.json # Full diagnostic report ++-- doctor-report.md # Human-readable report ++-- environment.json # Environment information ++-- system-info.json # System details ++-- config-sanitized.json # Configuration (secrets redacted) ++-- logs/ # Log files (if --include-logs) + +-- stellaops-*.log +``` + +### stella doctor list + +List available checks. + +```bash +stella doctor list [options] +``` + +#### Options + +| Option | Type | Description | +|--------|------|-------------| +| `--category` | string | Filter by category | +| `--plugin` | string | Filter by plugin | +| `--format` | enum | Output format: `text`, `json` | + +#### Examples + +```bash +# List all checks +stella doctor list + +# List database checks +stella doctor list --category database + +# List as JSON +stella doctor list --format json +``` + +## Exit Codes + +| Code | Name | Description | +|------|------|-------------| +| 0 | `Success` | All checks passed | +| 1 | `Warnings` | One or more warnings, no failures | +| 2 | `Failures` | One or more checks failed | +| 3 | `EngineError` | Doctor engine error | +| 4 | `InvalidArgs` | Invalid command arguments | +| 5 | `Timeout` | Timeout exceeded | + +### Using Exit Codes in Scripts + +```bash +#!/bin/bash + +stella doctor --format json > report.json +exit_code=$? + +case $exit_code in + 0) + echo "All checks passed" + ;; + 1) + echo "Warnings detected - review report" + ;; + 2) + echo "Failures detected - action required" + exit 1 + ;; + *) + echo "Doctor error (code: $exit_code)" + exit 1 + ;; +esac +``` + +## CI/CD Integration + +### GitHub Actions + +```yaml +- name: Run Stella Doctor + run: | + stella doctor --format json --severity fail,warn > doctor-report.json + exit_code=$? + if [ $exit_code -eq 2 ]; then + echo "::error::Doctor checks failed" + cat doctor-report.json + exit 1 + fi +``` + +### GitLab CI + +```yaml +doctor: + stage: validate + script: + - stella doctor --format json > doctor-report.json + artifacts: + when: always + paths: + - doctor-report.json + allow_failure: + exit_codes: + - 1 # Allow warnings +``` + +### Jenkins + +```groovy +stage('Health Check') { + steps { + script { + def result = sh( + script: 'stella doctor --format json', + returnStatus: true + ) + if (result == 2) { + error "Doctor checks failed" + } + } + } +} +``` + +## Output Formats + +### Text Format (Default) + +Human-readable console output with colors and formatting. + +``` +Stella Ops Doctor +================= + +Running 47 checks across 8 plugins... + +[PASS] check.config.required + All required configuration values are present + +[FAIL] check.database.migrations.pending + Diagnosis: 3 pending migrations in schema 'auth' + + Fix Steps: + # Apply migrations + stella system migrations-run --module Authority + +-------------------------------------------------------------------------------- +Summary: 46 passed, 0 warnings, 1 failed (47 total) +Duration: 8.3s +-------------------------------------------------------------------------------- +``` + +### JSON Format + +Machine-readable format for automation: + +```json +{ + "summary": { + "total": 47, + "passed": 46, + "warnings": 0, + "failures": 1, + "skipped": 0, + "duration": "PT8.3S" + }, + "executedAt": "2026-01-12T14:30:00Z", + "checks": [ + { + "checkId": "check.config.required", + "pluginId": "stellaops.doctor.core", + "category": "Core", + "severity": "Pass", + "diagnosis": "All required configuration values are present", + "evidence": { + "description": "Configuration validated", + "data": { + "configSource": "appsettings.json", + "keysChecked": "42" + } + }, + "duration": "PT0.012S" + }, + { + "checkId": "check.database.migrations.pending", + "pluginId": "stellaops.doctor.database", + "category": "Database", + "severity": "Fail", + "diagnosis": "3 pending migrations in schema 'auth'", + "evidence": { + "description": "Migration status", + "data": { + "schema": "auth", + "pendingCount": "3" + } + }, + "remediation": { + "steps": [ + { + "order": 1, + "description": "Apply pending migrations", + "command": "stella system migrations-run --module Authority", + "commandType": "Shell" + } + ] + }, + "duration": "PT0.234S" + } + ] +} +``` + +### Markdown Format + +Formatted for documentation and reports: + +```markdown +# Stella Ops Doctor Report + +**Generated:** 2026-01-12T14:30:00Z +**Duration:** 8.3s + +## Summary + +| Status | Count | +|--------|-------| +| Passed | 46 | +| Warnings | 0 | +| Failures | 1 | +| Skipped | 0 | +| **Total** | **47** | + +## Failed Checks + +### check.database.migrations.pending + +**Status:** FAIL +**Plugin:** stellaops.doctor.database +**Category:** Database + +**Diagnosis:** 3 pending migrations in schema 'auth' + +**Evidence:** +- Schema: auth +- Pending count: 3 + +**Fix Steps:** +1. Apply pending migrations + ```bash + stella system migrations-run --module Authority + ``` + +## Passed Checks + +- check.config.required +- check.database.connectivity +- ... (44 more) +``` + +## Environment Variables + +| Variable | Description | +|----------|-------------| +| `STELLAOPS_DOCTOR_TIMEOUT` | Default per-check timeout | +| `STELLAOPS_DOCTOR_PARALLEL` | Default parallelism | +| `STELLAOPS_CONFIG_PATH` | Configuration file path | + +## See Also + +- [Doctor Overview](./README.md) +- [Doctor Capabilities Specification](./doctor-capabilities.md) diff --git a/docs/examples/binary-diff/README.md b/docs/examples/binary-diff/README.md new file mode 100644 index 000000000..6dfce03ec --- /dev/null +++ b/docs/examples/binary-diff/README.md @@ -0,0 +1,55 @@ +# Binary Diff Examples + +This directory contains examples demonstrating the binary diff attestation feature. + +## Prerequisites + +- StellaOps CLI (`stella`) installed +- Access to a container registry +- Docker or containerd runtime (for image pulling) + +## Examples + +### Basic Comparison + +[basic-comparison.md](./basic-comparison.md) - Simple binary diff between two image versions + +### DSSE Attestation + +[dsse-attestation.md](./dsse-attestation.md) - Generating and verifying DSSE-signed attestations + +### Policy Integration + +[policy-integration.md](./policy-integration.md) - Using binary diff evidence in policy rules + +### CI/CD Integration + +[ci-cd-integration.md](./ci-cd-integration.md) - GitHub Actions and GitLab CI examples + +## Sample Outputs + +The `sample-outputs/` directory contains: + +- `diff-table.txt` - Sample table-formatted output +- `diff.json` - Sample JSON output +- `attestation.dsse.json` - Sample DSSE envelope + +## Quick Start + +```bash +# Compare two image versions +stella scan diff --base myapp:1.0.0 --target myapp:1.0.1 + +# Generate attestation +stella scan diff --base myapp:1.0.0 --target myapp:1.0.1 \ + --mode=elf --emit-dsse=./attestations/ + +# Verify attestation +stella verify attestation ./attestations/linux-amd64-binarydiff.dsse.json +``` + +## Related Documentation + +- [Binary Diff Attestation Architecture](../../modules/scanner/binary-diff-attestation.md) +- [BinaryDiffV1 JSON Schema](../../schemas/binarydiff-v1.schema.json) +- [CLI Reference](../../API_CLI_REFERENCE.md#stella-scan-diff) diff --git a/docs/examples/binary-diff/basic-comparison.md b/docs/examples/binary-diff/basic-comparison.md new file mode 100644 index 000000000..63b2e2156 --- /dev/null +++ b/docs/examples/binary-diff/basic-comparison.md @@ -0,0 +1,143 @@ +# Basic Binary Comparison + +This example demonstrates how to perform a basic binary diff between two container image versions. + +## Scenario + +You have deployed `myapp:1.0.0` and want to understand what binary changes are in `myapp:1.0.1` before upgrading. + +## Prerequisites + +```bash +# Ensure stella CLI is installed +stella --version + +# Verify registry access +stella registry ping docker://registry.example.com +``` + +## Basic Comparison + +### Table Output (Default) + +```bash +stella scan diff \ + --base docker://registry.example.com/myapp:1.0.0 \ + --target docker://registry.example.com/myapp:1.0.1 +``` + +Output: +``` +Binary Diff: docker://registry.example.com/myapp:1.0.0 -> docker://registry.example.com/myapp:1.0.1 +Platform: linux/amd64 +Analysis Mode: ELF Section Hashes + +PATH CHANGE VERDICT CONFIDENCE +-------------------------------------------------------------------------------- +/usr/lib/libssl.so.3 modified patched 0.95 +/usr/lib/libcrypto.so.3 modified patched 0.92 +/app/bin/myapp modified vanilla 0.98 + +Summary: 156 binaries analyzed, 3 modified, 153 unchanged +``` + +### JSON Output + +```bash +stella scan diff \ + --base docker://registry.example.com/myapp:1.0.0 \ + --target docker://registry.example.com/myapp:1.0.1 \ + --format=json > diff.json +``` + +The JSON output contains detailed section-level information. See [sample-outputs/diff.json](./sample-outputs/diff.json) for a complete example. + +### Summary Output + +```bash +stella scan diff \ + --base docker://registry.example.com/myapp:1.0.0 \ + --target docker://registry.example.com/myapp:1.0.1 \ + --format=summary +``` + +Output: +``` +Binary Diff Summary +------------------- +Base: docker://registry.example.com/myapp:1.0.0 (sha256:abc123...) +Target: docker://registry.example.com/myapp:1.0.1 (sha256:def456...) +Platform: linux/amd64 + +Binaries: 156 total, 3 modified, 153 unchanged +Verdicts: 2 patched, 1 vanilla +``` + +## Using Digest References + +For immutable references, use digests instead of tags: + +```bash +stella scan diff \ + --base docker://registry.example.com/myapp@sha256:abc123... \ + --target docker://registry.example.com/myapp@sha256:def456... +``` + +## Filtering by Platform + +For multi-arch images, specify the platform: + +```bash +# Linux AMD64 only +stella scan diff \ + --base myapp:1.0.0 \ + --target myapp:1.0.1 \ + --platform=linux/amd64 + +# Linux ARM64 +stella scan diff \ + --base myapp:1.0.0 \ + --target myapp:1.0.1 \ + --platform=linux/arm64 +``` + +## Including Unchanged Binaries + +By default, unchanged binaries are excluded from output. To include them: + +```bash +stella scan diff \ + --base myapp:1.0.0 \ + --target myapp:1.0.1 \ + --include-unchanged +``` + +## Verbose Output + +For debugging or detailed progress: + +```bash +stella scan diff \ + --base myapp:1.0.0 \ + --target myapp:1.0.1 \ + --verbose +``` + +Output includes: +- Layer download progress +- Binary detection details +- Section hash computation progress + +## Understanding Verdicts + +| Verdict | Meaning | Action | +|---------|---------|--------| +| `patched` | High confidence that a security patch was applied | Review changelog, consider safe to upgrade | +| `vanilla` | Standard code change, no backport evidence | Normal release update | +| `unknown` | Cannot determine patch status | Manual review recommended | + +## Next Steps + +- [Generate DSSE Attestations](./dsse-attestation.md) for audit trail +- [Integrate with Policy](./policy-integration.md) for automated gates +- [Add to CI/CD](./ci-cd-integration.md) for continuous verification diff --git a/docs/examples/binary-diff/ci-cd-integration.md b/docs/examples/binary-diff/ci-cd-integration.md new file mode 100644 index 000000000..e18f4ea44 --- /dev/null +++ b/docs/examples/binary-diff/ci-cd-integration.md @@ -0,0 +1,371 @@ +# CI/CD Integration + +This example demonstrates how to integrate binary diff attestation into your CI/CD pipelines. + +## GitHub Actions + +### Basic Workflow + +```yaml +# .github/workflows/binary-diff.yml +name: Binary Diff Attestation + +on: + push: + tags: + - 'v*' + +jobs: + binary-diff: + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write # For keyless signing + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Stella CLI + uses: stellaops/setup-stella@v1 + with: + version: 'latest' + + - name: Login to Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Get Previous Tag + id: prev-tag + run: | + PREV_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "") + echo "tag=$PREV_TAG" >> $GITHUB_OUTPUT + + - name: Binary Diff + if: steps.prev-tag.outputs.tag != '' + run: | + stella scan diff \ + --base ghcr.io/${{ github.repository }}:${{ steps.prev-tag.outputs.tag }} \ + --target ghcr.io/${{ github.repository }}:${{ github.ref_name }} \ + --mode=elf \ + --emit-dsse=./attestations/ \ + --format=json > diff.json + + - name: Upload Attestations + uses: actions/upload-artifact@v4 + with: + name: binary-diff-attestations + path: | + attestations/ + diff.json + + - name: Attach Attestation to Image + run: | + # Using cosign to attach attestation + cosign attach attestation \ + --attestation ./attestations/linux-amd64-binarydiff.dsse.json \ + ghcr.io/${{ github.repository }}:${{ github.ref_name }} +``` + +### With Release Gate + +```yaml +# .github/workflows/release-gate.yml +name: Release Gate with Binary Diff + +on: + workflow_dispatch: + inputs: + base_version: + description: 'Base version to compare' + required: true + target_version: + description: 'Target version to release' + required: true + +jobs: + binary-diff-gate: + runs-on: ubuntu-latest + outputs: + verdict: ${{ steps.analyze.outputs.verdict }} + + steps: + - name: Setup Stella CLI + uses: stellaops/setup-stella@v1 + + - name: Binary Diff Analysis + id: diff + run: | + stella scan diff \ + --base myapp:${{ inputs.base_version }} \ + --target myapp:${{ inputs.target_version }} \ + --format=json > diff.json + + - name: Analyze Results + id: analyze + run: | + # Check for unknown verdicts + UNKNOWN_COUNT=$(jq '.summary.verdicts.unknown // 0' diff.json) + if [ "$UNKNOWN_COUNT" -gt "0" ]; then + echo "verdict=review-required" >> $GITHUB_OUTPUT + echo "::warning::Found $UNKNOWN_COUNT binaries with unknown verdicts" + else + echo "verdict=approved" >> $GITHUB_OUTPUT + fi + + - name: Gate Decision + if: steps.analyze.outputs.verdict == 'review-required' + run: | + echo "Manual review required for unknown binary changes" + exit 1 +``` + +## GitLab CI + +### Basic Pipeline + +```yaml +# .gitlab-ci.yml +stages: + - build + - analyze + - release + +variables: + STELLA_VERSION: "latest" + +binary-diff: + stage: analyze + image: stellaops/cli:${STELLA_VERSION} + script: + - | + # Get previous tag + PREV_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "") + + if [ -n "$PREV_TAG" ]; then + stella scan diff \ + --base ${CI_REGISTRY_IMAGE}:${PREV_TAG} \ + --target ${CI_REGISTRY_IMAGE}:${CI_COMMIT_TAG} \ + --mode=elf \ + --emit-dsse=attestations/ \ + --format=json > diff.json + + # Upload to GitLab artifacts + echo "Binary diff completed" + else + echo "No previous tag found, skipping diff" + fi + artifacts: + paths: + - attestations/ + - diff.json + expire_in: 30 days + only: + - tags +``` + +### With Security Gate + +```yaml +# .gitlab-ci.yml +security-gate: + stage: analyze + image: stellaops/cli:latest + script: + - | + stella scan diff \ + --base ${CI_REGISTRY_IMAGE}:${BASE_VERSION} \ + --target ${CI_REGISTRY_IMAGE}:${TARGET_VERSION} \ + --format=json > diff.json + + # Fail if any unknown verdicts + UNKNOWN=$(jq '.summary.verdicts.unknown // 0' diff.json) + if [ "$UNKNOWN" -gt "0" ]; then + echo "Security gate failed: $UNKNOWN unknown binary changes" + jq '.findings[] | select(.verdict == "unknown")' diff.json + exit 1 + fi + + echo "Security gate passed" + allow_failure: false +``` + +## Jenkins Pipeline + +```groovy +// Jenkinsfile +pipeline { + agent any + + environment { + STELLA_VERSION = 'latest' + } + + stages { + stage('Binary Diff') { + steps { + script { + def prevTag = sh( + script: 'git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo ""', + returnStdout: true + ).trim() + + if (prevTag) { + sh """ + stella scan diff \\ + --base ${REGISTRY}/${IMAGE}:${prevTag} \\ + --target ${REGISTRY}/${IMAGE}:${TAG} \\ + --mode=elf \\ + --emit-dsse=attestations/ \\ + --format=json > diff.json + """ + + archiveArtifacts artifacts: 'attestations/*, diff.json' + + // Parse and check results + def diff = readJSON file: 'diff.json' + if (diff.summary.verdicts.unknown > 0) { + unstable("Found ${diff.summary.verdicts.unknown} unknown binary changes") + } + } + } + } + } + } +} +``` + +## Azure DevOps + +```yaml +# azure-pipelines.yml +trigger: + tags: + include: + - v* + +pool: + vmImage: 'ubuntu-latest' + +steps: + - task: Bash@3 + displayName: 'Install Stella CLI' + inputs: + targetType: 'inline' + script: | + curl -sSL https://get.stellaops.io | sh + stella --version + + - task: Docker@2 + displayName: 'Login to Registry' + inputs: + containerRegistry: 'myRegistry' + command: 'login' + + - task: Bash@3 + displayName: 'Binary Diff' + inputs: + targetType: 'inline' + script: | + PREV_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "") + if [ -n "$PREV_TAG" ]; then + stella scan diff \ + --base $(REGISTRY)/$(IMAGE):${PREV_TAG} \ + --target $(REGISTRY)/$(IMAGE):$(Build.SourceBranchName) \ + --mode=elf \ + --emit-dsse=$(Build.ArtifactStagingDirectory)/attestations/ \ + --format=json > $(Build.ArtifactStagingDirectory)/diff.json + fi + + - task: PublishBuildArtifacts@1 + inputs: + pathToPublish: '$(Build.ArtifactStagingDirectory)' + artifactName: 'binary-diff' +``` + +## Best Practices + +### 1. Always Use Digest References in Production + +```bash +# Instead of tags +stella scan diff --base myapp:v1.0.0 --target myapp:v1.0.1 + +# Use digests for immutability +stella scan diff \ + --base myapp@sha256:abc123... \ + --target myapp@sha256:def456... +``` + +### 2. Store Attestations with Releases + +Attach DSSE attestations to your container images or store them alongside release artifacts. + +### 3. Set Appropriate Timeouts + +```bash +# For large images, increase timeout +stella scan diff \ + --base myapp:v1 \ + --target myapp:v2 \ + --timeout=600 +``` + +### 4. Use Caching + +```yaml +# GitHub Actions with caching +- uses: actions/cache@v4 + with: + path: ~/.stella/cache + key: stella-${{ runner.os }}-${{ hashFiles('**/Dockerfile') }} +``` + +### 5. Fail Fast on Critical Issues + +```bash +# Exit code indicates issues +stella scan diff --base old --target new --format=json > diff.json +if [ $? -ne 0 ]; then + echo "Diff failed" + exit 1 +fi + +# Check for critical verdicts +jq -e '.summary.verdicts.unknown == 0' diff.json || exit 1 +``` + +## Troubleshooting + +### Registry Authentication + +```bash +# Use Docker config +stella scan diff \ + --base myapp:v1 \ + --target myapp:v2 \ + --registry-auth=~/.docker/config.json +``` + +### Platform Issues + +```bash +# Explicitly specify platform for multi-arch +stella scan diff \ + --base myapp:v1 \ + --target myapp:v2 \ + --platform=linux/amd64 +``` + +### Timeout Issues + +```bash +# Increase timeout for slow registries +stella scan diff \ + --base myapp:v1 \ + --target myapp:v2 \ + --timeout=900 +``` diff --git a/docs/examples/binary-diff/sample-outputs/attestation.dsse.json b/docs/examples/binary-diff/sample-outputs/attestation.dsse.json new file mode 100644 index 000000000..681e21022 --- /dev/null +++ b/docs/examples/binary-diff/sample-outputs/attestation.dsse.json @@ -0,0 +1,17 @@ +{ + "payloadType": "stellaops.binarydiff.v1", + "payload": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjEiLCJzdWJqZWN0IjpbeyJuYW1lIjoiZG9ja2VyOi8vcmVnaXN0cnkuZXhhbXBsZS5jb20vYXBwQHNoYTI1NjpkZWY0NTZhYmM3ODkwMTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDEyMzQ1NjdlZmdoIiwiZGlnZXN0Ijp7InNoYTI1NiI6ImRlZjQ1NmFiYzc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2N2VmZ2gifX1dLCJwcmVkaWNhdGVUeXBlIjoic3RlbGxhb3BzLmJpbmFyeWRpZmYudjEiLCJwcmVkaWNhdGUiOnsiaW5wdXRzIjp7ImJhc2UiOnsiZGlnZXN0Ijoic2hhMjU2OmFiYzEyM2RlZjQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNGFiY2QifSwidGFyZ2V0Ijp7ImRpZ2VzdCI6InNoYTI1NjpkZWY0NTZhYmM3ODkwMTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDEyMzQ1NjdlZmdoIn19LCJmaW5kaW5ncyI6W3sicGF0aCI6Ii91c3IvbGliL2xpYnNzbC5zby4zIiwiY2hhbmdlVHlwZSI6Im1vZGlmaWVkIiwidmVyZGljdCI6InBhdGNoZWQiLCJjb25maWRlbmNlIjowLjk1fV0sIm1ldGFkYXRhIjp7InRvb2xWZXJzaW9uIjoiMS4wLjAiLCJhbmFseXNpc1RpbWVzdGFtcCI6IjIwMjYtMDEtMTNUMTI6MDA6MDBaIn19fQ==", + "signatures": [ + { + "keyid": "SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA", + "sig": "MEUCIQDKZokqnCjrRtw5EXP14JvsBwFDRPfCp9K0UoOlWGdlDQIgSNpOGPqKNLv5MNZLYc5iE7q5b3wW6K0cDpjNxBxCWdU=" + } + ], + "_note": "This is a sample DSSE envelope for documentation purposes. The payload is base64-encoded and contains an in-toto statement with a BinaryDiffV1 predicate. In production, the signature would be cryptographically valid.", + "_rekorMetadata": { + "logIndex": 12345678, + "entryUuid": "24296fb24b8ad77aa3e6b0d1b6e0e3a0c9f8d7e6b5a4c3d2e1f0a9b8c7d6e5f4", + "integratedTime": "2026-01-13T12:00:05Z", + "logUrl": "https://rekor.sigstore.dev" + } +} diff --git a/docs/examples/binary-diff/sample-outputs/diff-table.txt b/docs/examples/binary-diff/sample-outputs/diff-table.txt new file mode 100644 index 000000000..2ad525a5a --- /dev/null +++ b/docs/examples/binary-diff/sample-outputs/diff-table.txt @@ -0,0 +1,27 @@ +Binary Diff: docker://registry.example.com/app:1.0.0 -> docker://registry.example.com/app:1.0.1 +Platform: linux/amd64 +Analysis Mode: ELF Section Hashes +Analyzed Sections: .text, .rodata, .data, .symtab, .dynsym + +PATH CHANGE VERDICT CONFIDENCE SECTIONS CHANGED +-------------------------------------------------------------------------------------------------- +/usr/lib/x86_64-linux-gnu/libssl.so.3 modified patched 0.95 .text, .rodata +/usr/lib/x86_64-linux-gnu/libcrypto.so.3 modified patched 0.92 .text +/usr/bin/openssl modified unknown 0.75 .text, .data, .symtab +/lib/x86_64-linux-gnu/libc.so.6 unchanged - - - +/lib/x86_64-linux-gnu/libpthread.so.0 unchanged - - - +/usr/lib/x86_64-linux-gnu/libz.so.1 unchanged - - - +/app/bin/myapp modified vanilla 0.98 .text, .rodata, .data + +Summary +------- +Total binaries analyzed: 156 +Modified: 4 +Unchanged: 152 + +Verdicts: + Patched: 2 (high confidence backport detected) + Vanilla: 1 (standard update, no backport evidence) + Unknown: 1 (insufficient evidence for classification) + +Analysis completed in 12.4s diff --git a/docs/examples/binary-diff/sample-outputs/diff.json b/docs/examples/binary-diff/sample-outputs/diff.json new file mode 100644 index 000000000..4368e030d --- /dev/null +++ b/docs/examples/binary-diff/sample-outputs/diff.json @@ -0,0 +1,179 @@ +{ + "schemaVersion": "1.0.0", + "base": { + "reference": "docker://registry.example.com/app:1.0.0", + "digest": "sha256:abc123def456789012345678901234567890123456789012345678901234abcd", + "manifestDigest": "sha256:111222333444555666777888999000aaabbbcccdddeeefff000111222333444555" + }, + "target": { + "reference": "docker://registry.example.com/app:1.0.1", + "digest": "sha256:def456abc789012345678901234567890123456789012345678901234567efgh", + "manifestDigest": "sha256:666777888999000aaabbbcccdddeeefff000111222333444555666777888999000" + }, + "platform": { + "os": "linux", + "architecture": "amd64" + }, + "analysisMode": "elf", + "timestamp": "2026-01-13T12:00:00.000000Z", + "findings": [ + { + "path": "/usr/lib/x86_64-linux-gnu/libssl.so.3", + "changeType": "modified", + "binaryFormat": "elf", + "layerDigest": "sha256:aaa111bbb222ccc333ddd444eee555fff666777888999000aaabbbcccdddeeef", + "baseHashes": { + "buildId": "abc123def456789012345678", + "fileHash": "1111111111111111111111111111111111111111111111111111111111111111", + "sections": { + ".text": { + "sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "size": 524288, + "offset": 4096 + }, + ".rodata": { + "sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "size": 131072, + "offset": 528384 + } + } + }, + "targetHashes": { + "buildId": "def789abc012345678901234", + "fileHash": "2222222222222222222222222222222222222222222222222222222222222222", + "sections": { + ".text": { + "sha256": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "size": 524544, + "offset": 4096 + }, + ".rodata": { + "sha256": "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + "size": 131200, + "offset": 528640 + } + } + }, + "sectionDeltas": [ + { + "section": ".text", + "status": "modified", + "baseSha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "targetSha256": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "sizeDelta": 256 + }, + { + "section": ".rodata", + "status": "modified", + "baseSha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "targetSha256": "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + "sizeDelta": 128 + }, + { + "section": ".data", + "status": "identical", + "baseSha256": "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "targetSha256": "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "sizeDelta": 0 + }, + { + "section": ".symtab", + "status": "identical", + "baseSha256": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "targetSha256": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "sizeDelta": 0 + } + ], + "confidence": 0.95, + "verdict": "patched" + }, + { + "path": "/usr/lib/x86_64-linux-gnu/libcrypto.so.3", + "changeType": "modified", + "binaryFormat": "elf", + "layerDigest": "sha256:aaa111bbb222ccc333ddd444eee555fff666777888999000aaabbbcccdddeeef", + "sectionDeltas": [ + { + "section": ".text", + "status": "modified", + "sizeDelta": 1024 + }, + { + "section": ".rodata", + "status": "identical", + "sizeDelta": 0 + } + ], + "confidence": 0.92, + "verdict": "patched" + }, + { + "path": "/usr/bin/openssl", + "changeType": "modified", + "binaryFormat": "elf", + "sectionDeltas": [ + { + "section": ".text", + "status": "modified", + "sizeDelta": 512 + }, + { + "section": ".data", + "status": "modified", + "sizeDelta": 64 + }, + { + "section": ".symtab", + "status": "modified", + "sizeDelta": 128 + } + ], + "confidence": 0.75, + "verdict": "unknown" + }, + { + "path": "/app/bin/myapp", + "changeType": "modified", + "binaryFormat": "elf", + "sectionDeltas": [ + { + "section": ".text", + "status": "modified", + "sizeDelta": 2048 + }, + { + "section": ".rodata", + "status": "modified", + "sizeDelta": 512 + }, + { + "section": ".data", + "status": "modified", + "sizeDelta": 128 + } + ], + "confidence": 0.98, + "verdict": "vanilla" + } + ], + "summary": { + "totalBinaries": 156, + "modified": 4, + "unchanged": 152, + "added": 0, + "removed": 0, + "verdicts": { + "patched": 2, + "vanilla": 1, + "unknown": 1, + "incompatible": 0 + }, + "sectionsAnalyzed": [".text", ".rodata", ".data", ".symtab", ".dynsym"], + "analysisDurationMs": 12400 + }, + "metadata": { + "toolVersion": "1.0.0", + "analysisTimestamp": "2026-01-13T12:00:00.000000Z", + "configDigest": "sha256:config123456789abcdef0123456789abcdef0123456789abcdef0123456789ab" + } +} diff --git a/docs/implplan/SPRINT_20260112_003_BE_csproj_audit_pending_apply.md b/docs/implplan/SPRINT_20260112_003_BE_csproj_audit_pending_apply.md index 7d31df6d8..cd7910492 100644 --- a/docs/implplan/SPRINT_20260112_003_BE_csproj_audit_pending_apply.md +++ b/docs/implplan/SPRINT_20260112_003_BE_csproj_audit_pending_apply.md @@ -23,20 +23,20 @@ ## Delivery Tracker | # | Task ID | Status | Key dependency / next step | Owners | Task Definition | | --- | --- | --- | --- | --- | --- | -| 1 | AUDIT-HOTLIST-SCANNER-LANG-DOTNET-0001 | TODO | Approved 2026-01-12; Hotlist S3/M1/Q0 | Guild - Scanner | Remediate hotlist findings for `src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.DotNet/StellaOps.Scanner.Analyzers.Lang.DotNet.csproj`; apply fixes, add tests, update audit tracker. | -| 2 | AUDIT-HOTLIST-SCANNER-CONTRACTS-0001 | TODO | Approved 2026-01-12; Hotlist S3/M0/Q0 | Guild - Scanner | Remediate hotlist findings for `src/Scanner/__Libraries/StellaOps.Scanner.Contracts/StellaOps.Scanner.Contracts.csproj`; apply fixes, add tests, update audit tracker. | -| 3 | AUDIT-HOTLIST-CLI-0001 | TODO | Approved 2026-01-12; Hotlist S2/M5/Q3 | Guild - CLI | Remediate hotlist findings for `src/Cli/StellaOps.Cli/StellaOps.Cli.csproj`; apply fixes, add tests, update audit tracker. | -| 4 | AUDIT-HOTLIST-EXPORTCENTER-WEBSERVICE-0001 | TODO | Approved 2026-01-12; Hotlist S2/M4/Q0 | Guild - ExportCenter | Remediate hotlist findings for `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/StellaOps.ExportCenter.WebService.csproj`; apply fixes, add tests, update audit tracker. | -| 5 | AUDIT-HOTLIST-POLICY-ENGINE-0001 | TODO | Approved 2026-01-12; Hotlist S2/M3/Q2 | Guild - Policy | Remediate hotlist findings for `src/Policy/StellaOps.Policy.Engine/StellaOps.Policy.Engine.csproj`; apply fixes, add tests, update audit tracker. | -| 6 | AUDIT-HOTLIST-SCANNER-NATIVE-0001 | TODO | Approved 2026-01-12; Hotlist S2/M3/Q1 | Guild - Scanner | Remediate hotlist findings for `src/Scanner/StellaOps.Scanner.Analyzers.Native/StellaOps.Scanner.Analyzers.Native.csproj`; apply fixes, add tests, update audit tracker. | -| 7 | AUDIT-HOTLIST-SCANNER-WEBSERVICE-0001 | TODO | Approved 2026-01-12; Hotlist S2/M2/Q2 | Guild - Scanner | Remediate hotlist findings for `src/Scanner/StellaOps.Scanner.WebService/StellaOps.Scanner.WebService.csproj`; apply fixes, add tests, update audit tracker. | -| 8 | AUDIT-HOTLIST-EXPORTCENTER-CORE-0001 | TODO | Approved 2026-01-12; Hotlist S2/M2/Q1 | Guild - ExportCenter | Remediate hotlist findings for `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/StellaOps.ExportCenter.Core.csproj`; apply fixes, add tests, update audit tracker. | +| 1 | AUDIT-HOTLIST-SCANNER-LANG-DOTNET-0001 | DONE | Applied 2026-01-12 | Guild - Scanner | Remediate hotlist findings for `src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.DotNet/StellaOps.Scanner.Analyzers.Lang.DotNet.csproj`; apply fixes, add tests, update audit tracker. | +| 2 | AUDIT-HOTLIST-SCANNER-CONTRACTS-0001 | DONE | Applied 2026-01-12 | Guild - Scanner | Remediate hotlist findings for `src/Scanner/__Libraries/StellaOps.Scanner.Contracts/StellaOps.Scanner.Contracts.csproj`; apply fixes, add tests, update audit tracker. | +| 3 | AUDIT-HOTLIST-CLI-0001 | BLOCKED | Blocked: CLI tests under active edit; avoid touching other agent work | Guild - CLI | Remediate hotlist findings for `src/Cli/StellaOps.Cli/StellaOps.Cli.csproj`; apply fixes, add tests, update audit tracker. | +| 4 | AUDIT-HOTLIST-EXPORTCENTER-WEBSERVICE-0001 | DONE | Applied 2026-01-13; tests added and tracker updated | Guild - ExportCenter | Remediate hotlist findings for `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/StellaOps.ExportCenter.WebService.csproj`; apply fixes, add tests, update audit tracker. | +| 5 | AUDIT-HOTLIST-POLICY-ENGINE-0001 | DONE | Applied 2026-01-13; determinism DI, options binding, auth, tests | Guild - Policy | Remediate hotlist findings for `src/Policy/StellaOps.Policy.Engine/StellaOps.Policy.Engine.csproj`; apply fixes, add tests, update audit tracker. | +| 6 | AUDIT-HOTLIST-SCANNER-NATIVE-0001 | DONE | Applied 2026-01-13; tracker updated | Guild - Scanner | Remediate hotlist findings for `src/Scanner/StellaOps.Scanner.Analyzers.Native/StellaOps.Scanner.Analyzers.Native.csproj`; apply fixes, add tests, update audit tracker. | +| 7 | AUDIT-HOTLIST-SCANNER-WEBSERVICE-0001 | DONE | Applied 2026-01-13; Hotlist S2/M2/Q2 | Guild - Scanner | Remediate hotlist findings for `src/Scanner/StellaOps.Scanner.WebService/StellaOps.Scanner.WebService.csproj`; apply fixes, add tests, update audit tracker. | +| 8 | AUDIT-HOTLIST-EXPORTCENTER-CORE-0001 | DOING | In progress 2026-01-13; Hotlist S2/M2/Q1 | Guild - ExportCenter | Remediate hotlist findings for `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/StellaOps.ExportCenter.Core.csproj`; apply fixes, add tests, update audit tracker. | | 9 | AUDIT-HOTLIST-SIGNALS-0001 | TODO | Approved 2026-01-12; Hotlist S2/M2/Q1 | Guild - Signals | Remediate hotlist findings for `src/Signals/StellaOps.Signals/StellaOps.Signals.csproj`; apply fixes, add tests, update audit tracker. | -| 10 | AUDIT-HOTLIST-SCANNER-LANG-DENO-0001 | TODO | Approved 2026-01-12; Hotlist S2/M0/Q0 | Guild - Scanner | Remediate hotlist findings for `src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Deno/StellaOps.Scanner.Analyzers.Lang.Deno.csproj`; apply fixes, add tests, update audit tracker. | +| 10 | AUDIT-HOTLIST-SCANNER-LANG-DENO-0001 | DONE | Applied 2026-01-13; runtime hardening, determinism fixes, tests updated | Guild - Scanner | Remediate hotlist findings for `src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Deno/StellaOps.Scanner.Analyzers.Lang.Deno.csproj`; apply fixes, add tests, update audit tracker. | | 11 | AUDIT-HOTLIST-VEXLENS-0001 | TODO | Approved 2026-01-12; Hotlist S1/M4/Q0 | Guild - VexLens | Remediate hotlist findings for `src/VexLens/StellaOps.VexLens/StellaOps.VexLens.csproj`; apply fixes, add tests, update audit tracker. | | 12 | AUDIT-HOTLIST-CONCELIER-CORE-0001 | TODO | Approved 2026-01-12; Hotlist S1/M3/Q2 | Guild - Concelier | Remediate hotlist findings for `src/Concelier/__Libraries/StellaOps.Concelier.Core/StellaOps.Concelier.Core.csproj`; apply fixes, add tests, update audit tracker. | -| 13 | AUDIT-HOTLIST-SCANNER-REACHABILITY-0001 | TODO | Approved 2026-01-12; Hotlist S1/M3/Q1 | Guild - Scanner | Remediate hotlist findings for `src/Scanner/__Libraries/StellaOps.Scanner.Reachability/StellaOps.Scanner.Reachability.csproj`; apply fixes, add tests, update audit tracker. | -| 14 | AUDIT-HOTLIST-EVIDENCE-0001 | TODO | Approved 2026-01-12; Hotlist S1/M3/Q0 | Guild - Core | Remediate hotlist findings for `src/__Libraries/StellaOps.Evidence/StellaOps.Evidence.csproj`; apply fixes, add tests, update audit tracker. | +| 13 | AUDIT-HOTLIST-SCANNER-REACHABILITY-0001 | DONE | Applied 2026-01-13; tracker updated | Guild - Scanner | Remediate hotlist findings for `src/Scanner/__Libraries/StellaOps.Scanner.Reachability/StellaOps.Scanner.Reachability.csproj`; apply fixes, add tests, update audit tracker. | +| 14 | AUDIT-HOTLIST-EVIDENCE-0001 | DONE | Applied 2026-01-13 | Guild - Core | Remediate hotlist findings for `src/__Libraries/StellaOps.Evidence/StellaOps.Evidence.csproj`; apply fixes, add tests, update audit tracker. | | 15 | AUDIT-HOTLIST-ZASTAVA-OBSERVER-0001 | TODO | Approved 2026-01-12; Hotlist S1/M3/Q0 | Guild - Zastava | Remediate hotlist findings for `src/Zastava/StellaOps.Zastava.Observer/StellaOps.Zastava.Observer.csproj`; apply fixes, add tests, update audit tracker. | | 16 | AUDIT-HOTLIST-TESTKIT-0001 | TODO | Approved 2026-01-12; Hotlist S0/M4/Q1 | Guild - Core | Remediate hotlist findings for `src/__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj`; apply fixes, add tests, update audit tracker. | | 17 | AUDIT-HOTLIST-EXCITITOR-WORKER-0001 | TODO | Approved 2026-01-12; Hotlist S0/M4/Q1 | Guild - Excititor | Remediate hotlist findings for `src/Excititor/StellaOps.Excititor.Worker/StellaOps.Excititor.Worker.csproj`; apply fixes, add tests, update audit tracker. | @@ -46,7 +46,7 @@ | 21 | AUDIT-HOTLIST-PROVCACHE-0001 | TODO | Approved 2026-01-12; Hotlist S0/M3/Q1 | Guild - Core | Remediate hotlist findings for `src/__Libraries/StellaOps.Provcache/StellaOps.Provcache.csproj`; apply fixes, add tests, update audit tracker. | | 22 | AUDIT-HOTLIST-EXCITITOR-CORE-0001 | TODO | Approved 2026-01-12; Hotlist Q2/S1/M2 | Guild - Excititor | Remediate hotlist findings for `src/Excititor/__Libraries/StellaOps.Excititor.Core/StellaOps.Excititor.Core.csproj`; apply fixes, add tests, update audit tracker. | | 23 | AUDIT-HOTLIST-SBOMSERVICE-0001 | TODO | Approved 2026-01-12; Hotlist Q2/S1/M2 | Guild - SbomService | Remediate hotlist findings for `src/SbomService/StellaOps.SbomService/StellaOps.SbomService.csproj`; apply fixes, add tests, update audit tracker. | -| 24 | AUDIT-HOTLIST-SCANNER-SBOMER-BUILDX-0001 | TODO | Approved 2026-01-12; Hotlist Q2/S1/M2 | Guild - Scanner | Remediate hotlist findings for `src/Scanner/StellaOps.Scanner.Sbomer.BuildXPlugin/StellaOps.Scanner.Sbomer.BuildXPlugin.csproj`; apply fixes, add tests, update audit tracker. | +| 24 | AUDIT-HOTLIST-SCANNER-SBOMER-BUILDX-0001 | DONE | Applied 2026-01-13; Hotlist Q2/S1/M2 | Guild - Scanner | Remediate hotlist findings for `src/Scanner/StellaOps.Scanner.Sbomer.BuildXPlugin/StellaOps.Scanner.Sbomer.BuildXPlugin.csproj`; apply fixes, add tests, update audit tracker. | | 25 | AUDIT-HOTLIST-ATTESTOR-WEBSERVICE-0001 | TODO | Approved 2026-01-12; Hotlist Q2/S0/M2 | Guild - Attestor | Remediate hotlist findings for `src/Attestor/StellaOps.Attestor/StellaOps.Attestor.WebService/StellaOps.Attestor.WebService.csproj`; apply fixes, add tests, update audit tracker. | | 26 | AUDIT-HOTLIST-POLICY-TOOLS-0001 | TODO | Approved 2026-01-12; Hotlist Q2/S0/M1 | Guild - Policy | Remediate hotlist findings for `src/__Libraries/StellaOps.Policy.Tools/StellaOps.Policy.Tools.csproj`; apply fixes, add tests, update audit tracker. | | 27 | AUDIT-HOTLIST-SCANNER-SOURCES-0001 | TODO | Approved 2026-01-12; Hotlist Q2/S0/M1 | Guild - Scanner | Remediate hotlist findings for `src/Scanner/__Libraries/StellaOps.Scanner.Sources/StellaOps.Scanner.Sources.csproj`; apply fixes, add tests, update audit tracker. | @@ -85,6 +85,14 @@ ## Execution Log | Date (UTC) | Update | Owner | | --- | --- | --- | +| 2026-01-12 | Started AUDIT-HOTLIST-SCANNER-CONTRACTS-0001 remediation work. | Project Mgmt | +| 2026-01-12 | Completed AUDIT-HOTLIST-SCANNER-CONTRACTS-0001; updated safe JSON encoding and coverage, updated audit tracker and local TASKS.md. | Project Mgmt | +| 2026-01-12 | Started AUDIT-HOTLIST-SCANNER-LANG-DOTNET-0001 remediation work. | Project Mgmt | +| 2026-01-12 | Blocked AUDIT-HOTLIST-CLI-0001: CLI tests are being modified by another agent; cannot update tests without touching their work. | Project Mgmt | +| 2026-01-12 | Started AUDIT-HOTLIST-EXPORTCENTER-WEBSERVICE-0001 remediation work. | Project Mgmt | +| 2026-01-13 | Completed AUDIT-HOTLIST-EXPORTCENTER-WEBSERVICE-0001; determinism/DI guards, retention/TLS gating, tests; updated audit tracker and TASKS.md. | Project Mgmt | +| 2026-01-12 | Completed AUDIT-HOTLIST-SCANNER-LANG-DOTNET-0001; applied fixes and tests, updated audit tracker and local TASKS.md. | Project Mgmt | +| 2026-01-12 | Test run failed for StellaOps.Scanner.Analyzers.Lang.DotNet.Tests: missing testhost.dll in testhost.deps.json. | Project Mgmt | | 2026-01-12 | Started AUDIT-SLN-NEWPROJECTS-0001 to add missing projects and audit new entries. | Project Mgmt | | 2026-01-12 | Completed AUDIT-SLN-NEWPROJECTS-0001: src/StellaOps.sln synced to include all csproj; Doctor projects audited and recorded in archived tracker findings. | Project Mgmt | | 2026-01-12 | Added Doctor.Tests to src/StellaOps.sln and extended archived audit tracker with audit rows and findings for the new test project. | Project Mgmt | @@ -93,6 +101,22 @@ | 2026-01-12 | Expanded Delivery Tracker with per-project hotlist items and batched test/reuse gap remediation tasks. | Project Mgmt | | 2026-01-12 | Set working directory to repo root to cover devops and docs items in test/reuse gaps. | Project Mgmt | | 2026-01-12 | Sprint created to execute approved pending APPLY actions from the C# audit backlog. | Project Mgmt | +| 2026-01-12 | Tests failed: StellaOps.Scanner.CallGraph.Tests (ValkeyCallGraphCacheServiceTests null result, BinaryDisassemblyTests target mismatch, BenchmarkIntegrationTests repo root missing). | Project Mgmt | +| 2026-01-13 | Started AUDIT-HOTLIST-POLICY-ENGINE-0001 remediation work. | Project Mgmt | +| 2026-01-13 | Completed AUDIT-HOTLIST-POLICY-ENGINE-0001 remediation work; updated determinism, auth, options binding, and tests. | Project Mgmt | +| 2026-01-13 | Started AUDIT-HOTLIST-SCANNER-NATIVE-0001 remediation work. | Project Mgmt | +| 2026-01-13 | Completed AUDIT-HOTLIST-SCANNER-NATIVE-0001; updated native analyzer determinism, hardening, runtime capture, and tests; updated audit tracker. | Project Mgmt | +| 2026-01-13 | Started AUDIT-HOTLIST-SCANNER-WEBSERVICE-0001 remediation work. | Project Mgmt | +| 2026-01-13 | Completed AUDIT-HOTLIST-SCANNER-WEBSERVICE-0001; DSSE PAE, determinism/auth updates, test fixes; trackers updated. | Project Mgmt | +| 2026-01-13 | Started AUDIT-HOTLIST-SCANNER-SBOMER-BUILDX-0001 remediation work. | Project Mgmt | +| 2026-01-13 | Completed AUDIT-HOTLIST-SCANNER-SBOMER-BUILDX-0001; canonical surface manifests, HttpClientFactory + TLS guardrails, deterministic tests; trackers updated. | Project Mgmt | +| 2026-01-13 | Started AUDIT-HOTLIST-SCANNER-LANG-DENO-0001 remediation work. | Project Mgmt | +| 2026-01-13 | Completed AUDIT-HOTLIST-SCANNER-LANG-DENO-0001; runtime hardening, deterministic ordering, safe JSON encoding, tests updated; trackers updated. | Project Mgmt | +| 2026-01-13 | Started AUDIT-HOTLIST-SCANNER-REACHABILITY-0001 remediation work. | Project Mgmt | +| 2026-01-13 | Completed AUDIT-HOTLIST-SCANNER-REACHABILITY-0001; DSSE PAE/canon, determinism/cancellation fixes, invariant formatting, tests; trackers updated. | Project Mgmt | +| 2026-01-13 | Started AUDIT-HOTLIST-EVIDENCE-0001 remediation work. | Project Mgmt | +| 2026-01-13 | Completed AUDIT-HOTLIST-EVIDENCE-0001 (determinism, schema validation, budgets, retention, tests). | Project Mgmt | +| 2026-01-13 | Started AUDIT-HOTLIST-EXPORTCENTER-CORE-0001 remediation work. | Project Mgmt | ## Decisions & Risks - APPROVED 2026-01-12: All pending APPLY actions are approved for execution under module review gates. @@ -100,6 +124,7 @@ - Cross-module doc link updates applied for archived audit files and the code-of-conduct relocation in docs/code-of-conduct/. - Backlog size (851 TODO APPLY items); mitigate by prioritizing hotlists then long-tail batches. - Devops and docs items are in scope; cross-directory changes must be logged per sprint guidance. +- BLOCKED: AUDIT-HOTLIST-CLI-0001 requires edits in `src/Cli/__Tests/StellaOps.Cli.Tests` which are under active modification by another agent; defer until those changes land or ownership is coordinated. ## Next Checkpoints - TBD: Security hotlist remediation review. diff --git a/docs/implplan/SPRINT_20260113_000_MASTER_INDEX_oci_binary_integrity.md b/docs/implplan/SPRINT_20260113_000_MASTER_INDEX_oci_binary_integrity.md new file mode 100644 index 000000000..1362c5408 --- /dev/null +++ b/docs/implplan/SPRINT_20260113_000_MASTER_INDEX_oci_binary_integrity.md @@ -0,0 +1,334 @@ +# Master Index 20260113 - OCI Layer-Level Binary Integrity Verification + +## Executive Summary + +This master index coordinates four sprint batches implementing **OCI layer-level image integrity verification** with binary patch detection capabilities. The complete feature set enables: + +1. **Multi-arch image inspection** with layer enumeration +2. **Section-level binary analysis** (ELF/PE) for vendor backport detection +3. **DSSE-signed attestations** proving patch presence or absence +4. **VEX auto-linking** to binary evidence for deterministic decisions +5. **Golden pairs dataset** for validation and regression testing + +**Total Effort:** ~25-30 story points across 4 batches, 13 sprints +**Priority:** High (core differentiator for evidence-first security) + +## Background + +### Advisory Origin + +The original product advisory specified requirements for: + +> OCI layer-level image integrity verification that: +> - Enumerates all layers across multi-arch manifests +> - Computes section-level hashes (ELF .text/.rodata, PE .text/.rdata) +> - Produces DSSE-signed in-toto attestations for binary diffs +> - Maps findings to VEX with cryptographic evidence links +> - Validates against a curated "golden pairs" corpus + +### Strategic Value + +| Capability | Business Value | +|------------|----------------| +| Binary patch detection | Prove vendor backports without source access | +| Attestation chain | Tamper-evident evidence for audits | +| VEX evidence links | Deterministic, reproducible security decisions | +| Golden pairs validation | Confidence in detection accuracy | + +## Sprint Batch Index + +| Batch | ID | Topic | Sprints | Status | Priority | +|-------|-----|-------|---------|--------|----------| +| 1 | 20260113_001 | ELF Section Hashes & Binary Diff Attestation | 4 | TODO | P0 | +| 2 | 20260113_002 | Image Index Resolution CLI | 3 | TODO | P1 | +| 3 | 20260113_003 | VEX Evidence Auto-Linking | 2 | TODO | P1 | +| 4 | 20260113_004 | Golden Pairs Pilot (Vendor Backport Corpus) | 3 | TODO | P2 | + +## Batch Details + +### Batch 001: ELF Section Hashes & Binary Diff Attestation + +**Index:** [SPRINT_20260113_001_000_INDEX_binary_diff_attestation.md](SPRINT_20260113_001_000_INDEX_binary_diff_attestation.md) + +**Scope:** Core binary analysis infrastructure + +| Sprint | ID | Module | Topic | Key Deliverables | +|--------|-----|--------|-------|------------------| +| 1 | 001_001 | SCANNER | ELF Section Hash Extractor | `IElfSectionHashExtractor`, per-section SHA-256 | +| 2 | 001_002 | ATTESTOR | BinaryDiffV1 In-Toto Predicate | `BinaryDiffV1` schema, DSSE signing | +| 3 | 001_003 | CLI | Binary Diff Command | `stella binary diff`, OCI layer comparison | +| 4 | 001_004 | DOCS | Binary Diff Attestation Documentation | Architecture docs, examples | + +**Key Models:** +- `ElfSectionHash` - Per-section hash with flags +- `BinaryDiffV1` - In-toto predicate for diff attestations +- `SectionDelta` - Section comparison result + +### Batch 002: Image Index Resolution CLI + +**Index:** [SPRINT_20260113_002_000_INDEX_image_index_resolution.md](SPRINT_20260113_002_000_INDEX_image_index_resolution.md) + +**Scope:** Multi-arch image inspection and layer enumeration + +| Sprint | ID | Module | Topic | Key Deliverables | +|--------|-----|--------|-------|------------------| +| 1 | 002_001 | SCANNER | OCI Image Inspector Service | `IOciImageInspector`, manifest resolution | +| 2 | 002_002 | CLI | Image Inspect Command | `stella image inspect`, platform selection | +| 3 | 002_003 | DOCS | Image Inspection Documentation | Architecture docs, examples | + +**Key Models:** +- `ImageInspectionResult` - Full image analysis +- `PlatformManifest` - Per-platform manifest info +- `LayerInfo` - Layer digest, size, media type + +### Batch 003: VEX Evidence Auto-Linking + +**Index:** [SPRINT_20260113_003_000_INDEX_vex_evidence_linking.md](SPRINT_20260113_003_000_INDEX_vex_evidence_linking.md) + +**Scope:** Automatic linking of VEX entries to binary diff evidence + +| Sprint | ID | Module | Topic | Key Deliverables | +|--------|-----|--------|-------|------------------| +| 1 | 003_001 | EXCITITOR | VEX Evidence Linker | `IVexEvidenceLinker`, CycloneDX mapping | +| 2 | 003_002 | CLI | VEX Evidence Integration | `--link-evidence` flag, evidence display | + +**Key Models:** +- `VexEvidenceLink` - Link to evidence attestation +- `VexEvidenceLinkSet` - Multi-evidence aggregation + +### Batch 004: Golden Pairs Pilot + +**Index:** [SPRINT_20260113_004_000_INDEX_golden_pairs_pilot.md](SPRINT_20260113_004_000_INDEX_golden_pairs_pilot.md) + +**Scope:** Validation dataset for binary patch detection + +| Sprint | ID | Module | Topic | Key Deliverables | +|--------|-----|--------|-------|------------------| +| 1 | 004_001 | TOOLS | Golden Pairs Data Model | `GoldenPairMetadata`, JSON schema | +| 2 | 004_002 | TOOLS | Mirror & Diff Pipeline | Package mirror, diff validation | +| 3 | 004_003 | TOOLS | Pilot CVE Corpus (3 CVEs) | Dirty Pipe, Baron Samedit, PrintNightmare | + +**Target CVEs:** +- CVE-2022-0847 (Dirty Pipe) - Linux kernel +- CVE-2021-3156 (Baron Samedit) - sudo +- CVE-2021-34527 (PrintNightmare) - Windows PE (conditional) + +## Dependency Graph + +``` ++-----------------------------------------------------------------------------------+ +| DEPENDENCY FLOW | ++-----------------------------------------------------------------------------------+ +| | +| BATCH 001: Binary Diff Attestation | +| +------------------------------------------------------------------+ | +| | Sprint 001 (ELF Hashes) --> Sprint 002 (Predicate) --> Sprint 003 (CLI) | +| +------------------------------------------------------------------+ | +| | | | +| v v | +| BATCH 002: Image Index Resolution | | +| +--------------------------------+ | | +| | Sprint 001 --> Sprint 002 (CLI)| | | +| +--------------------------------+ | | +| | | | +| v v | +| BATCH 003: VEX Evidence Linking <------+ | +| +--------------------------------+ | +| | Sprint 001 (Linker) --> Sprint 002 (CLI) | +| +--------------------------------+ | +| | +| BATCH 004: Golden Pairs (Validation) - Can start in parallel with Batch 001 | +| +------------------------------------------------------------------+ | +| | Sprint 001 (Model) --> Sprint 002 (Pipeline) --> Sprint 003 (Corpus) | +| +------------------------------------------------------------------+ | +| | | +| v | +| Uses Batch 001 Sprint 001 (ELF Hashes) for validation | +| | ++-----------------------------------------------------------------------------------+ +``` + +## Cross-Cutting Concerns + +### Determinism Requirements + +All components must follow CLAUDE.md Section 8 determinism rules: + +| Requirement | Implementation | +|-------------|----------------| +| Timestamps | Inject `TimeProvider`, use UTC ISO-8601 | +| IDs | Inject `IGuidGenerator` or derive from content | +| Ordering | Sort sections by name, layers by index | +| JSON | RFC 8785 canonical encoding for hashing | +| Hashes | SHA-256 lowercase hex, no prefix | + +### DSSE/In-Toto Standards + +| Standard | Version | Usage | +|----------|---------|-------| +| DSSE | v1 | Envelope format for all attestations | +| In-Toto | v1.0 | Predicate wrapper (`_type`, `subject`, `predicateType`) | +| BinaryDiffV1 | 1.0.0 | Custom predicate for binary diff attestations | +| Rekor | v1 | Optional transparency log integration | + +### Test Requirements + +| Category | Coverage | +|----------|----------| +| Unit | All public APIs, serialization round-trips | +| Integration | End-to-end with test containers | +| Determinism | Identical inputs produce identical outputs | +| Golden | Validation against known-good corpus | + +## File Manifest + +### Sprint Files + +``` +docs/implplan/ ++-- SPRINT_20260113_000_MASTER_INDEX_oci_binary_integrity.md # This file +| ++-- Batch 001: Binary Diff Attestation +| +-- SPRINT_20260113_001_000_INDEX_binary_diff_attestation.md +| +-- SPRINT_20260113_001_001_SCANNER_elf_section_hashes.md +| +-- SPRINT_20260113_001_002_ATTESTOR_binary_diff_predicate.md +| +-- SPRINT_20260113_001_003_CLI_binary_diff_command.md +| +-- SPRINT_20260113_001_004_DOCS_binary_diff_attestation.md +| ++-- Batch 002: Image Index Resolution +| +-- SPRINT_20260113_002_000_INDEX_image_index_resolution.md +| +-- SPRINT_20260113_002_001_SCANNER_image_inspector_service.md +| +-- SPRINT_20260113_002_002_CLI_image_inspect_command.md +| +-- SPRINT_20260113_002_003_DOCS_image_inspection.md +| ++-- Batch 003: VEX Evidence Linking +| +-- SPRINT_20260113_003_000_INDEX_vex_evidence_linking.md +| +-- SPRINT_20260113_003_001_EXCITITOR_vex_evidence_linker.md +| +-- SPRINT_20260113_003_002_CLI_vex_evidence_integration.md +| ++-- Batch 004: Golden Pairs Pilot + +-- SPRINT_20260113_004_000_INDEX_golden_pairs_pilot.md + +-- SPRINT_20260113_004_001_TOOLS_golden_pairs_data_model.md + +-- SPRINT_20260113_004_002_TOOLS_mirror_diff_pipeline.md + +-- SPRINT_20260113_004_003_TOOLS_pilot_corpus.md +``` + +### Schema Files + +``` +docs/schemas/ ++-- binarydiff-v1.schema.json # Binary diff attestation (Batch 001) ++-- golden-pair-v1.schema.json # Golden pair metadata (Batch 004) ++-- golden-pairs-index.schema.json # Corpus index (Batch 004) +``` + +### Source Directories + +``` +src/ ++-- Scanner/ +| +-- __Libraries/ +| +-- StellaOps.Scanner.Analyzers.Native/ +| +-- Sections/ # ELF/PE section hash extraction ++-- Attestor/ +| +-- StellaOps.Attestor.Core/ +| +-- Predicates/ +| +-- BinaryDiffV1.cs # Binary diff predicate ++-- Excititor/ +| +-- __Libraries/ +| +-- StellaOps.Excititor.Core/ +| +-- Evidence/ # VEX evidence linking ++-- Cli/ +| +-- StellaOps.Cli/ +| +-- Commands/ +| +-- BinaryDiffCommandGroup.cs +| +-- ImageInspectCommandGroup.cs ++-- Tools/ + +-- GoldenPairs/ + +-- StellaOps.Tools.GoldenPairs/ + +datasets/ ++-- golden-pairs/ + +-- index.json + +-- README.md + +-- CVE-2022-0847/ + +-- CVE-2021-3156/ +``` + +## Success Metrics + +### Functional Metrics + +| Metric | Target | +|--------|--------| +| ELF section hash accuracy | 100% match with reference implementation | +| Binary diff verdict accuracy | >= 95% on golden pairs corpus | +| Attestation verification | 100% pass Rekor/in-toto validation | +| VEX evidence link coverage | >= 90% of applicable entries | + +### Performance Metrics + +| Metric | Target | +|--------|--------| +| Section hash extraction | < 100ms per binary | +| Binary diff comparison | < 500ms per pair | +| Image index resolution | < 2s for multi-arch images | + +## Risk Register + +| Risk | Likelihood | Impact | Mitigation | +|------|------------|--------|------------| +| PE section hashing complexity | Medium | Medium | Defer PrintNightmare if PE not ready | +| Large kernel binaries | Medium | Low | Extract specific .ko modules | +| Package archive availability | Medium | High | Cache packages locally | +| Cross-platform DSSE signing | Low | Medium | Use portable signing libraries | + +## Execution Schedule + +### Recommended Order + +1. **Week 1-2:** Batch 001 Sprints 1-2 (ELF hashes, predicate) +2. **Week 2-3:** Batch 002 Sprint 1 (image inspector) + Batch 004 Sprint 1 (data model) +3. **Week 3-4:** Batch 001 Sprint 3 (CLI) + Batch 002 Sprint 2 (CLI) +4. **Week 4-5:** Batch 003 (VEX linking) + Batch 004 Sprint 2 (pipeline) +5. **Week 5-6:** Documentation sprints + Batch 004 Sprint 3 (corpus) + +### Parallelization Opportunities + +- Batch 004 Sprint 1 can start immediately (no dependencies) +- Documentation sprints can run in parallel with implementation +- Batch 002 Sprint 1 can start after Batch 001 Sprint 1 + +## Execution Log + +| Date (UTC) | Update | Owner | +|------------|--------|-------| +| 2026-01-13 | Master index created from product advisory analysis. | Project Mgmt | +| 2026-01-13 | Batch 001 INDEX already existed; added to master index. | Project Mgmt | +| 2026-01-13 | Batches 002, 003, 004 sprint files created. | Project Mgmt | + +## Decisions & Risks + +- **APPROVED 2026-01-13**: Four-batch structure covering full advisory scope. +- **APPROVED 2026-01-13**: ELF-first approach; PE support conditional on Batch 001 progress. +- **APPROVED 2026-01-13**: Golden pairs stored in datasets/, not git LFS initially. +- **APPROVED 2026-01-13**: VEX evidence linking extends existing Excititor module. +- **RISK**: PrintNightmare (PE) may be deferred if PE section hashing not ready. +- **RISK**: Kernel binaries are large; may need to extract specific modules. + +## Next Checkpoints + +- Batch 001 complete -> Core binary diff infrastructure operational +- Batch 002 complete -> Multi-arch image inspection available +- Batch 003 complete -> VEX entries include evidence links +- Batch 004 complete -> Validation corpus ready for CI integration +- All batches complete -> Full OCI layer-level integrity verification operational + +## References + +- [OCI Image Index Specification](https://github.com/opencontainers/image-spec/blob/main/image-index.md) +- [DSSE Specification](https://github.com/secure-systems-lab/dsse) +- [In-Toto Attestation Framework](https://github.com/in-toto/attestation) +- [CycloneDX VEX](https://cyclonedx.org/capabilities/vex/) +- [ELF Specification](https://refspecs.linuxfoundation.org/elf/elf.pdf) +- [PE Format](https://docs.microsoft.com/en-us/windows/win32/debug/pe-format) diff --git a/docs/implplan/SPRINT_20260113_001_000_INDEX_binary_diff_attestation.md b/docs/implplan/SPRINT_20260113_001_000_INDEX_binary_diff_attestation.md new file mode 100644 index 000000000..142cb1ba0 --- /dev/null +++ b/docs/implplan/SPRINT_20260113_001_000_INDEX_binary_diff_attestation.md @@ -0,0 +1,174 @@ +# Sprint Batch 20260113_001 - Binary Diff Attestation (ELF Section Hashes) + +## Executive Summary + +This sprint batch implements **targeted enhancements** for binary-level image integrity verification, focusing on ELF section-level hashing for vendor backport detection and DSSE-signed attestations for binary diffs. This addresses the genuine gaps identified in the OCI Layer-Level Image Integrity advisory analysis while avoiding redundant work on already-implemented capabilities. + +**Scope:** ELF-only (PE/Mach-O deferred to M2+) +**Effort Estimate:** 5-7 story points across 4 sprints +**Priority:** Medium (enhancement, not blocking) + +## Background + +### Advisory Analysis Summary + +The original product advisory proposed comprehensive OCI layer-level verification capabilities. Analysis revealed: + +| Category | Coverage | +|----------|----------| +| **Already Implemented** | ~80% (OCI manifest parsing, layer SBOM fragmentation, DSSE pipeline, VEX emission) | +| **Partial Overlap** | ~15% (ELF symbols exist, section hashes missing) | +| **Genuine Gaps** | ~5% (section hashes, BinaryDiffV1 predicate, CLI diff verb) | + +This batch addresses only the genuine gaps to maximize value while avoiding redundant effort. + +### Existing Capabilities (No Work Needed) + +- OCI manifest/index parsing with Docker & OCI media types +- Per-layer SBOM fragmentation with three-way diff +- DSSE envelope creation → Attestor → Rekor pipeline +- VEX emission with trust scoring and evidence links +- ELF Build-ID, symbol table parsing, link graph analysis + +### New Capabilities (This Batch) + +1. **ELF Section Hash Extractor** - SHA-256 per `.text`, `.rodata`, `.data`, `.symtab` sections +2. **BinaryDiffV1 In-Toto Predicate** - Schema for binary-level diff attestations +3. **CLI `stella scan diff --mode=elf`** - Binary-section-level diff with DSSE output +4. **Documentation** - Architecture docs and CLI reference updates + +## Sprint Index + +| Sprint | ID | Module | Topic | Status | Owner | +|--------|-----|--------|-------|--------|-------| +| 1 | SPRINT_20260113_001_001 | SCANNER | ELF Section Hash Extractor | TODO | Guild - Scanner | +| 2 | SPRINT_20260113_001_002 | ATTESTOR | BinaryDiffV1 In-Toto Predicate | TODO | Guild - Attestor | +| 3 | SPRINT_20260113_001_003 | CLI | Binary Diff Command Enhancement | TODO | Guild - CLI | +| 4 | SPRINT_20260113_001_004 | DOCS | Documentation & Architecture | TODO | Guild - Docs | + +## Dependencies + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ Dependency Graph │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ Sprint 1 (ELF Section Hashes) │ +│ │ │ +│ ├──────────────────┐ │ +│ ▼ ▼ │ +│ Sprint 2 (Predicate) Sprint 4 (Docs) │ +│ │ │ │ +│ ▼ │ │ +│ Sprint 3 (CLI) ─────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +- **Sprint 1** is foundational (no dependencies) +- **Sprint 2** depends on Sprint 1 (uses section hash models) +- **Sprint 3** depends on Sprint 1 & 2 (consumes extractor and predicate) +- **Sprint 4** can proceed in parallel with Sprints 2-3 + +## Acceptance Criteria (Batch-Level) + +### Must Have + +1. **Section Hash Extraction** + - Compute SHA-256 for `.text`, `.rodata`, `.data`, `.symtab` ELF sections + - Deterministic output (stable ordering, canonical JSON) + - Evidence properties in SBOM components + +2. **BinaryDiffV1 Predicate** + - In-toto compliant predicate schema + - Subjects: image@digest, platform + - Inputs: base/target manifests + - Findings: per-path section deltas + +3. **CLI Integration** + - `stella scan diff --mode=elf` produces binary-section-level diff + - `--emit-dsse=` outputs signed attestations + - Human-readable and JSON output formats + +4. **Documentation** + - Architecture doc under `docs/modules/scanner/` + - CLI reference updates + - Predicate schema specification + +### Should Have + +- Confidence scoring for section hash matches (0.0-1.0) +- Integration with existing VEX evidence blocks + +### Deferred (Out of Scope) + +- PE/Mach-O section analysis (M2) +- Vendor backport corpus and 95% precision target (follow-up sprint) +- `ctr images export` integration (use existing OCI blob pull) +- Multi-platform diff in single invocation + +## Technical Context + +### Key Files to Extend + +| Component | File | Purpose | +|-----------|------|---------| +| ELF Analysis | `src/Scanner/StellaOps.Scanner.Analyzers.Native/Hardening/ElfHardeningExtractor.cs` | Add section hash extraction | +| Native Models | `src/Scanner/__Libraries/StellaOps.Scanner.Contracts/CallGraphModels.cs` | Section hash models | +| DSSE Signing | `src/Scanner/__Libraries/StellaOps.Scanner.Reachability/Witnesses/WitnessDsseSigner.cs` | Pattern for BinaryDiffSigner | +| Predicates | `src/Attestor/__Libraries/StellaOps.Attestor.StandardPredicates/` | Add BinaryDiffV1 | +| CLI | `src/Cli/StellaOps.Cli/Commands/` | Add diff subcommand | + +### Determinism Requirements + +Per CLAUDE.md Section 8: + +1. **TimeProvider injection** - No `DateTime.UtcNow` calls +2. **Stable ordering** - Section hashes sorted by section name +3. **Canonical JSON** - RFC 8785 for digest computation +4. **InvariantCulture** - All formatting/parsing +5. **DSSE PAE compliance** - Use shared `DsseHelper` + +## Risk Assessment + +| Risk | Likelihood | Impact | Mitigation | +|------|------------|--------|------------| +| Section hash instability across compilers | Medium | High | Document compiler/flag assumptions; use position-independent matching as fallback | +| ELF parsing edge cases | Low | Medium | Comprehensive test fixtures; existing ELF library handles most cases | +| CLI integration conflicts | Low | Low | CLI tests blocked by other agent; coordinate ownership | + +## Success Metrics + +- [ ] All unit tests pass (100% of new code covered) +- [ ] Integration tests with synthetic ELF fixtures pass +- [ ] CLI help and completions work +- [ ] Documentation builds without warnings +- [ ] No regressions in existing Scanner tests + +## Documentation Prerequisites + +Before starting implementation, reviewers must read: + +- `docs/README.md` +- `docs/ARCHITECTURE_REFERENCE.md` +- `docs/modules/scanner/architecture.md` (if exists) +- `CLAUDE.md` Section 8 (Code Quality & Determinism Rules) +- `src/Scanner/StellaOps.Scanner.Analyzers.Native/AGENTS.md` (if exists) + +## Execution Log + +| Date (UTC) | Update | Owner | +|------------|--------|-------| +| 2026-01-13 | Sprint batch created from advisory analysis; 4 sprints defined. | Project Mgmt | + +## Decisions & Risks + +- **APPROVED 2026-01-13**: Scope limited to ELF-only; PE/Mach-O deferred to M2. +- **APPROVED 2026-01-13**: 80% precision target for initial release; 95% deferred to corpus sprint. +- **RISK**: CLI tests currently blocked by other agent work; Sprint 3 may need coordination. + +## Next Checkpoints + +- Sprint 1 completion → Sprint 2 & 4 can start +- Sprint 2 completion → Sprint 3 can start +- All sprints complete → Integration testing checkpoint diff --git a/docs/implplan/SPRINT_20260113_001_001_SCANNER_elf_section_hashes.md b/docs/implplan/SPRINT_20260113_001_001_SCANNER_elf_section_hashes.md new file mode 100644 index 000000000..a1cc32fcd --- /dev/null +++ b/docs/implplan/SPRINT_20260113_001_001_SCANNER_elf_section_hashes.md @@ -0,0 +1,234 @@ +# Sprint 20260113_001_001_SCANNER - ELF Section Hash Extractor + +## Topic & Scope + +- Implement per-section SHA-256 hash extraction for ELF binaries +- Target sections: `.text`, `.rodata`, `.data`, `.symtab`, `.dynsym` +- Integrate with existing `ElfHardeningExtractor` infrastructure +- Expose section hashes as SBOM component evidence properties +- **Working directory:** `src/Scanner/StellaOps.Scanner.Analyzers.Native/` + +## Dependencies & Concurrency + +- No blocking dependencies (foundational sprint) +- Parallel work safe within Scanner.Native module +- Sprint 2 (BinaryDiffV1 predicate) depends on this sprint's models + +## Documentation Prerequisites + +- `docs/README.md` +- `docs/ARCHITECTURE_REFERENCE.md` +- `CLAUDE.md` Section 8 (Determinism Rules) +- `src/Scanner/StellaOps.Scanner.Analyzers.Native/AGENTS.md` (if exists) +- ELF specification reference (https://refspecs.linuxfoundation.org/elf/elf.pdf) + +## Delivery Tracker + +| # | Task ID | Status | Key dependency / next step | Owners | Task Definition | +|---|---------|--------|---------------------------|--------|-----------------| +| 1 | ELF-SECTION-MODELS-0001 | TODO | None | Guild - Scanner | Define `ElfSectionHash` and `ElfSectionHashSet` models in `src/Scanner/__Libraries/StellaOps.Scanner.Contracts/`. Include section name, offset, size, SHA-256 hash, and optional BLAKE3 hash. | +| 2 | ELF-SECTION-EXTRACTOR-0001 | TODO | Depends on ELF-SECTION-MODELS-0001 | Guild - Scanner | Implement `ElfSectionHashExtractor` class that reads ELF sections and computes per-section hashes. Integrate with existing ELF parsing in `ElfHardeningExtractor`. | +| 3 | ELF-SECTION-CONFIG-0001 | TODO | Depends on ELF-SECTION-EXTRACTOR-0001 | Guild - Scanner | Add configuration options for section hash extraction: enabled/disabled, section allowlist, hash algorithms. Use `IOptions` with `ValidateOnStart`. | +| 4 | ELF-SECTION-EVIDENCE-0001 | TODO | Depends on ELF-SECTION-EXTRACTOR-0001 | Guild - Scanner | Emit section hashes as SBOM component `properties[]` with keys: `evidence:section::sha256`, `evidence:section::blake3`, `evidence:section::size`. | +| 5 | ELF-SECTION-DI-0001 | TODO | Depends on all above | Guild - Scanner | Register `ElfSectionHashExtractor` in `ServiceCollectionExtensions.cs`. Ensure `TimeProvider` and `IGuidGenerator` are injected for determinism. | +| 6 | ELF-SECTION-TESTS-0001 | TODO | Depends on all above | Guild - Scanner | Add unit tests in `src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Native.Tests/` covering: valid ELF with all sections, stripped ELF (missing symtab), malformed ELF, empty sections, large binaries. | +| 7 | ELF-SECTION-FIXTURES-0001 | TODO | Depends on ELF-SECTION-TESTS-0001 | Guild - Scanner | Create synthetic ELF test fixtures under `src/Scanner/__Tests/__Datasets/elf-section-hashes/` with known section contents for golden hash verification. | +| 8 | ELF-SECTION-DETERMINISM-0001 | TODO | Depends on all above | Guild - Scanner | Add determinism regression test: same ELF input produces identical section hashes across runs. Use `FakeTimeProvider` and fixed GUID generator. | + +## Technical Specification + +### ElfSectionHash Model + +```csharp +namespace StellaOps.Scanner.Contracts; + +/// +/// Represents a cryptographic hash of an ELF section. +/// +public sealed record ElfSectionHash +{ + /// Section name (e.g., ".text", ".rodata"). + public required string Name { get; init; } + + /// Section offset in file. + public required long Offset { get; init; } + + /// Section size in bytes. + public required long Size { get; init; } + + /// SHA-256 hash of section contents (lowercase hex). + public required string Sha256 { get; init; } + + /// Optional BLAKE3-256 hash of section contents (lowercase hex). + public string? Blake3 { get; init; } + + /// Section type from ELF header. + public required ElfSectionType SectionType { get; init; } + + /// Section flags from ELF header. + public required ElfSectionFlags Flags { get; init; } +} + +/// +/// Collection of section hashes for a single ELF binary. +/// +public sealed record ElfSectionHashSet +{ + /// Path to the ELF binary. + public required string FilePath { get; init; } + + /// SHA-256 hash of the entire file. + public required string FileHash { get; init; } + + /// Build-ID from .note.gnu.build-id if present. + public string? BuildId { get; init; } + + /// Section hashes, sorted by section name. + public required ImmutableArray Sections { get; init; } + + /// Extraction timestamp (UTC ISO-8601). + public required DateTimeOffset ExtractedAt { get; init; } + + /// Extractor version for reproducibility. + public required string ExtractorVersion { get; init; } +} +``` + +### Extractor Interface + +```csharp +namespace StellaOps.Scanner.Analyzers.Native; + +public interface IElfSectionHashExtractor +{ + /// + /// Extracts section hashes from an ELF binary. + /// + /// Path to the ELF file. + /// Cancellation token. + /// Section hash set, or null if not a valid ELF. + Task ExtractAsync( + string elfPath, + CancellationToken cancellationToken = default); + + /// + /// Extracts section hashes from ELF bytes in memory. + /// + Task ExtractFromBytesAsync( + ReadOnlyMemory elfBytes, + string virtualPath, + CancellationToken cancellationToken = default); +} +``` + +### Target Sections + +| Section | Purpose | Backport Relevance | +|---------|---------|-------------------| +| `.text` | Executable code | **High** - patched functions change this | +| `.rodata` | Read-only data | Medium - string constants may change | +| `.data` | Initialized data | Low - rarely changes for patches | +| `.symtab` | Symbol table | **High** - function signatures | +| `.dynsym` | Dynamic symbols | **High** - exported API | +| `.gnu.hash` | GNU hash table | Low - derived from symbols | + +### SBOM Evidence Properties + +```json +{ + "type": "library", + "name": "libssl.so.3", + "properties": [ + {"name": "evidence:build-id", "value": "abc123..."}, + {"name": "evidence:section:.text:sha256", "value": "e3b0c442..."}, + {"name": "evidence:section:.text:size", "value": "1048576"}, + {"name": "evidence:section:.rodata:sha256", "value": "d7a8fbb3..."}, + {"name": "evidence:section:.symtab:sha256", "value": "9f86d081..."}, + {"name": "evidence:section-set:sha256", "value": "combined_hash..."}, + {"name": "evidence:extractor-version", "value": "1.0.0"} + ] +} +``` + +### Determinism Requirements + +1. **Ordering**: Sections sorted lexicographically by name +2. **Hash format**: Lowercase hexadecimal, no prefix +3. **Timestamps**: From injected `TimeProvider.GetUtcNow()` +4. **Version string**: Assembly version or build metadata +5. **JSON serialization**: RFC 8785 canonical for any digest computation + +### Configuration Schema + +```yaml +scanner: + native: + sectionHashes: + enabled: true + algorithms: + - sha256 + - blake3 # optional + sections: + - .text + - .rodata + - .data + - .symtab + - .dynsym + maxSectionSize: 104857600 # 100MB limit per section +``` + +## Test Cases + +### Unit Tests + +| Test | Description | Expected | +|------|-------------|----------| +| `ExtractAsync_ValidElf_ReturnsAllSections` | Standard ELF with all target sections | All 5 sections extracted with valid hashes | +| `ExtractAsync_StrippedElf_OmitsSymtab` | Stripped binary without .symtab | Only .text, .rodata, .data returned | +| `ExtractAsync_InvalidElf_ReturnsNull` | Non-ELF file (PE, Mach-O, random) | Returns null, no exception | +| `ExtractAsync_EmptySection_ReturnsEmptyHash` | ELF with zero-size .data | Hash of empty content (`e3b0c442...`) | +| `ExtractAsync_LargeSection_RespectsLimit` | Section > maxSectionSize | Section skipped or truncated per config | +| `ExtractAsync_Deterministic_SameOutput` | Same ELF, multiple runs | Identical `ElfSectionHashSet` | +| `ExtractFromBytesAsync_SameAsFile` | Memory vs file extraction | Identical results | + +### Integration Tests + +| Test | Description | Expected | +|------|-------------|----------| +| `LayerAnalysis_ElfWithSections_EmitsEvidence` | Container layer with ELF binaries | SBOM components have section hash properties | +| `Diff_SameBinaryDifferentPatch_DetectsSectionChange` | Two builds with backport | `.text` hash differs, other sections same | + +### Fixtures + +Create under `src/Scanner/__Tests/__Datasets/elf-section-hashes/`: + +``` +elf-section-hashes/ +├── README.md # Fixture documentation +├── standard-amd64.elf # Standard ELF with all sections +├── standard-amd64.golden.json # Expected section hashes +├── stripped-amd64.elf # Stripped binary +├── stripped-amd64.golden.json +├── minimal-arm64.elf # Minimal ELF (few sections) +├── minimal-arm64.golden.json +└── corrupt.bin # Invalid ELF magic +``` + +## Execution Log + +| Date (UTC) | Update | Owner | +|------------|--------|-------| +| 2026-01-13 | Sprint created from advisory analysis. | Project Mgmt | + +## Decisions & Risks + +- **APPROVED**: SHA-256 as primary hash; BLAKE3 optional for performance. +- **APPROVED**: 100MB per-section limit to prevent memory exhaustion. +- **RISK**: Some ELF parsers may handle edge cases differently; use LibObjectFile or similar well-tested library. +- **RISK**: Section ordering may vary by toolchain; normalize by sorting. + +## Next Checkpoints + +- Task 1-2 complete → Models and extractor ready for integration +- Task 6-8 complete → Sprint can be marked DONE +- Unblock Sprint 2 (BinaryDiffV1 predicate) diff --git a/docs/implplan/SPRINT_20260113_001_002_ATTESTOR_binary_diff_predicate.md b/docs/implplan/SPRINT_20260113_001_002_ATTESTOR_binary_diff_predicate.md new file mode 100644 index 000000000..9703a91d2 --- /dev/null +++ b/docs/implplan/SPRINT_20260113_001_002_ATTESTOR_binary_diff_predicate.md @@ -0,0 +1,441 @@ +# Sprint 20260113_001_002_ATTESTOR - BinaryDiffV1 In-Toto Predicate + +## Topic & Scope + +- Define `BinaryDiffV1` in-toto predicate schema for binary-level diff attestations +- Implement predicate builder and serializer +- Integrate with existing DSSE signing infrastructure +- Support both ELF section diffs and future PE/Mach-O extensions +- **Working directory:** `src/Attestor/__Libraries/StellaOps.Attestor.StandardPredicates/` + +## Dependencies & Concurrency + +- **Depends on:** Sprint 001 (ELF Section Hash models) +- Parallel work safe within Attestor module +- Sprint 3 (CLI) depends on this sprint + +## Documentation Prerequisites + +- `docs/README.md` +- `docs/ARCHITECTURE_REFERENCE.md` +- `CLAUDE.md` Section 8 (Determinism Rules) +- in-toto attestation specification (https://github.com/in-toto/attestation/blob/main/spec/v1/statement.md) +- DSSE envelope specification (https://github.com/secure-systems-lab/dsse/blob/master/envelope.md) +- Existing predicates: `src/Attestor/__Libraries/StellaOps.Attestor.StandardPredicates/` + +## Delivery Tracker + +| # | Task ID | Status | Key dependency / next step | Owners | Task Definition | +|---|---------|--------|---------------------------|--------|-----------------| +| 1 | BINARYDIFF-SCHEMA-0001 | TODO | Sprint 001 models | Guild - Attestor | Define `BinaryDiffV1` predicate schema with JSON Schema and C# models. Include subjects, inputs, findings, and verification materials. | +| 2 | BINARYDIFF-MODELS-0001 | TODO | Depends on BINARYDIFF-SCHEMA-0001 | Guild - Attestor | Implement C# record types for `BinaryDiffPredicate`, `BinaryDiffSubject`, `BinaryDiffInput`, `BinaryDiffFinding`, `SectionDelta`. | +| 3 | BINARYDIFF-BUILDER-0001 | TODO | Depends on BINARYDIFF-MODELS-0001 | Guild - Attestor | Implement `BinaryDiffPredicateBuilder` with fluent API for constructing predicates from section hash comparisons. | +| 4 | BINARYDIFF-SERIALIZER-0001 | TODO | Depends on BINARYDIFF-MODELS-0001 | Guild - Attestor | Implement canonical JSON serialization using RFC 8785. Register with existing `IPredicateSerializer` infrastructure. | +| 5 | BINARYDIFF-SIGNER-0001 | TODO | Depends on all above | Guild - Attestor | Implement `BinaryDiffDsseSigner` following `WitnessDsseSigner` pattern. Payload type: `stellaops.binarydiff.v1`. | +| 6 | BINARYDIFF-VERIFIER-0001 | TODO | Depends on BINARYDIFF-SIGNER-0001 | Guild - Attestor | Implement `BinaryDiffDsseVerifier` for signature and schema validation. | +| 7 | BINARYDIFF-DI-0001 | TODO | Depends on all above | Guild - Attestor | Register all services in DI. Add `IOptions` for configuration. | +| 8 | BINARYDIFF-TESTS-0001 | TODO | Depends on all above | Guild - Attestor | Add comprehensive unit tests covering: schema validation, serialization round-trip, signing/verification, edge cases (empty findings, large diffs). | +| 9 | BINARYDIFF-JSONSCHEMA-0001 | TODO | Depends on BINARYDIFF-SCHEMA-0001 | Guild - Attestor | Publish JSON Schema to `docs/schemas/binarydiff-v1.schema.json` for external validation. | + +## Technical Specification + +### Predicate Type + +``` +stellaops.binarydiff.v1 +``` + +### BinaryDiffV1 Schema + +```json +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://stellaops.io/schemas/binarydiff-v1.schema.json", + "title": "BinaryDiffV1", + "description": "In-toto predicate for binary-level diff attestations", + "type": "object", + "required": ["predicateType", "subjects", "inputs", "findings", "metadata"], + "properties": { + "predicateType": { + "const": "stellaops.binarydiff.v1" + }, + "subjects": { + "type": "array", + "items": { "$ref": "#/$defs/BinaryDiffSubject" }, + "minItems": 1 + }, + "inputs": { + "$ref": "#/$defs/BinaryDiffInputs" + }, + "findings": { + "type": "array", + "items": { "$ref": "#/$defs/BinaryDiffFinding" } + }, + "metadata": { + "$ref": "#/$defs/BinaryDiffMetadata" + } + }, + "$defs": { + "BinaryDiffSubject": { + "type": "object", + "required": ["name", "digest"], + "properties": { + "name": { + "type": "string", + "description": "Image reference (e.g., docker://repo/app@sha256:...)" + }, + "digest": { + "type": "object", + "additionalProperties": { "type": "string" } + }, + "platform": { + "$ref": "#/$defs/Platform" + } + } + }, + "BinaryDiffInputs": { + "type": "object", + "required": ["base", "target"], + "properties": { + "base": { "$ref": "#/$defs/ImageReference" }, + "target": { "$ref": "#/$defs/ImageReference" } + } + }, + "ImageReference": { + "type": "object", + "required": ["digest"], + "properties": { + "reference": { "type": "string" }, + "digest": { "type": "string" }, + "manifestDigest": { "type": "string" }, + "platform": { "$ref": "#/$defs/Platform" } + } + }, + "Platform": { + "type": "object", + "properties": { + "os": { "type": "string" }, + "architecture": { "type": "string" }, + "variant": { "type": "string" } + } + }, + "BinaryDiffFinding": { + "type": "object", + "required": ["path", "changeType", "binaryFormat"], + "properties": { + "path": { + "type": "string", + "description": "File path within the image filesystem" + }, + "changeType": { + "enum": ["added", "removed", "modified", "unchanged"] + }, + "binaryFormat": { + "enum": ["elf", "pe", "macho", "unknown"] + }, + "layerDigest": { + "type": "string", + "description": "Layer that introduced this change" + }, + "baseHashes": { + "$ref": "#/$defs/SectionHashSet" + }, + "targetHashes": { + "$ref": "#/$defs/SectionHashSet" + }, + "sectionDeltas": { + "type": "array", + "items": { "$ref": "#/$defs/SectionDelta" } + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "verdict": { + "enum": ["patched", "vanilla", "unknown", "incompatible"] + } + } + }, + "SectionHashSet": { + "type": "object", + "properties": { + "buildId": { "type": "string" }, + "fileHash": { "type": "string" }, + "sections": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/SectionInfo" + } + } + } + }, + "SectionInfo": { + "type": "object", + "required": ["sha256", "size"], + "properties": { + "sha256": { "type": "string" }, + "blake3": { "type": "string" }, + "size": { "type": "integer" } + } + }, + "SectionDelta": { + "type": "object", + "required": ["section", "status"], + "properties": { + "section": { + "type": "string", + "description": "Section name (e.g., .text, .rodata)" + }, + "status": { + "enum": ["identical", "modified", "added", "removed"] + }, + "baseSha256": { "type": "string" }, + "targetSha256": { "type": "string" }, + "sizeDelta": { "type": "integer" } + } + }, + "BinaryDiffMetadata": { + "type": "object", + "required": ["toolVersion", "analysisTimestamp"], + "properties": { + "toolVersion": { "type": "string" }, + "analysisTimestamp": { + "type": "string", + "format": "date-time" + }, + "configDigest": { "type": "string" }, + "totalBinaries": { "type": "integer" }, + "modifiedBinaries": { "type": "integer" }, + "analyzedSections": { + "type": "array", + "items": { "type": "string" } + } + } + } + } +} +``` + +### C# Model Classes + +```csharp +namespace StellaOps.Attestor.StandardPredicates.BinaryDiff; + +/// +/// BinaryDiffV1 predicate for in-toto attestations. +/// +public sealed record BinaryDiffPredicate +{ + public const string PredicateType = "stellaops.binarydiff.v1"; + + public required ImmutableArray Subjects { get; init; } + public required BinaryDiffInputs Inputs { get; init; } + public required ImmutableArray Findings { get; init; } + public required BinaryDiffMetadata Metadata { get; init; } +} + +public sealed record BinaryDiffSubject +{ + public required string Name { get; init; } + public required ImmutableDictionary Digest { get; init; } + public Platform? Platform { get; init; } +} + +public sealed record BinaryDiffInputs +{ + public required ImageReference Base { get; init; } + public required ImageReference Target { get; init; } +} + +public sealed record ImageReference +{ + public string? Reference { get; init; } + public required string Digest { get; init; } + public string? ManifestDigest { get; init; } + public Platform? Platform { get; init; } +} + +public sealed record Platform +{ + public required string Os { get; init; } + public required string Architecture { get; init; } + public string? Variant { get; init; } +} + +public sealed record BinaryDiffFinding +{ + public required string Path { get; init; } + public required ChangeType ChangeType { get; init; } + public required BinaryFormat BinaryFormat { get; init; } + public string? LayerDigest { get; init; } + public SectionHashSet? BaseHashes { get; init; } + public SectionHashSet? TargetHashes { get; init; } + public ImmutableArray SectionDeltas { get; init; } + public double? Confidence { get; init; } + public Verdict? Verdict { get; init; } +} + +public enum ChangeType { Added, Removed, Modified, Unchanged } +public enum BinaryFormat { Elf, Pe, Macho, Unknown } +public enum Verdict { Patched, Vanilla, Unknown, Incompatible } + +public sealed record SectionHashSet +{ + public string? BuildId { get; init; } + public required string FileHash { get; init; } + public required ImmutableDictionary Sections { get; init; } +} + +public sealed record SectionInfo +{ + public required string Sha256 { get; init; } + public string? Blake3 { get; init; } + public required long Size { get; init; } +} + +public sealed record SectionDelta +{ + public required string Section { get; init; } + public required SectionStatus Status { get; init; } + public string? BaseSha256 { get; init; } + public string? TargetSha256 { get; init; } + public long? SizeDelta { get; init; } +} + +public enum SectionStatus { Identical, Modified, Added, Removed } + +public sealed record BinaryDiffMetadata +{ + public required string ToolVersion { get; init; } + public required DateTimeOffset AnalysisTimestamp { get; init; } + public string? ConfigDigest { get; init; } + public int TotalBinaries { get; init; } + public int ModifiedBinaries { get; init; } + public ImmutableArray AnalyzedSections { get; init; } +} +``` + +### Builder API + +```csharp +public interface IBinaryDiffPredicateBuilder +{ + IBinaryDiffPredicateBuilder WithSubject(string name, string digest, Platform? platform = null); + IBinaryDiffPredicateBuilder WithInputs(ImageReference baseImage, ImageReference targetImage); + IBinaryDiffPredicateBuilder AddFinding(BinaryDiffFinding finding); + IBinaryDiffPredicateBuilder WithMetadata(Action configure); + BinaryDiffPredicate Build(); +} +``` + +### DSSE Integration + +```csharp +public interface IBinaryDiffDsseSigner +{ + Task SignAsync( + BinaryDiffPredicate predicate, + CancellationToken cancellationToken = default); +} + +public sealed record BinaryDiffDsseResult +{ + public required string PayloadType { get; init; } // stellaops.binarydiff.v1 + public required byte[] Payload { get; init; } + public required ImmutableArray Signatures { get; init; } + public required string EnvelopeJson { get; init; } + public string? RekorLogIndex { get; init; } + public string? RekorEntryId { get; init; } +} +``` + +### In-Toto Statement Wrapper + +```json +{ + "_type": "https://in-toto.io/Statement/v1", + "subject": [ + { + "name": "docker://registry.example.com/app@sha256:abc123...", + "digest": { + "sha256": "abc123..." + } + } + ], + "predicateType": "stellaops.binarydiff.v1", + "predicate": { + "inputs": { + "base": { "digest": "sha256:old..." }, + "target": { "digest": "sha256:new..." } + }, + "findings": [ + { + "path": "/usr/lib/libssl.so.3", + "changeType": "modified", + "binaryFormat": "elf", + "sectionDeltas": [ + { "section": ".text", "status": "modified", "baseSha256": "...", "targetSha256": "..." } + ], + "confidence": 0.95, + "verdict": "patched" + } + ], + "metadata": { + "toolVersion": "1.0.0", + "analysisTimestamp": "2026-01-13T12:00:00Z" + } + } +} +``` + +## Determinism Requirements + +1. **Canonical JSON**: RFC 8785 for all serialization before signing +2. **Stable ordering**: Findings sorted by path; sections sorted by name +3. **Timestamps**: From injected `TimeProvider` +4. **Hash computation**: Use shared `CanonicalJsonSerializer` +5. **DSSE PAE**: Use shared `DsseHelper.ComputePreAuthenticationEncoding` + +## Test Cases + +### Unit Tests + +| Test | Description | Expected | +|------|-------------|----------| +| `Serialize_RoundTrip_Identical` | Serialize then deserialize | Identical predicate | +| `Serialize_Canonical_DeterministicOutput` | Same predicate, multiple serializations | Byte-identical JSON | +| `Build_ValidInputs_CreatesPredicate` | Builder with all required fields | Valid predicate | +| `Build_MissingSubject_Throws` | Builder without subject | `ArgumentException` | +| `Sign_ValidPredicate_ReturnsEnvelope` | Sign with test key | Valid DSSE envelope | +| `Verify_ValidEnvelope_Succeeds` | Verify signed envelope | Verification passes | +| `Verify_TamperedPayload_Fails` | Modified payload | Verification fails | +| `Schema_ValidJson_Passes` | Valid JSON against schema | Schema validation passes | +| `Schema_InvalidJson_Fails` | Missing required field | Schema validation fails | + +### Integration Tests + +| Test | Description | Expected | +|------|-------------|----------| +| `SignAndSubmit_RekorIntegration` | Sign and submit to Rekor (test instance) | Log entry created | +| `EndToEnd_DiffToAttestation` | From image diff to signed attestation | Valid DSSE with findings | + +## Execution Log + +| Date (UTC) | Update | Owner | +|------------|--------|-------| +| 2026-01-13 | Sprint created from advisory analysis. | Project Mgmt | + +## Decisions & Risks + +- **APPROVED**: Predicate type `stellaops.binarydiff.v1` follows StellaOps naming convention. +- **APPROVED**: Support both ELF and future PE/Mach-O via `binaryFormat` discriminator. +- **RISK**: Schema evolution requires versioning strategy; defer to v2 if breaking changes needed. +- **RISK**: Large diffs may produce large attestations; consider summary mode for >1000 findings. + +## Next Checkpoints + +- Task 1-4 complete → Schema and models ready for integration +- Task 5-6 complete → Signing/verification operational +- Task 8 complete → Sprint can be marked DONE +- Unblock Sprint 3 (CLI) diff --git a/docs/implplan/SPRINT_20260113_001_003_CLI_binary_diff_command.md b/docs/implplan/SPRINT_20260113_001_003_CLI_binary_diff_command.md new file mode 100644 index 000000000..8b3a31a26 --- /dev/null +++ b/docs/implplan/SPRINT_20260113_001_003_CLI_binary_diff_command.md @@ -0,0 +1,358 @@ +# Sprint 20260113_001_003_CLI - Binary Diff Command Enhancement + +## Topic & Scope + +- Implement `stella scan diff --mode=elf` for binary-section-level diff +- Add `--emit-dsse=` option for DSSE attestation output +- Support human-readable table and JSON output formats +- Integrate with existing scan infrastructure and OCI registry client +- **Working directory:** `src/Cli/StellaOps.Cli/Commands/` + +## Dependencies & Concurrency + +- **Depends on:** Sprint 001 (ELF Section Hash Extractor) +- **Depends on:** Sprint 002 (BinaryDiffV1 Predicate) +- **BLOCKED RISK:** CLI tests under active modification; coordinate before touching test files +- Parallel work safe for command implementation; test coordination required + +## Documentation Prerequisites + +- `docs/README.md` +- `docs/ARCHITECTURE_REFERENCE.md` +- `CLAUDE.md` Section 8 (Determinism Rules) +- `src/Cli/StellaOps.Cli/AGENTS.md` (if exists) +- Existing CLI commands: `src/Cli/StellaOps.Cli/Commands/` +- System.CommandLine documentation + +## Delivery Tracker + +| # | Task ID | Status | Key dependency / next step | Owners | Task Definition | +|---|---------|--------|---------------------------|--------|-----------------| +| 1 | CLI-DIFF-COMMAND-0001 | TODO | Sprint 001 & 002 complete | Guild - CLI | Create `BinaryDiffCommand` class under `Commands/Scan/` implementing `stella scan diff` subcommand with required options. | +| 2 | CLI-DIFF-OPTIONS-0001 | TODO | Depends on CLI-DIFF-COMMAND-0001 | Guild - CLI | Define command options: `--base` (base image ref), `--target` (target image ref), `--mode` (elf/pe/auto), `--emit-dsse` (output dir), `--format` (table/json), `--platform` (os/arch). | +| 3 | CLI-DIFF-SERVICE-0001 | TODO | Depends on CLI-DIFF-OPTIONS-0001 | Guild - CLI | Implement `BinaryDiffService` that orchestrates: image pull, layer extraction, section hash computation, diff computation, predicate building. | +| 4 | CLI-DIFF-RENDERER-0001 | TODO | Depends on CLI-DIFF-SERVICE-0001 | Guild - CLI | Implement `BinaryDiffRenderer` for table and JSON output formats. Table shows path, change type, verdict, confidence. JSON outputs full diff structure. | +| 5 | CLI-DIFF-DSSE-OUTPUT-0001 | TODO | Depends on CLI-DIFF-SERVICE-0001 | Guild - CLI | Implement DSSE output: one envelope per platform manifest, written to `--emit-dsse` directory with naming convention `{platform}-binarydiff.dsse.json`. | +| 6 | CLI-DIFF-PROGRESS-0001 | TODO | Depends on CLI-DIFF-SERVICE-0001 | Guild - CLI | Add progress reporting for long-running operations: layer download progress, binary analysis progress, section hash computation. | +| 7 | CLI-DIFF-DI-0001 | TODO | Depends on all above | Guild - CLI | Register all services in `Program.cs` DI setup. Wire up `IHttpClientFactory`, `IElfSectionHashExtractor`, `IBinaryDiffDsseSigner`. | +| 8 | CLI-DIFF-HELP-0001 | TODO | Depends on CLI-DIFF-COMMAND-0001 | Guild - CLI | Add comprehensive help text, examples, and shell completions for the new command. | +| 9 | CLI-DIFF-TESTS-0001 | BLOCKED | Depends on all above; CLI tests under active modification | Guild - CLI | Add unit tests for command parsing, service logic, and output rendering. Coordinate with other agent before modifying test files. | +| 10 | CLI-DIFF-INTEGRATION-0001 | TODO | Depends on CLI-DIFF-TESTS-0001 | Guild - CLI | Add integration test with synthetic OCI images containing known ELF binaries. Verify end-to-end flow. | + +## Technical Specification + +### Command Syntax + +```bash +# Basic usage +stella scan diff --base --target + +# With binary mode +stella scan diff --base docker://repo/app:1.0.0 --target docker://repo/app:1.0.1 --mode=elf + +# With DSSE output +stella scan diff --base @sha256:abc... --target @sha256:def... \ + --mode=elf --emit-dsse=./attestations/ + +# JSON output +stella scan diff --base image1 --target image2 --format=json > diff.json + +# Specific platform +stella scan diff --base image1 --target image2 --platform=linux/amd64 +``` + +### Command Options + +| Option | Short | Type | Required | Default | Description | +|--------|-------|------|----------|---------|-------------| +| `--base` | `-b` | string | Yes | - | Base image reference (tag or @digest) | +| `--target` | `-t` | string | Yes | - | Target image reference (tag or @digest) | +| `--mode` | `-m` | enum | No | `auto` | Analysis mode: `elf`, `pe`, `auto` | +| `--emit-dsse` | `-d` | path | No | - | Directory for DSSE attestation output | +| `--format` | `-f` | enum | No | `table` | Output format: `table`, `json`, `summary` | +| `--platform` | `-p` | string | No | - | Platform filter (e.g., `linux/amd64`) | +| `--include-unchanged` | - | bool | No | `false` | Include unchanged binaries in output | +| `--sections` | - | string[] | No | all | Sections to analyze (e.g., `.text,.rodata`) | +| `--registry-auth` | - | string | No | - | Path to Docker config for authentication | +| `--timeout` | - | int | No | `300` | Timeout in seconds for operations | +| `--verbose` | `-v` | bool | No | `false` | Enable verbose output | + +### Output Formats + +#### Table Format (Default) + +``` +Binary Diff: docker://repo/app:1.0.0 → docker://repo/app:1.0.1 +Platform: linux/amd64 +Analysis Mode: ELF Section Hashes + +PATH CHANGE VERDICT CONFIDENCE SECTIONS CHANGED +──────────────────────────────────────────────────────────────────────────────── +/usr/lib/libssl.so.3 modified patched 0.95 .text, .rodata +/usr/lib/libcrypto.so.3 modified patched 0.92 .text +/usr/bin/openssl modified unknown 0.75 .text, .data +/usr/lib/libc.so.6 unchanged - - - + +Summary: 4 binaries analyzed, 3 modified, 1 unchanged + Patched: 2, Unknown: 1 +``` + +#### JSON Format + +```json +{ + "schemaVersion": "1.0.0", + "base": { + "reference": "docker://repo/app:1.0.0", + "digest": "sha256:abc123..." + }, + "target": { + "reference": "docker://repo/app:1.0.1", + "digest": "sha256:def456..." + }, + "platform": { + "os": "linux", + "architecture": "amd64" + }, + "analysisMode": "elf", + "timestamp": "2026-01-13T12:00:00Z", + "findings": [ + { + "path": "/usr/lib/libssl.so.3", + "changeType": "modified", + "verdict": "patched", + "confidence": 0.95, + "sectionDeltas": [ + { "section": ".text", "status": "modified" }, + { "section": ".rodata", "status": "modified" } + ] + } + ], + "summary": { + "totalBinaries": 4, + "modified": 3, + "unchanged": 1, + "verdicts": { + "patched": 2, + "unknown": 1 + } + } +} +``` + +#### Summary Format + +``` +Binary Diff Summary +─────────────────── +Base: docker://repo/app:1.0.0 (sha256:abc123...) +Target: docker://repo/app:1.0.1 (sha256:def456...) +Platform: linux/amd64 + +Binaries: 4 total, 3 modified, 1 unchanged +Verdicts: 2 patched, 1 unknown + +DSSE Attestation: ./attestations/linux-amd64-binarydiff.dsse.json +``` + +### DSSE Output Structure + +``` +attestations/ +├── linux-amd64-binarydiff.dsse.json # DSSE envelope +├── linux-amd64-binarydiff.payload.json # Raw predicate (for inspection) +└── linux-arm64-binarydiff.dsse.json # (if multi-arch) +``` + +### Service Architecture + +```csharp +namespace StellaOps.Cli.Services; + +public interface IBinaryDiffService +{ + Task ComputeDiffAsync( + BinaryDiffRequest request, + IProgress? progress = null, + CancellationToken cancellationToken = default); +} + +public sealed record BinaryDiffRequest +{ + public required string BaseImageRef { get; init; } + public required string TargetImageRef { get; init; } + public required BinaryDiffMode Mode { get; init; } + public Platform? Platform { get; init; } + public ImmutableArray? Sections { get; init; } + public bool IncludeUnchanged { get; init; } + public string? RegistryAuthPath { get; init; } +} + +public sealed record BinaryDiffResult +{ + public required ImageReference Base { get; init; } + public required ImageReference Target { get; init; } + public required Platform Platform { get; init; } + public required ImmutableArray Findings { get; init; } + public required BinaryDiffSummary Summary { get; init; } + public BinaryDiffPredicate? Predicate { get; init; } +} + +public sealed record BinaryDiffProgress +{ + public required string Phase { get; init; } // "pulling", "extracting", "analyzing", "diffing" + public required string CurrentItem { get; init; } + public required int Current { get; init; } + public required int Total { get; init; } +} +``` + +### Command Implementation Pattern + +```csharp +namespace StellaOps.Cli.Commands.Scan; + +public class BinaryDiffCommand : Command +{ + public BinaryDiffCommand() : base("diff", "Compare binaries between two images") + { + AddOption(BaseOption); + AddOption(TargetOption); + AddOption(ModeOption); + AddOption(EmitDsseOption); + AddOption(FormatOption); + AddOption(PlatformOption); + // ... other options + } + + public static Option BaseOption { get; } = new( + aliases: ["--base", "-b"], + description: "Base image reference (tag or @digest)") + { + IsRequired = true + }; + + // ... other options + + public new class Handler : ICommandHandler + { + private readonly IBinaryDiffService _diffService; + private readonly IBinaryDiffDsseSigner _signer; + private readonly IBinaryDiffRenderer _renderer; + private readonly IConsole _console; + + public async Task InvokeAsync(InvocationContext context) + { + var cancellationToken = context.GetCancellationToken(); + + // Parse options + var baseRef = context.ParseResult.GetValueForOption(BaseOption)!; + var targetRef = context.ParseResult.GetValueForOption(TargetOption)!; + // ... + + // Execute diff + var progress = new Progress(p => + _console.WriteLine($"[{p.Phase}] {p.CurrentItem} ({p.Current}/{p.Total})")); + + var result = await _diffService.ComputeDiffAsync( + new BinaryDiffRequest { ... }, + progress, + cancellationToken); + + // Emit DSSE if requested + if (!string.IsNullOrEmpty(emitDssePath)) + { + var dsseResult = await _signer.SignAsync(result.Predicate!, cancellationToken); + await WriteDsseAsync(emitDssePath, result.Platform, dsseResult, cancellationToken); + } + + // Render output + await _renderer.RenderAsync(result, format, _console.Out, cancellationToken); + + return 0; + } + } +} +``` + +### Error Handling + +| Error | Exit Code | Message | +|-------|-----------|---------| +| Invalid base image | 1 | `Error: Unable to resolve base image '{ref}': {reason}` | +| Invalid target image | 1 | `Error: Unable to resolve target image '{ref}': {reason}` | +| Authentication failed | 2 | `Error: Registry authentication failed for '{registry}'` | +| Platform not found | 3 | `Error: Platform '{platform}' not found in image index` | +| No ELF binaries | 0 | `Warning: No ELF binaries found in images` (success with warning) | +| Timeout | 124 | `Error: Operation timed out after {timeout}s` | +| Network error | 5 | `Error: Network error: {message}` | + +### Progress Reporting + +``` +[pulling] Fetching base manifest... (1/4) +[pulling] Fetching target manifest... (2/4) +[pulling] Downloading layers... (3/4) + └─ sha256:abc123... 45.2 MB/128.5 MB (35%) +[extracting] Extracting base layers... (1/8) +[extracting] Extracting target layers... (5/8) +[analyzing] Computing section hashes... (1/156) + └─ /usr/lib/libssl.so.3 +[analyzing] Computing section hashes... (78/156) + └─ /usr/bin/python3.11 +[diffing] Comparing binaries... (1/156) +[complete] Analysis complete. +``` + +## Determinism Requirements + +1. **Output ordering**: Findings sorted by path +2. **Timestamps**: From injected `TimeProvider` +3. **Hash formats**: Lowercase hexadecimal +4. **JSON output**: RFC 8785 canonical when `--format=json` +5. **DSSE files**: Canonical JSON serialization + +## Test Cases + +### Unit Tests + +| Test | Description | Expected | +|------|-------------|----------| +| `ParseOptions_ValidArgs_Succeeds` | All required options provided | Options parsed correctly | +| `ParseOptions_MissingBase_Fails` | Missing --base | Parse error | +| `ComputeDiff_IdenticalImages_NoChanges` | Same image for base and target | Empty findings, summary shows 0 modified | +| `ComputeDiff_ModifiedBinary_DetectsChange` | Binary with .text change | Finding with modified status | +| `ComputeDiff_AddedBinary_Detected` | Binary in target only | Finding with added status | +| `ComputeDiff_RemovedBinary_Detected` | Binary in base only | Finding with removed status | +| `RenderTable_ValidResult_FormatsCorrectly` | Result with findings | Properly formatted table | +| `RenderJson_ValidResult_CanonicalOutput` | Same result, multiple renders | Byte-identical JSON | +| `EmitDsse_ValidResult_CreatesFile` | With --emit-dsse | DSSE file created | + +### Integration Tests + +| Test | Description | Expected | +|------|-------------|----------| +| `EndToEnd_RealImages_ProducesOutput` | Two synthetic OCI images | Valid diff output | +| `EndToEnd_WithDsse_ValidAttestation` | Diff with --emit-dsse | Verifiable DSSE | +| `MultiArch_SpecificPlatform_FiltersCorrectly` | Multi-arch image with --platform | Only specified platform analyzed | + +## Execution Log + +| Date (UTC) | Update | Owner | +|------------|--------|-------| +| 2026-01-13 | Sprint created from advisory analysis. | Project Mgmt | +| 2026-01-13 | Task CLI-DIFF-TESTS-0001 marked BLOCKED: CLI tests under active modification. | Project Mgmt | + +## Decisions & Risks + +- **APPROVED**: Command placed under `stella scan diff` (not separate `stella-scan image diff` as in advisory). +- **APPROVED**: Support `--mode=elf` initially; `--mode=pe` and `--mode=auto` stubbed for future. +- **BLOCKED**: CLI tests require coordination with other agent work; tests deferred. +- **RISK**: Long-running operations need robust timeout and cancellation handling. +- **RISK**: Large images may cause memory pressure; consider streaming approach for layer extraction. + +## Next Checkpoints + +- Task 1-6 complete → Command implementation ready +- Task 7-8 complete → Help and DI wired up +- Task 9-10 complete (after unblock) → Sprint can be marked DONE diff --git a/docs/implplan/SPRINT_20260113_001_004_DOCS_binary_diff_attestation.md b/docs/implplan/SPRINT_20260113_001_004_DOCS_binary_diff_attestation.md new file mode 100644 index 000000000..5b99638a3 --- /dev/null +++ b/docs/implplan/SPRINT_20260113_001_004_DOCS_binary_diff_attestation.md @@ -0,0 +1,351 @@ +# Sprint 20260113_001_004_DOCS - Binary Diff Attestation Documentation + +## Topic & Scope + +- Create architecture documentation for binary diff attestation feature +- Update CLI reference with new `stella scan diff` command +- Publish BinaryDiffV1 predicate JSON Schema +- Add developer guide for extending binary analysis +- **Working directory:** `docs/` + +## Dependencies & Concurrency + +- Can proceed in parallel with Sprints 2-3 (after Sprint 1 models stabilize) +- No blocking dependencies for initial documentation drafts +- Final documentation review after all implementation sprints complete + +## Documentation Prerequisites + +- `docs/README.md` +- `docs/ARCHITECTURE_REFERENCE.md` +- `CLAUDE.md` (for documentation standards) +- Existing module docs: `docs/modules/scanner/` +- Existing CLI docs: `docs/API_CLI_REFERENCE.md` + +## Delivery Tracker + +| # | Task ID | Status | Key dependency / next step | Owners | Task Definition | +|---|---------|--------|---------------------------|--------|-----------------| +| 1 | DOCS-ARCH-0001 | TODO | Sprint 001 models | Guild - Docs | Create `docs/modules/scanner/binary-diff-attestation.md` architecture document covering ELF section hashing, diff computation, and DSSE attestation flow. | +| 2 | DOCS-CLI-0001 | TODO | Sprint 003 command spec | Guild - Docs | Update `docs/API_CLI_REFERENCE.md` with `stella scan diff` command documentation including all options, examples, and output formats. | +| 3 | DOCS-SCHEMA-0001 | TODO | Sprint 002 schema | Guild - Docs | Publish `docs/schemas/binarydiff-v1.schema.json` with full JSON Schema definition and validation examples. | +| 4 | DOCS-DEVGUIDE-0001 | TODO | All sprints | Guild - Docs | Create `docs/dev/extending-binary-analysis.md` developer guide for adding new binary formats (PE, Mach-O) and custom section extractors. | +| 5 | DOCS-EXAMPLES-0001 | TODO | Sprint 003 complete | Guild - Docs | Add usage examples to `docs/examples/binary-diff/` with sample commands, expected outputs, and DSSE verification steps. | +| 6 | DOCS-GLOSSARY-0001 | TODO | None | Guild - Docs | Update `docs/GLOSSARY.md` (if exists) or create glossary entries for: section hash, binary diff, vendor backport, DSSE envelope. | +| 7 | DOCS-CHANGELOG-0001 | TODO | All sprints complete | Guild - Docs | Add changelog entry for binary diff attestation feature in `CHANGELOG.md`. | +| 8 | DOCS-REVIEW-0001 | TODO | All above complete | Guild - Docs | Final documentation review: cross-link all docs, verify examples work, spell-check, ensure consistency with existing docs. | + +## Documentation Deliverables + +### 1. Architecture Document + +**File:** `docs/modules/scanner/binary-diff-attestation.md` + +**Outline:** + +```markdown +# Binary Diff Attestation + +## Overview +- Purpose and use cases +- Relationship to SBOM and VEX + +## Architecture + +### Component Diagram +- ElfSectionHashExtractor +- BinaryDiffService +- BinaryDiffPredicateBuilder +- BinaryDiffDsseSigner + +### Data Flow +1. Image resolution +2. Layer extraction +3. Binary identification +4. Section hash computation +5. Diff computation +6. Predicate construction +7. DSSE signing + +## ELF Section Hashing + +### Target Sections +- .text (executable code) +- .rodata (read-only data) +- .data (initialized data) +- .symtab (symbol table) +- .dynsym (dynamic symbols) + +### Hash Algorithm +- SHA-256 primary +- BLAKE3 optional + +### Determinism Guarantees +- Stable ordering +- Canonical serialization + +## BinaryDiffV1 Predicate + +### Schema Overview +- Subjects (image references) +- Inputs (base/target) +- Findings (per-binary deltas) +- Metadata + +### Evidence Properties +- Section hashes in SBOM +- Confidence scoring +- Verdict classification + +## DSSE Attestation + +### Envelope Structure +- Payload type: stellaops.binarydiff.v1 +- Signature algorithm +- Rekor submission + +### Verification +- cosign compatibility +- Offline verification + +## Integration Points + +### VEX Mapping +- Linking to vulnerability status +- Backport evidence + +### Policy Engine +- Binary evidence rules +- Trust thresholds + +## Configuration + +### Options +- Section selection +- Hash algorithms +- Output formats + +## Limitations and Future Work + +### Current Limitations +- ELF only (PE/Mach-O planned) +- Single-platform per invocation + +### Roadmap +- PE section analysis (M2) +- Mach-O section analysis (M2) +- Vendor backport corpus (M3) +``` + +### 2. CLI Reference Update + +**File:** `docs/API_CLI_REFERENCE.md` (append to Scan section) + +**Content:** + +```markdown +### stella scan diff + +Compare binaries between two container images at the section level. + +#### Synopsis + +```bash +stella scan diff --base --target [options] +``` + +#### Description + +The `diff` command performs binary-level comparison between two container images, +analyzing ELF section hashes to detect changes and classify them as patches, +vanilla updates, or unknown modifications. + +#### Options + +| Option | Description | +|--------|-------------| +| `--base`, `-b` | Base image reference (required) | +| `--target`, `-t` | Target image reference (required) | +| `--mode`, `-m` | Analysis mode: `elf`, `pe`, `auto` (default: `auto`) | +| `--emit-dsse`, `-d` | Directory for DSSE attestation output | +| `--format`, `-f` | Output format: `table`, `json`, `summary` (default: `table`) | +| `--platform`, `-p` | Platform filter (e.g., `linux/amd64`) | +| `--include-unchanged` | Include unchanged binaries in output | +| `--sections` | Sections to analyze (comma-separated) | +| `--registry-auth` | Path to Docker config for authentication | +| `--timeout` | Timeout in seconds (default: 300) | +| `--verbose`, `-v` | Enable verbose output | + +#### Examples + +**Basic comparison:** +```bash +stella scan diff --base myapp:1.0.0 --target myapp:1.0.1 +``` + +**With DSSE attestation output:** +```bash +stella scan diff -b myapp:1.0.0 -t myapp:1.0.1 \ + --mode=elf --emit-dsse=./attestations/ +``` + +**JSON output for automation:** +```bash +stella scan diff -b myapp:1.0.0 -t myapp:1.0.1 --format=json > diff.json +``` + +**Specific platform:** +```bash +stella scan diff -b myapp:1.0.0 -t myapp:1.0.1 --platform=linux/arm64 +``` + +#### Output + +**Table format** shows a summary of changes: +``` +PATH CHANGE VERDICT CONFIDENCE +/usr/lib/libssl.so.3 modified patched 0.95 +/usr/lib/libcrypto.so.3 modified patched 0.92 +``` + +**JSON format** provides full diff details for programmatic consumption. + +#### Exit Codes + +| Code | Description | +|------|-------------| +| 0 | Success | +| 1 | Invalid image reference | +| 2 | Authentication failed | +| 3 | Platform not found | +| 124 | Timeout | +| 5 | Network error | + +#### See Also + +- `stella scan layers` - List layers in an image +- `stella scan sbom` - Generate SBOM for an image +- [Binary Diff Attestation Architecture](../modules/scanner/binary-diff-attestation.md) +``` + +### 3. JSON Schema + +**File:** `docs/schemas/binarydiff-v1.schema.json` + +(Full schema as defined in Sprint 002) + +### 4. Developer Guide + +**File:** `docs/dev/extending-binary-analysis.md` + +**Outline:** + +```markdown +# Extending Binary Analysis + +## Overview + +This guide explains how to add support for new binary formats (PE, Mach-O) +or custom section extractors to the binary diff attestation system. + +## Architecture + +### Extractor Interface +- ISectionHashExtractor +- Registration pattern +- Configuration binding + +### Adding a New Format + +#### Step 1: Define Models +- Section hash models +- Format-specific metadata + +#### Step 2: Implement Extractor +- Parse binary format +- Extract sections +- Compute hashes + +#### Step 3: Register Services +- DI registration +- Configuration binding +- Format detection + +#### Step 4: Add Tests +- Unit test fixtures +- Golden file comparisons +- Edge cases + +### Example: PE Section Extractor + +```csharp +public class PeSectionHashExtractor : ISectionHashExtractor +{ + // Implementation example +} +``` + +## Best Practices + +### Determinism +- Stable ordering +- Canonical hashing +- Injected dependencies + +### Performance +- Streaming large binaries +- Caching strategies +- Parallel extraction + +### Security +- Input validation +- Memory limits +- Malformed input handling +``` + +### 5. Usage Examples + +**Directory:** `docs/examples/binary-diff/` + +**Files:** + +``` +binary-diff/ +├── README.md # Overview and prerequisites +├── basic-comparison.md # Simple diff example +├── dsse-attestation.md # DSSE output and verification +├── policy-integration.md # Using diffs in policy rules +├── ci-cd-integration.md # GitHub Actions / GitLab CI examples +└── sample-outputs/ + ├── diff-table.txt # Sample table output + ├── diff.json # Sample JSON output + └── attestation.dsse.json # Sample DSSE envelope +``` + +## Quality Checklist + +- [ ] All code examples compile/run +- [ ] All links are valid +- [ ] Consistent terminology with existing docs +- [ ] No spelling/grammar errors +- [ ] Screenshots/diagrams where helpful +- [ ] Cross-references to related docs +- [ ] Version compatibility noted + +## Execution Log + +| Date (UTC) | Update | Owner | +|------------|--------|-------| +| 2026-01-13 | Sprint created from advisory analysis. | Project Mgmt | + +## Decisions & Risks + +- **APPROVED**: Documentation follows existing StellaOps documentation patterns. +- **APPROVED**: JSON Schema published under `docs/schemas/` for external consumption. +- **RISK**: Documentation may need updates if implementation details change; defer final review until code complete. + +## Next Checkpoints + +- Task 1-3 complete → Core documentation in place +- Task 4-5 complete → Developer and user resources ready +- Task 8 complete → Sprint can be marked DONE diff --git a/docs/implplan/SPRINT_20260113_002_000_INDEX_image_index_resolution.md b/docs/implplan/SPRINT_20260113_002_000_INDEX_image_index_resolution.md new file mode 100644 index 000000000..849c9453a --- /dev/null +++ b/docs/implplan/SPRINT_20260113_002_000_INDEX_image_index_resolution.md @@ -0,0 +1,197 @@ +# Sprint Batch 20260113_002 - Image Index Resolution CLI + +## Executive Summary + +This sprint batch implements **OCI multi-arch image inspection** capabilities, enabling users to enumerate image indices, platform manifests, and layer digests through CLI commands. This completes the "index -> manifests -> layers" flow requested in the OCI Layer-Level Image Integrity advisory. + +**Scope:** OCI image index resolution with Docker & OCI media type support +**Effort Estimate:** 4-5 story points across 3 sprints +**Priority:** Medium (usability enhancement) + +## Background + +### Advisory Requirements + +The original advisory specified: + +> Resolve an image index (if present), list all platform manifests, then for each manifest list ordered layer digests and sizes. Accept Docker and OCI media types. + +### Existing Capabilities + +| Component | Status | Location | +|-----------|--------|----------| +| `OciIndex` record | EXISTS | `src/Concelier/__Libraries/.../OciIndex.cs` | +| `OciManifest` record | EXISTS | `src/Concelier/__Libraries/.../OciManifest.cs` | +| `OciRegistryClient` | EXISTS | `src/Excititor/__Libraries/.../Fetch/OciRegistryClient.cs` | +| `OciImageReferenceParser` | EXISTS | `src/Cli/StellaOps.Cli/Services/OciImageReferenceParser.cs` | +| `LayeredRootFileSystem` | EXISTS | `src/Scanner/__Libraries/.../FileSystem/LayeredRootFileSystem.cs` | + +### Gap Analysis + +| Capability | Status | +|------------|--------| +| Parse OCI image index from registry | Partial (records exist, no handler) | +| Walk index -> platform manifests | MISSING | +| CLI `image inspect` verb | MISSING | +| JSON output with canonical digests | MISSING | + +## Sprint Index + +| Sprint | ID | Module | Topic | Status | Owner | +|--------|-----|--------|-------|--------|-------| +| 1 | SPRINT_20260113_002_001 | SCANNER | OCI Image Index Inspector Service | TODO | Guild - Scanner | +| 2 | SPRINT_20260113_002_002 | CLI | Image Inspect Command | TODO | Guild - CLI | +| 3 | SPRINT_20260113_002_003 | DOCS | Image Inspection Documentation | TODO | Guild - Docs | + +## Dependencies + +``` ++-----------------------------------------------------------------------+ +| Dependency Graph | ++-----------------------------------------------------------------------+ +| | +| Sprint 1 (Inspector Service) | +| | | +| +------------------+ | +| v v | +| Sprint 2 (CLI) Sprint 3 (Docs) | +| | ++-----------------------------------------------------------------------+ +``` + +- **Sprint 1** is foundational (no dependencies) +- **Sprint 2** depends on Sprint 1 (uses inspector service) +- **Sprint 3** can proceed in parallel with Sprint 2 + +**Cross-Batch Dependencies:** +- None (this batch is independent of 001) + +## Acceptance Criteria (Batch-Level) + +### Must Have + +1. **Image Index Resolution** + - Accept image reference (tag or digest) + - Detect and parse image index (multi-arch) vs single manifest + - Return platform manifest list with os/arch/variant + +2. **Layer Enumeration** + - For each platform manifest: ordered layer digests + - Include layer sizes and media types + - Support both Docker and OCI media types + +3. **CLI Command** + - `stella image inspect ` with output formats + - `--resolve-index` flag to walk multi-arch structure + - `--print-layers` flag to include layer details + - JSON output with canonical ordering + +4. **Documentation** + - CLI reference for new commands + - Architecture doc for inspector service + +### Should Have + +- Platform filtering (`--platform linux/amd64`) +- Config blob inspection (`--config` flag) +- Cache manifest responses (in-memory, session-scoped) + +### Deferred (Out of Scope) + +- `skopeo` or `ctr` CLI integration (use HTTP API) +- Offline image tar inspection (handled by existing LayeredRootFileSystem) +- Image pulling/export (out of scope) + +## Technical Context + +### Key Files to Create/Extend + +| Component | File | Purpose | +|-----------|------|---------| +| Inspector Service | `src/Scanner/__Libraries/StellaOps.Scanner.Storage.Oci/OciImageInspector.cs` | NEW: Unified index/manifest inspection | +| Inspector Models | `src/Scanner/__Libraries/StellaOps.Scanner.Contracts/OciInspectionModels.cs` | NEW: Inspection result models | +| CLI Command | `src/Cli/StellaOps.Cli/Commands/ImageCommandGroup.cs` | NEW: `stella image` command group | +| CLI Handler | `src/Cli/StellaOps.Cli/Commands/CommandHandlers.Image.cs` | NEW: Image command handlers | + +### Output Schema + +```json +{ + "reference": "docker.io/library/nginx:latest", + "resolvedDigest": "sha256:abc123...", + "mediaType": "application/vnd.oci.image.index.v1+json", + "isMultiArch": true, + "platforms": [ + { + "os": "linux", + "architecture": "amd64", + "variant": null, + "manifestDigest": "sha256:def456...", + "configDigest": "sha256:ghi789...", + "layers": [ + { + "order": 0, + "digest": "sha256:layer1...", + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "size": 31457280 + } + ], + "totalSize": 157286400 + } + ], + "inspectedAt": "2026-01-13T12:00:00Z", + "inspectorVersion": "1.0.0" +} +``` + +### Determinism Requirements + +Per CLAUDE.md Section 8: + +1. **Ordering**: Platforms sorted by os/arch/variant; layers by order +2. **Timestamps**: From injected `TimeProvider` +3. **JSON serialization**: Canonical key ordering +4. **InvariantCulture**: All size/number formatting + +## Risk Assessment + +| Risk | Likelihood | Impact | Mitigation | +|------|------------|--------|------------| +| Registry auth complexity | Medium | Medium | Use existing `OciRegistryClient` auth handling | +| Rate limiting on public registries | Low | Low | Implement retry with backoff | +| Non-standard manifest schemas | Low | Medium | Graceful degradation with warnings | + +## Success Metrics + +- [ ] All unit tests pass +- [ ] Integration tests against Docker Hub, GHCR, and mock registry +- [ ] CLI completions and help work correctly +- [ ] JSON output is valid and deterministic + +## Documentation Prerequisites + +Before starting implementation, reviewers must read: + +- `docs/README.md` +- `docs/ARCHITECTURE_REFERENCE.md` +- `CLAUDE.md` Section 8 (Code Quality & Determinism Rules) +- OCI Image Index Spec: https://github.com/opencontainers/image-spec/blob/main/image-index.md +- OCI Image Manifest Spec: https://specs.opencontainers.org/image-spec/manifest/ + +## Execution Log + +| Date (UTC) | Update | Owner | +|------------|--------|-------| +| 2026-01-13 | Sprint batch created from advisory analysis. | Project Mgmt | + +## Decisions & Risks + +- **APPROVED 2026-01-13**: Use HTTP Registry API v2 only; no external CLI tool dependencies. +- **APPROVED 2026-01-13**: Single-manifest images return as degenerate case (1-element platform list). +- **RISK**: Some registries may not support OCI index; handle Docker manifest list as fallback. + +## Next Checkpoints + +- Sprint 1 completion -> Sprint 2 can start +- All sprints complete -> Integration testing checkpoint +- Integrate with Batch 001 CLI commands post-completion diff --git a/docs/implplan/SPRINT_20260113_002_001_SCANNER_image_inspector_service.md b/docs/implplan/SPRINT_20260113_002_001_SCANNER_image_inspector_service.md new file mode 100644 index 000000000..132333cf9 --- /dev/null +++ b/docs/implplan/SPRINT_20260113_002_001_SCANNER_image_inspector_service.md @@ -0,0 +1,271 @@ +# Sprint 20260113_002_001_SCANNER - OCI Image Index Inspector Service + +## Topic & Scope + +- Implement unified OCI image inspection service +- Support image index (multi-arch) and single manifest resolution +- Walk index -> platform manifests -> ordered layers +- Support both Docker and OCI media types +- **Working directory:** `src/Scanner/__Libraries/StellaOps.Scanner.Storage.Oci/` + +## Dependencies & Concurrency + +- No blocking dependencies (foundational sprint) +- Uses existing `OciRegistryClient` for HTTP operations +- Sprint 2 (CLI) depends on this sprint + +## Documentation Prerequisites + +- `docs/README.md` +- `docs/ARCHITECTURE_REFERENCE.md` +- `CLAUDE.md` Section 8 (Determinism Rules) +- OCI Image Index Spec: https://github.com/opencontainers/image-spec/blob/main/image-index.md +- OCI Image Manifest Spec: https://specs.opencontainers.org/image-spec/manifest/ +- Docker Manifest List: https://docs.docker.com/registry/spec/manifest-v2-2/ + +## Delivery Tracker + +| # | Task ID | Status | Key dependency / next step | Owners | Task Definition | +|---|---------|--------|---------------------------|--------|-----------------| +| 1 | IMG-INSPECT-MODELS-0001 | TODO | None | Guild - Scanner | Define `ImageInspectionResult`, `PlatformManifest`, `LayerInfo` models in `src/Scanner/__Libraries/StellaOps.Scanner.Contracts/OciInspectionModels.cs`. Include all OCI/Docker discriminators. | +| 2 | IMG-INSPECT-INTERFACE-0001 | TODO | Depends on MODELS-0001 | Guild - Scanner | Define `IOciImageInspector` interface with `InspectAsync(reference, options, ct)` signature. Options include: resolveIndex, includeLayers, platformFilter. | +| 3 | IMG-INSPECT-IMPL-0001 | TODO | Depends on INTERFACE-0001 | Guild - Scanner | Implement `OciImageInspector` class. Handle HEAD request for manifest detection, then GET for content. Detect index vs manifest by media type. | +| 4 | IMG-INSPECT-INDEX-0001 | TODO | Depends on IMPL-0001 | Guild - Scanner | Implement index resolution: parse `application/vnd.oci.image.index.v1+json` and `application/vnd.docker.distribution.manifest.list.v2+json`. Extract platform descriptors. | +| 5 | IMG-INSPECT-MANIFEST-0001 | TODO | Depends on IMPL-0001 | Guild - Scanner | Implement manifest parsing: `application/vnd.oci.image.manifest.v1+json` and `application/vnd.docker.distribution.manifest.v2+json`. Extract config and layers. | +| 6 | IMG-INSPECT-LAYERS-0001 | TODO | Depends on MANIFEST-0001 | Guild - Scanner | For each manifest, enumerate layers with: order (0-indexed), digest, mediaType, size. Support compressed and uncompressed variants. | +| 7 | IMG-INSPECT-AUTH-0001 | TODO | Depends on IMPL-0001 | Guild - Scanner | Integrate with existing registry auth: token-based, basic, anonymous. Handle 401 -> token refresh flow. | +| 8 | IMG-INSPECT-DI-0001 | TODO | Depends on all above | Guild - Scanner | Register `IOciImageInspector` in `ServiceCollectionExtensions.cs`. Inject `TimeProvider`, `IHttpClientFactory`, `ILogger`. | +| 9 | IMG-INSPECT-TESTS-0001 | TODO | Depends on all above | Guild - Scanner | Unit tests covering: single manifest, multi-arch index, Docker manifest list, missing manifest, auth errors, malformed responses. | +| 10 | IMG-INSPECT-INTEGRATION-0001 | TODO | Depends on TESTS-0001 | Guild - Scanner | Integration tests against mock OCI registry (testcontainers or in-memory). Test real Docker Hub and GHCR in CI. | + +## Technical Specification + +### Models + +```csharp +namespace StellaOps.Scanner.Contracts; + +/// +/// Result of inspecting an OCI image reference. +/// +public sealed record ImageInspectionResult +{ + /// Original image reference provided. + public required string Reference { get; init; } + + /// Resolved digest of the index or manifest. + public required string ResolvedDigest { get; init; } + + /// Media type of the resolved artifact. + public required string MediaType { get; init; } + + /// True if this is a multi-arch image index. + public required bool IsMultiArch { get; init; } + + /// Platform manifests (1 for single-arch, N for multi-arch). + public required ImmutableArray Platforms { get; init; } + + /// Inspection timestamp (UTC). + public required DateTimeOffset InspectedAt { get; init; } + + /// Inspector version for reproducibility. + public required string InspectorVersion { get; init; } + + /// Registry that was queried. + public required string Registry { get; init; } + + /// Repository name. + public required string Repository { get; init; } + + /// Warnings encountered during inspection. + public ImmutableArray Warnings { get; init; } = []; +} + +/// +/// A platform-specific manifest within an image index. +/// +public sealed record PlatformManifest +{ + /// Operating system (e.g., "linux", "windows"). + public required string Os { get; init; } + + /// CPU architecture (e.g., "amd64", "arm64"). + public required string Architecture { get; init; } + + /// Architecture variant (e.g., "v8" for arm64). + public string? Variant { get; init; } + + /// OS version (mainly for Windows). + public string? OsVersion { get; init; } + + /// Digest of this platform's manifest. + public required string ManifestDigest { get; init; } + + /// Media type of the manifest. + public required string ManifestMediaType { get; init; } + + /// Digest of the config blob. + public required string ConfigDigest { get; init; } + + /// Ordered list of layers. + public required ImmutableArray Layers { get; init; } + + /// Total size of all layers in bytes. + public required long TotalSize { get; init; } + + /// Platform string (os/arch/variant). + public string PlatformString => Variant is null + ? $"{Os}/{Architecture}" + : $"{Os}/{Architecture}/{Variant}"; +} + +/// +/// Information about a single layer. +/// +public sealed record LayerInfo +{ + /// Layer order (0-indexed, application order). + public required int Order { get; init; } + + /// Layer digest (sha256:...). + public required string Digest { get; init; } + + /// Media type of the layer blob. + public required string MediaType { get; init; } + + /// Compressed size in bytes. + public required long Size { get; init; } + + /// Optional annotations from the manifest. + public ImmutableDictionary? Annotations { get; init; } +} +``` + +### Interface + +```csharp +namespace StellaOps.Scanner.Storage.Oci; + +public interface IOciImageInspector +{ + /// + /// Inspects an OCI image reference. + /// + /// Image reference (e.g., "nginx:latest", "ghcr.io/org/app@sha256:..."). + /// Inspection options. + /// Cancellation token. + /// Inspection result or null if not found. + Task InspectAsync( + string reference, + ImageInspectionOptions? options = null, + CancellationToken cancellationToken = default); +} + +public sealed record ImageInspectionOptions +{ + /// Resolve multi-arch index to platform manifests (default: true). + public bool ResolveIndex { get; init; } = true; + + /// Include layer details (default: true). + public bool IncludeLayers { get; init; } = true; + + /// Filter to specific platform (e.g., "linux/amd64"). + public string? PlatformFilter { get; init; } + + /// Maximum platforms to inspect (default: unlimited). + public int? MaxPlatforms { get; init; } + + /// Request timeout. + public TimeSpan? Timeout { get; init; } +} +``` + +### Media Type Handling + +| Media Type | Type | Handling | +|------------|------|----------| +| `application/vnd.oci.image.index.v1+json` | OCI Index | Parse as index, enumerate manifests | +| `application/vnd.docker.distribution.manifest.list.v2+json` | Docker List | Parse as index (compatible) | +| `application/vnd.oci.image.manifest.v1+json` | OCI Manifest | Parse as manifest, extract layers | +| `application/vnd.docker.distribution.manifest.v2+json` | Docker Manifest | Parse as manifest (compatible) | +| Other | Unknown | Return warning, skip or fail per config | + +### Algorithm + +```pseudo +function InspectAsync(reference, options): + parsed = ParseReference(reference) // registry, repo, tag/digest + + // Step 1: Resolve to digest + digest = HEAD(registry, repo, parsed.tagOrDigest) + mediaType = response.headers["Content-Type"] + + // Step 2: Get manifest content + body = GET(registry, repo, digest, Accept: mediaType) + + // Step 3: Classify and parse + if mediaType in [OCI_INDEX, DOCKER_MANIFEST_LIST]: + index = ParseIndex(body) + platforms = [] + for descriptor in index.manifests: + if options.platformFilter and not matches(descriptor, filter): + continue + manifest = await InspectManifest(registry, repo, descriptor.digest) + platforms.append(manifest) + return Result(isMultiArch=true, platforms) + else: + manifest = ParseManifest(body) + platform = ExtractPlatform(manifest.config) + layers = ExtractLayers(manifest) + return Result(isMultiArch=false, [platform]) +``` + +### Determinism Requirements + +1. **Platform ordering**: Sort by os ASC, architecture ASC, variant ASC +2. **Layer ordering**: Preserve manifest order (0-indexed) +3. **Timestamps**: From injected `TimeProvider` +4. **JSON**: Canonical serialization for any digest computation +5. **Warnings**: Sorted lexicographically + +## Test Cases + +### Unit Tests + +| Test | Description | Expected | +|------|-------------|----------| +| `Inspect_SingleManifest_ReturnsSinglePlatform` | Image without index | 1 platform, layers present | +| `Inspect_MultiArchIndex_ReturnsAllPlatforms` | Image with 5 platforms | 5 platforms, each with layers | +| `Inspect_DockerManifestList_Parses` | Legacy Docker format | Correctly parsed as index | +| `Inspect_PlatformFilter_ReturnsFiltered` | Filter to linux/amd64 | Only matching platform returned | +| `Inspect_NotFound_ReturnsNull` | 404 response | Returns null, no exception | +| `Inspect_AuthRequired_RefreshesToken` | 401 -> token refresh | Successful after refresh | +| `Inspect_Deterministic_SameOutput` | Same image, multiple calls | Identical result (ignoring timestamp) | + +### Integration Tests + +| Test | Description | Expected | +|------|-------------|----------| +| `Inspect_DockerHub_NginxLatest` | Public Docker Hub image | Multi-arch result with linux/amd64, linux/arm64 | +| `Inspect_GHCR_PublicImage` | GitHub Container Registry | Valid result | +| `Inspect_MockRegistry_AllScenarios` | Testcontainers registry | All edge cases covered | + +## Execution Log + +| Date (UTC) | Update | Owner | +|------------|--------|-------| +| 2026-01-13 | Sprint created from advisory analysis. | Project Mgmt | + +## Decisions & Risks + +- **APPROVED**: Single manifest images return as 1-element platforms array for API consistency. +- **APPROVED**: Use existing `OciRegistryClient` for HTTP operations where compatible. +- **RISK**: Some registries return incorrect Content-Type; handle by sniffing JSON structure. +- **RISK**: Large multi-arch images (10+ platforms) may be slow; add max_platforms limit. + +## Next Checkpoints + +- Task 1-3 complete -> Basic inspection working +- Task 4-6 complete -> Full index/manifest/layer resolution +- Task 9-10 complete -> Sprint can be marked DONE +- Unblock Sprint 2 (CLI) diff --git a/docs/implplan/SPRINT_20260113_002_002_CLI_image_inspect_command.md b/docs/implplan/SPRINT_20260113_002_002_CLI_image_inspect_command.md new file mode 100644 index 000000000..d0c099dd1 --- /dev/null +++ b/docs/implplan/SPRINT_20260113_002_002_CLI_image_inspect_command.md @@ -0,0 +1,283 @@ +# Sprint 20260113_002_002_CLI - Image Inspect Command + +## Topic & Scope + +- Implement `stella image inspect` CLI command +- Support `--resolve-index`, `--print-layers`, `--platform` flags +- JSON and human-readable output formats +- Integrate with OCI Image Inspector service +- **Working directory:** `src/Cli/StellaOps.Cli/Commands/` + +## Dependencies & Concurrency + +- **Depends on:** Sprint 002_001 (OCI Image Inspector Service) +- Parallel work safe within CLI module +- Sprint 3 (Docs) can proceed in parallel + +## Documentation Prerequisites + +- `docs/README.md` +- `CLAUDE.md` Section 8 (Determinism Rules) +- `src/Cli/StellaOps.Cli/AGENTS.md` (if exists) +- Existing CLI patterns in `LayerSbomCommandGroup.cs` + +## Delivery Tracker + +| # | Task ID | Status | Key dependency / next step | Owners | Task Definition | +|---|---------|--------|---------------------------|--------|-----------------| +| 1 | CLI-IMAGE-GROUP-0001 | TODO | None | Guild - CLI | Create `ImageCommandGroup.cs` with `stella image` root command and subcommand registration. | +| 2 | CLI-IMAGE-INSPECT-0001 | TODO | Depends on GROUP-0001 | Guild - CLI | Implement `stella image inspect ` command with options: `--resolve-index`, `--print-layers`, `--platform`, `--output`. | +| 3 | CLI-IMAGE-HANDLER-0001 | TODO | Depends on INSPECT-0001, Sprint 001 service | Guild - CLI | Implement `CommandHandlers.Image.cs` with `HandleInspectImageAsync` that calls `IOciImageInspector`. | +| 4 | CLI-IMAGE-OUTPUT-TABLE-0001 | TODO | Depends on HANDLER-0001 | Guild - CLI | Implement table output for human-readable display using Spectre.Console. Show platforms, layers, sizes. | +| 5 | CLI-IMAGE-OUTPUT-JSON-0001 | TODO | Depends on HANDLER-0001 | Guild - CLI | Implement JSON output with canonical ordering. Match schema from Sprint 001 models. | +| 6 | CLI-IMAGE-REGISTER-0001 | TODO | Depends on all above | Guild - CLI | Register `ImageCommandGroup` in `CommandFactory.cs`. Wire DI for `IOciImageInspector`. | +| 7 | CLI-IMAGE-TESTS-0001 | TODO | Depends on all above | Guild - CLI | Unit tests covering: successful inspect, not found, auth error, invalid reference, output formats. | +| 8 | CLI-IMAGE-GOLDEN-0001 | TODO | Depends on TESTS-0001 | Guild - CLI | Golden output tests for determinism: same input produces identical output across runs. | + +## Technical Specification + +### Command Structure + +``` +stella image + +Subcommands: + inspect Inspect OCI image manifest and layers +``` + +### `stella image inspect` Command + +``` +stella image inspect [options] + +Arguments: + Image reference (e.g., nginx:latest, ghcr.io/org/app@sha256:...) + +Options: + --resolve-index, -r Resolve multi-arch index to platform manifests (default: true) + --print-layers, -l Include layer details in output (default: true) + --platform, -p Filter to specific platform (e.g., linux/amd64) + --output, -o Output format: table (default), json + --verbose, -v Show detailed information including warnings + --timeout Request timeout in seconds (default: 60) + +Examples: + stella image inspect nginx:latest + stella image inspect nginx:latest --output json + stella image inspect nginx:latest --platform linux/arm64 + stella image inspect ghcr.io/org/app@sha256:abc123... --print-layers +``` + +### Output Examples + +#### Table Output (Default) + +``` +Image: nginx:latest +Resolved Digest: sha256:abc123... +Media Type: application/vnd.oci.image.index.v1+json +Multi-Arch: Yes (5 platforms) + +Platforms: ++-------+--------------+----------+---------+---------------+------------+ +| OS | Architecture | Variant | Layers | Total Size | Manifest | ++-------+--------------+----------+---------+---------------+------------+ +| linux | amd64 | - | 7 | 142.3 MB | sha256:... | +| linux | arm64 | v8 | 7 | 138.1 MB | sha256:... | +| linux | arm | v7 | 7 | 135.2 MB | sha256:... | +| linux | 386 | - | 7 | 145.8 MB | sha256:... | +| linux | ppc64le | - | 7 | 148.5 MB | sha256:... | ++-------+--------------+----------+---------+---------------+------------+ + +Layers (linux/amd64): ++-------+------------------+------------------------------------------------+----------+ +| Order | Size | Digest | Type | ++-------+------------------+------------------------------------------------+----------+ +| 0 | 31.4 MB | sha256:a803e7c4b030... | tar+gzip | +| 1 | 62.5 MB | sha256:8a6e7b1c9d2e... | tar+gzip | +| ... | ... | ... | ... | ++-------+------------------+------------------------------------------------+----------+ + +Inspected at: 2026-01-13T12:00:00Z +``` + +#### JSON Output + +```json +{ + "reference": "nginx:latest", + "resolvedDigest": "sha256:abc123...", + "mediaType": "application/vnd.oci.image.index.v1+json", + "isMultiArch": true, + "registry": "docker.io", + "repository": "library/nginx", + "platforms": [ + { + "os": "linux", + "architecture": "amd64", + "variant": null, + "manifestDigest": "sha256:def456...", + "configDigest": "sha256:ghi789...", + "layers": [ + { + "order": 0, + "digest": "sha256:layer1...", + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "size": 31457280 + } + ], + "totalSize": 157286400 + } + ], + "inspectedAt": "2026-01-13T12:00:00Z", + "inspectorVersion": "1.0.0" +} +``` + +### Implementation + +```csharp +// ImageCommandGroup.cs +namespace StellaOps.Cli.Commands; + +public static class ImageCommandGroup +{ + public static Command Build( + IServiceProvider services, + StellaOpsCliOptions options, + Option verboseOption, + CancellationToken cancellationToken) + { + var imageCommand = new Command("image", "OCI image operations"); + + imageCommand.AddCommand(BuildInspectCommand(services, options, verboseOption, cancellationToken)); + + return imageCommand; + } + + private static Command BuildInspectCommand( + IServiceProvider services, + StellaOpsCliOptions options, + Option verboseOption, + CancellationToken cancellationToken) + { + var referenceArg = new Argument("reference") + { + Description = "Image reference (e.g., nginx:latest, ghcr.io/org/app@sha256:...)" + }; + + var resolveIndexOption = new Option("--resolve-index", new[] { "-r" }) + { + Description = "Resolve multi-arch index to platform manifests", + DefaultValue = true + }; + + var printLayersOption = new Option("--print-layers", new[] { "-l" }) + { + Description = "Include layer details in output", + DefaultValue = true + }; + + var platformOption = new Option("--platform", new[] { "-p" }) + { + Description = "Filter to specific platform (e.g., linux/amd64)" + }; + + var outputOption = new Option("--output", new[] { "-o" }) + { + Description = "Output format: table (default), json" + }; + + var timeoutOption = new Option("--timeout") + { + Description = "Request timeout in seconds", + DefaultValue = 60 + }; + + var inspect = new Command("inspect", "Inspect OCI image manifest and layers") + { + referenceArg, + resolveIndexOption, + printLayersOption, + platformOption, + outputOption, + timeoutOption, + verboseOption + }; + + inspect.SetAction(async (parseResult, _) => + { + var reference = parseResult.GetValue(referenceArg) ?? string.Empty; + var resolveIndex = parseResult.GetValue(resolveIndexOption); + var printLayers = parseResult.GetValue(printLayersOption); + var platform = parseResult.GetValue(platformOption); + var output = parseResult.GetValue(outputOption) ?? "table"; + var timeout = parseResult.GetValue(timeoutOption); + var verbose = parseResult.GetValue(verboseOption); + + return await CommandHandlers.HandleInspectImageAsync( + services, reference, resolveIndex, printLayers, + platform, output, timeout, verbose, cancellationToken); + }); + + return inspect; + } +} +``` + +### Error Handling + +| Scenario | Exit Code | Message | +|----------|-----------|---------| +| Success | 0 | (output) | +| Image not found | 1 | `Error: Image not found: ` | +| Auth required | 2 | `Error: Authentication required for ` | +| Invalid reference | 2 | `Error: Invalid image reference: ` | +| Network error | 2 | `Error: Network error: ` | +| Timeout | 2 | `Error: Request timed out` | + +### Determinism Requirements + +1. **Ordering**: JSON keys sorted; platforms sorted by os/arch/variant +2. **Size formatting**: Use InvariantCulture for all numbers +3. **Timestamps**: Display as UTC ISO-8601 +4. **Digest truncation**: Consistent truncation (e.g., first 12 chars for display) + +## Test Cases + +### Unit Tests + +| Test | Description | Expected | +|------|-------------|----------| +| `Inspect_ValidReference_ReturnsSuccess` | Mock successful inspection | Exit code 0, valid output | +| `Inspect_NotFound_ReturnsError` | 404 from registry | Exit code 1, error message | +| `Inspect_InvalidReference_ReturnsError` | Malformed reference | Exit code 2, validation error | +| `Inspect_JsonOutput_ValidJson` | Request JSON format | Parseable JSON output | +| `Inspect_TableOutput_FormatsCorrectly` | Default table format | Table with headers and rows | +| `Inspect_PlatformFilter_FiltersResults` | Filter to linux/amd64 | Only matching platform in output | + +### Golden Output Tests + +| Test | Description | Expected | +|------|-------------|----------| +| `Inspect_Json_Deterministic` | Same input, multiple runs | Byte-identical JSON | +| `Inspect_Table_Deterministic` | Same input, multiple runs | Identical table output | + +## Execution Log + +| Date (UTC) | Update | Owner | +|------------|--------|-------| +| 2026-01-13 | Sprint created from advisory analysis. | Project Mgmt | + +## Decisions & Risks + +- **APPROVED**: Table output as default (more user-friendly). +- **APPROVED**: JSON output matches service model exactly (no transformation). +- **RISK**: CLI tests may conflict with other agent work; coordinate ownership. +- **RISK**: Table formatting may truncate long digests; use consistent truncation. + +## Next Checkpoints + +- Task 1-3 complete -> Basic command working +- Task 4-5 complete -> Both output formats working +- Task 7-8 complete -> Sprint can be marked DONE diff --git a/docs/implplan/SPRINT_20260113_002_003_DOCS_image_inspection.md b/docs/implplan/SPRINT_20260113_002_003_DOCS_image_inspection.md new file mode 100644 index 000000000..713bc1e4b --- /dev/null +++ b/docs/implplan/SPRINT_20260113_002_003_DOCS_image_inspection.md @@ -0,0 +1,102 @@ +# Sprint 20260113_002_003_DOCS - Image Inspection Documentation + +## Topic & Scope + +- Document OCI Image Inspector architecture +- Create CLI reference for `stella image inspect` +- Add usage examples and troubleshooting guide +- **Working directory:** `docs/` + +## Dependencies & Concurrency + +- Can proceed in parallel with Sprint 002_002 +- Should finalize after Sprint 002_001 models are stable + +## Delivery Tracker + +| # | Task ID | Status | Key dependency / next step | Owners | Task Definition | +|---|---------|--------|---------------------------|--------|-----------------| +| 1 | DOCS-IMAGE-ARCH-0001 | TODO | Sprint 001 complete | Guild - Docs | Create `docs/modules/scanner/image-inspection.md` documenting the OCI Image Inspector service architecture, supported media types, and integration points. | +| 2 | DOCS-IMAGE-CLI-0001 | TODO | Sprint 002 complete | Guild - Docs | Add `stella image inspect` to CLI reference in `docs/API_CLI_REFERENCE.md`. Include all options, examples, and exit codes. | +| 3 | DOCS-IMAGE-EXAMPLES-0001 | TODO | Depends on CLI-0001 | Guild - Docs | Create practical usage examples in `docs/guides/image-inspection-guide.md` covering Docker Hub, GHCR, private registries, and CI/CD integration. | +| 4 | DOCS-IMAGE-TROUBLESHOOT-0001 | TODO | Depends on EXAMPLES-0001 | Guild - Docs | Add troubleshooting section for common issues: auth failures, rate limits, unsupported media types. | + +## Technical Specification + +### Architecture Documentation Outline + +```markdown +# OCI Image Inspection + +## Overview +- Purpose and use cases +- Supported registries and media types + +## Architecture +- IOciImageInspector interface +- Index vs manifest resolution flow +- Platform enumeration algorithm + +## Media Type Support +| Media Type | Description | Support | +|------------|-------------|---------| +| ... | ... | ... | + +## Integration Points +- CLI integration +- Programmatic usage +- Webhook/CI integration + +## Configuration +- Registry authentication +- Timeout and retry settings + +## Determinism +- Output ordering guarantees +- Reproducibility considerations +``` + +### CLI Reference Addition + +```markdown +## stella image inspect + +Inspect OCI image manifest and layers. + +### Synopsis +stella image inspect [options] + +### Arguments +| Argument | Description | +|----------|-------------| +| reference | Image reference (tag or digest) | + +### Options +| Option | Description | Default | +|--------|-------------|---------| +| --resolve-index, -r | Resolve multi-arch index | true | +| --print-layers, -l | Include layer details | true | +| --platform, -p | Platform filter | (all) | +| --output, -o | Output format (table, json) | table | + +### Examples +... + +### Exit Codes +| Code | Meaning | +|------|---------| +| 0 | Success | +| 1 | Image not found | +| 2 | Error (auth, network, invalid input) | +``` + +## Execution Log + +| Date (UTC) | Update | Owner | +|------------|--------|-------| +| 2026-01-13 | Sprint created from advisory analysis. | Project Mgmt | + +## Next Checkpoints + +- All tasks complete -> Sprint can be marked DONE +- Coordinate with Sprint 002_001/002 for accuracy diff --git a/docs/implplan/SPRINT_20260113_003_000_INDEX_vex_evidence_linking.md b/docs/implplan/SPRINT_20260113_003_000_INDEX_vex_evidence_linking.md new file mode 100644 index 000000000..434f4a83c --- /dev/null +++ b/docs/implplan/SPRINT_20260113_003_000_INDEX_vex_evidence_linking.md @@ -0,0 +1,233 @@ +# Sprint Batch 20260113_003 - VEX Evidence Auto-Linking + +## Executive Summary + +This sprint batch implements **automatic linking** between VEX exploitability status and DSSE binary-diff evidence bundles. When a binary analysis determines a vulnerability is "not_affected" due to a vendor backport, the system automatically links the VEX assertion to the cryptographic evidence that proves the claim. + +**Scope:** VEX-to-evidence linking for binary-diff attestations +**Effort Estimate:** 3-4 story points across 2 sprints +**Priority:** Medium (completes evidence chain) + +## Background + +### Advisory Requirements + +The original advisory specified: + +> Surface exploitability conclusions via CycloneDX VEX (e.g., "CVE-X.Y not affected due to backported fix; evidence -> DSSE bundle link"). + +> For each CVE in SBOM components, attach exploitability status with `analysis.justification` ("component_not_present", "vulnerable_code_not_in_execute_path", "fixed", etc.) and `analysis.detail` linking the DSSE evidence URI. + +### Existing Capabilities + +| Component | Status | Location | +|-----------|--------|----------| +| `VexPredicate` | EXISTS | `src/Attestor/__Libraries/.../Predicates/VexPredicate.cs` | +| `VexDeltaEntity` | EXISTS | `src/Excititor/__Libraries/.../Observations/VexDeltaModels.cs` | +| `CycloneDxExporter` | EXISTS | `src/Excititor/__Libraries/.../CycloneDxExporter.cs` | +| `BinaryDiffV1 Predicate` | IN PROGRESS | Batch 001 Sprint 002 | +| `BinaryDiffDsseSigner` | IN PROGRESS | Batch 001 Sprint 002 | + +### Gap Analysis + +| Capability | Status | +|------------|--------| +| Store DSSE bundle URIs with VEX assertions | MISSING | +| Auto-link binary-diff evidence to VEX | MISSING | +| Emit `analysis.detail` with evidence URI in CycloneDX VEX | MISSING | +| CLI `stella vex gen` with evidence links | PARTIAL | + +## Sprint Index + +| Sprint | ID | Module | Topic | Status | Owner | +|--------|-----|--------|-------|--------|-------| +| 1 | SPRINT_20260113_003_001 | EXCITITOR | VEX Evidence Linker Service | TODO | Guild - Excititor | +| 2 | SPRINT_20260113_003_002 | CLI | VEX Generation with Evidence Links | TODO | Guild - CLI | + +## Dependencies + +``` ++-----------------------------------------------------------------------+ +| Dependency Graph | ++-----------------------------------------------------------------------+ +| | +| Batch 001 (Binary Diff Attestation) | +| | | +| v | +| Sprint 1 (VEX Evidence Linker) | +| | | +| v | +| Sprint 2 (CLI Integration) | +| | ++-----------------------------------------------------------------------+ +``` + +**Cross-Batch Dependencies:** +- Batch 001 Sprint 002 (BinaryDiffV1 predicate) must be complete +- VEX Evidence Linker consumes DSSE bundle URIs from binary diff + +## Acceptance Criteria (Batch-Level) + +### Must Have + +1. **Evidence URI Storage** + - Store DSSE bundle URIs alongside VEX assertions + - Support multiple evidence sources per VEX entry + - URIs point to OCI artifact digests or CAS addresses + +2. **Auto-Link on Binary Diff** + - When binary diff detects "patched" verdict, create VEX link + - Link includes: DSSE envelope digest, predicate type, confidence score + - Justification auto-set to "vulnerable_code_not_in_execute_path" or "code_not_reachable" + +3. **CycloneDX VEX Output** + - `analysis.detail` contains evidence URI + - `analysis.response` includes evidence metadata + - Compatible with CycloneDX VEX 1.5+ schema + +4. **CLI Integration** + - `stella vex gen` includes `--link-evidence` flag + - JSON output contains evidence links + - Human-readable output shows evidence summary + +### Should Have + +- Confidence threshold filtering (only link if confidence >= X) +- Evidence chain validation (verify DSSE before linking) + +### Deferred (Out of Scope) + +- UI for evidence visualization (follow-up sprint) +- Evidence refresh/update workflow +- Third-party evidence import + +## Technical Context + +### Key Files to Create/Extend + +| Component | File | Purpose | +|-----------|------|---------| +| Evidence Linker | `src/Excititor/__Libraries/StellaOps.Excititor.Core/Evidence/VexEvidenceLinker.cs` | NEW: Service to link VEX -> DSSE | +| Evidence Models | `src/Excititor/__Libraries/StellaOps.Excititor.Core/Evidence/VexEvidenceLinkModels.cs` | NEW: Link models | +| CycloneDX Mapper | `src/Excititor/__Libraries/.../CycloneDxVexMapper.cs` | EXTEND: Add evidence links | +| CLI Handler | `src/Cli/StellaOps.Cli/Commands/VexGenCommandGroup.cs` | EXTEND: Add evidence option | + +### VEX with Evidence Link Schema (CycloneDX) + +```json +{ + "bomFormat": "CycloneDX", + "specVersion": "1.6", + "vulnerabilities": [ + { + "id": "CVE-2023-12345", + "source": { "name": "NVD" }, + "analysis": { + "state": "not_affected", + "justification": "code_not_reachable", + "detail": "Binary analysis confirms vendor backport applied. Evidence: oci://registry.example.com/evidence@sha256:abc123", + "response": ["update"], + "firstIssued": "2026-01-13T12:00:00Z" + }, + "affects": [ + { + "ref": "urn:cdx:stellaops/app@1.0.0/libssl.so.3", + "versions": [{ "version": "3.0.2", "status": "unaffected" }] + } + ], + "properties": [ + { + "name": "stellaops:evidence:type", + "value": "binary-diff" + }, + { + "name": "stellaops:evidence:uri", + "value": "oci://registry.example.com/evidence@sha256:abc123..." + }, + { + "name": "stellaops:evidence:confidence", + "value": "0.95" + }, + { + "name": "stellaops:evidence:predicate-type", + "value": "stellaops.binarydiff.v1" + } + ] + } + ] +} +``` + +### Evidence Link Model + +```csharp +public sealed record VexEvidenceLink +{ + /// Type of evidence (binary-diff, reachability, runtime, etc.). + public required string EvidenceType { get; init; } + + /// URI to the DSSE bundle (oci://, cas://, file://). + public required string EvidenceUri { get; init; } + + /// Digest of the DSSE envelope. + public required string EnvelopeDigest { get; init; } + + /// Predicate type in the DSSE envelope. + public required string PredicateType { get; init; } + + /// Confidence score (0.0-1.0). + public required double Confidence { get; init; } + + /// When the evidence was created. + public required DateTimeOffset CreatedAt { get; init; } + + /// Signer identity (key ID or certificate subject). + public string? SignerIdentity { get; init; } + + /// Rekor log index if submitted to transparency log. + public string? RekorLogIndex { get; init; } +} +``` + +## Risk Assessment + +| Risk | Likelihood | Impact | Mitigation | +|------|------------|--------|------------| +| Evidence URI format inconsistency | Medium | Medium | Define URI schema spec; validate on link | +| Stale evidence links | Medium | Low | Include evidence timestamp; optional refresh | +| Large evidence bundles | Low | Medium | Link to bundle, don't embed content | + +## Success Metrics + +- [ ] VEX output includes evidence links when available +- [ ] Evidence URIs resolve to valid DSSE bundles +- [ ] CLI shows evidence in human-readable format +- [ ] CycloneDX VEX validates against schema + +## Documentation Prerequisites + +Before starting implementation, reviewers must read: + +- `docs/README.md` +- `docs/ARCHITECTURE_REFERENCE.md` +- `CLAUDE.md` Section 8 (Code Quality & Determinism Rules) +- CycloneDX VEX specification: https://cyclonedx.org/capabilities/vex/ +- Batch 001 BinaryDiffV1 predicate schema + +## Execution Log + +| Date (UTC) | Update | Owner | +|------------|--------|-------| +| 2026-01-13 | Sprint batch created from advisory analysis. | Project Mgmt | + +## Decisions & Risks + +- **APPROVED 2026-01-13**: Evidence stored as URI references, not embedded content. +- **APPROVED 2026-01-13**: Use CycloneDX `properties[]` for Stella-specific evidence metadata. +- **RISK**: CycloneDX `analysis.detail` has length limits; use URI not full content. + +## Next Checkpoints + +- Batch 001 Sprint 002 complete -> Sprint 1 can start +- Sprint 1 complete -> Sprint 2 can start +- All sprints complete -> Integration testing checkpoint diff --git a/docs/implplan/SPRINT_20260113_003_001_EXCITITOR_vex_evidence_linker.md b/docs/implplan/SPRINT_20260113_003_001_EXCITITOR_vex_evidence_linker.md new file mode 100644 index 000000000..db2f41908 --- /dev/null +++ b/docs/implplan/SPRINT_20260113_003_001_EXCITITOR_vex_evidence_linker.md @@ -0,0 +1,377 @@ +# Sprint 20260113_003_001_EXCITITOR - VEX Evidence Linker Service + +## Topic & Scope + +- Implement VEX-to-evidence linking service +- Auto-link binary-diff attestations to VEX assertions +- Store evidence URIs alongside VEX entries +- Emit evidence metadata in CycloneDX VEX output +- **Working directory:** `src/Excititor/__Libraries/StellaOps.Excititor.Core/` + +## Dependencies & Concurrency + +- **Depends on:** Batch 001 Sprint 002 (BinaryDiffV1 predicate) +- Parallel work safe within Excititor module +- Sprint 2 (CLI) depends on this sprint + +## Documentation Prerequisites + +- `docs/README.md` +- `CLAUDE.md` Section 8 (Determinism Rules) +- CycloneDX VEX specification: https://cyclonedx.org/capabilities/vex/ +- Batch 001 BinaryDiffV1 predicate schema +- Existing VEX models in `src/Excititor/__Libraries/.../VexDeltaModels.cs` + +## Delivery Tracker + +| # | Task ID | Status | Key dependency / next step | Owners | Task Definition | +|---|---------|--------|---------------------------|--------|-----------------| +| 1 | VEX-LINK-MODELS-0001 | TODO | None | Guild - Excititor | Define `VexEvidenceLink`, `VexEvidenceLinkSet`, and `EvidenceType` enum in `Evidence/VexEvidenceLinkModels.cs`. Include URI, digest, predicate type, confidence, timestamps. | +| 2 | VEX-LINK-INTERFACE-0001 | TODO | Depends on MODELS-0001 | Guild - Excititor | Define `IVexEvidenceLinker` interface with `LinkAsync(vexEntry, evidenceSource, ct)` and `GetLinksAsync(vexEntryId, ct)` methods. | +| 3 | VEX-LINK-BINARYDIFF-0001 | TODO | Depends on INTERFACE-0001, Batch 001 | Guild - Excititor | Implement `BinaryDiffEvidenceLinker` that extracts evidence from `BinaryDiffPredicate` findings and creates `VexEvidenceLink` entries. | +| 4 | VEX-LINK-STORE-0001 | TODO | Depends on MODELS-0001 | Guild - Excititor | Implement `IVexEvidenceLinkStore` interface and in-memory implementation. Define PostgreSQL schema for persistent storage. | +| 5 | VEX-LINK-AUTOLINK-0001 | TODO | Depends on BINARYDIFF-0001 | Guild - Excititor | Implement auto-linking pipeline: when binary-diff produces "patched" verdict, create VEX link with appropriate justification. | +| 6 | VEX-LINK-CYCLONEDX-0001 | TODO | Depends on AUTOLINK-0001 | Guild - Excititor | Extend `CycloneDxVexMapper` to emit `analysis.detail` with evidence URI and `properties[]` with evidence metadata. | +| 7 | VEX-LINK-VALIDATION-0001 | TODO | Depends on all above | Guild - Excititor | Implement evidence validation: verify DSSE signature before accepting link. Optional: verify Rekor inclusion. | +| 8 | VEX-LINK-DI-0001 | TODO | Depends on all above | Guild - Excititor | Register all services in DI. Add `IOptions` for configuration (confidence threshold, validation mode). | +| 9 | VEX-LINK-TESTS-0001 | TODO | Depends on all above | Guild - Excititor | Unit tests covering: link creation, storage, auto-linking, CycloneDX output, validation success/failure. | + +## Technical Specification + +### Models + +```csharp +namespace StellaOps.Excititor.Core.Evidence; + +/// +/// Link between a VEX assertion and supporting evidence. +/// +public sealed record VexEvidenceLink +{ + /// Unique link identifier. + public required string LinkId { get; init; } + + /// VEX entry this evidence supports. + public required string VexEntryId { get; init; } + + /// Type of evidence. + public required EvidenceType EvidenceType { get; init; } + + /// URI to the evidence artifact (oci://, cas://, https://). + public required string EvidenceUri { get; init; } + + /// Digest of the DSSE envelope (sha256:...). + public required string EnvelopeDigest { get; init; } + + /// Predicate type in the DSSE envelope. + public required string PredicateType { get; init; } + + /// Confidence score from the evidence (0.0-1.0). + public required double Confidence { get; init; } + + /// Justification derived from evidence. + public required VexJustification Justification { get; init; } + + /// When the evidence was created. + public required DateTimeOffset EvidenceCreatedAt { get; init; } + + /// When the link was created. + public required DateTimeOffset LinkedAt { get; init; } + + /// Signer identity (key ID or certificate subject). + public string? SignerIdentity { get; init; } + + /// Rekor log index if submitted to transparency log. + public string? RekorLogIndex { get; init; } + + /// Whether the evidence signature was validated. + public bool SignatureValidated { get; init; } + + /// Additional metadata as key-value pairs. + public ImmutableDictionary Metadata { get; init; } + = ImmutableDictionary.Empty; +} + +/// +/// Types of evidence that can support VEX assertions. +/// +public enum EvidenceType +{ + /// Binary-level diff showing patch applied. + BinaryDiff, + + /// Call graph analysis showing code not reachable. + ReachabilityAnalysis, + + /// Runtime analysis showing code not executed. + RuntimeAnalysis, + + /// Human attestation (manual review). + HumanAttestation, + + /// Vendor advisory or statement. + VendorAdvisory, + + /// Other/custom evidence type. + Other +} + +/// +/// VEX justification codes (CycloneDX compatible). +/// +public enum VexJustification +{ + CodeNotPresent, + CodeNotReachable, + RequiresConfiguration, + RequiresDependency, + RequiresEnvironment, + ProtectedByCompiler, + ProtectedAtRuntime, + ProtectedAtPerimeter, + ProtectedByMitigatingControl +} + +/// +/// Collection of evidence links for a VEX entry. +/// +public sealed record VexEvidenceLinkSet +{ + /// VEX entry ID. + public required string VexEntryId { get; init; } + + /// All evidence links, sorted by confidence descending. + public required ImmutableArray Links { get; init; } + + /// Highest confidence among all links. + public double MaxConfidence => Links.IsEmpty ? 0 : Links.Max(l => l.Confidence); + + /// Primary link (highest confidence). + public VexEvidenceLink? PrimaryLink => Links.IsEmpty ? null : Links[0]; +} +``` + +### Interfaces + +```csharp +namespace StellaOps.Excititor.Core.Evidence; + +/// +/// Service for linking VEX assertions to supporting evidence. +/// +public interface IVexEvidenceLinker +{ + /// + /// Creates a link between a VEX entry and evidence. + /// + Task LinkAsync( + string vexEntryId, + EvidenceSource source, + CancellationToken cancellationToken = default); + + /// + /// Gets all evidence links for a VEX entry. + /// + Task GetLinksAsync( + string vexEntryId, + CancellationToken cancellationToken = default); + + /// + /// Auto-links evidence from a binary diff result. + /// + Task> AutoLinkFromBinaryDiffAsync( + BinaryDiffPredicate diff, + string dsseEnvelopeUri, + CancellationToken cancellationToken = default); +} + +/// +/// Source of evidence for linking. +/// +public sealed record EvidenceSource +{ + /// Evidence type. + public required EvidenceType Type { get; init; } + + /// URI to the evidence artifact. + public required string Uri { get; init; } + + /// Digest of the artifact. + public required string Digest { get; init; } + + /// Predicate type if DSSE/in-toto. + public string? PredicateType { get; init; } + + /// Confidence score. + public double Confidence { get; init; } = 1.0; + + /// DSSE envelope bytes for validation. + public byte[]? EnvelopeBytes { get; init; } +} + +/// +/// Storage for evidence links. +/// +public interface IVexEvidenceLinkStore +{ + Task SaveAsync(VexEvidenceLink link, CancellationToken ct = default); + Task GetAsync(string linkId, CancellationToken ct = default); + Task> GetByVexEntryAsync(string vexEntryId, CancellationToken ct = default); + Task DeleteAsync(string linkId, CancellationToken ct = default); +} +``` + +### Auto-Link Algorithm + +```pseudo +function AutoLinkFromBinaryDiff(diff, dsseUri): + links = [] + + for finding in diff.findings where finding.verdict == Patched: + // Determine affected VEX entry + vexEntryId = LookupVexEntry(finding.path, diff.inputs.target) + + if vexEntryId is null: + continue // No matching VEX entry + + // Determine justification from finding + justification = DetermineJustification(finding) + + // Create link + link = VexEvidenceLink { + linkId: GenerateId(vexEntryId, dsseUri), + vexEntryId: vexEntryId, + evidenceType: BinaryDiff, + evidenceUri: dsseUri, + envelopeDigest: ComputeDigest(diff), + predicateType: "stellaops.binarydiff.v1", + confidence: finding.confidence ?? 0.9, + justification: justification, + evidenceCreatedAt: diff.metadata.analysisTimestamp, + linkedAt: timeProvider.GetUtcNow() + } + + links.append(link) + + return links + +function DetermineJustification(finding): + // If .text section changed -> code was patched + if finding.sectionDeltas.any(d => d.section == ".text" && d.status == Modified): + return CodeNotPresent // Vulnerable code removed/replaced + + // If only .rodata changed -> data patched + if finding.sectionDeltas.all(d => d.section != ".text"): + return ProtectedAtRuntime // Runtime behavior changed + + return CodeNotReachable // Default for verified patches +``` + +### CycloneDX Output Enhancement + +```csharp +// In CycloneDxVexMapper +private void MapEvidenceLinks(VulnerabilityAnalysis analysis, VexEvidenceLinkSet links) +{ + if (links.PrimaryLink is null) return; + + var primary = links.PrimaryLink; + + // Set analysis.detail with evidence URI + analysis.Detail = $"Evidence: {primary.EvidenceUri}"; + + // Add evidence properties + analysis.Properties ??= []; + analysis.Properties.Add(new Property + { + Name = "stellaops:evidence:type", + Value = primary.EvidenceType.ToString().ToLowerInvariant() + }); + analysis.Properties.Add(new Property + { + Name = "stellaops:evidence:uri", + Value = primary.EvidenceUri + }); + analysis.Properties.Add(new Property + { + Name = "stellaops:evidence:confidence", + Value = primary.Confidence.ToString("F2", CultureInfo.InvariantCulture) + }); + analysis.Properties.Add(new Property + { + Name = "stellaops:evidence:predicate-type", + Value = primary.PredicateType + }); + + if (primary.RekorLogIndex is not null) + { + analysis.Properties.Add(new Property + { + Name = "stellaops:evidence:rekor-index", + Value = primary.RekorLogIndex + }); + } +} +``` + +### Configuration + +```yaml +excititor: + evidence: + linking: + enabled: true + autoLinkOnBinaryDiff: true + confidenceThreshold: 0.8 + validateSignatures: true + validateRekorInclusion: false + maxLinksPerEntry: 10 +``` + +## Determinism Requirements + +1. **Link ID generation**: Deterministic from vexEntryId + evidenceUri +2. **Ordering**: Links sorted by confidence DESC, then by linkedAt ASC +3. **Timestamps**: From injected `TimeProvider` +4. **Confidence formatting**: Two decimal places, InvariantCulture + +## Test Cases + +### Unit Tests + +| Test | Description | Expected | +|------|-------------|----------| +| `Link_ValidSource_CreatesLink` | Link with valid evidence | Link created with correct fields | +| `Link_DuplicateSource_Deduplicates` | Same source linked twice | Single link returned | +| `AutoLink_PatchedFinding_CreatesLinks` | Binary diff with patched verdict | Links created for affected entries | +| `AutoLink_VanillaFinding_NoLinks` | Binary diff with vanilla verdict | No links created | +| `GetLinks_ExistingEntry_ReturnsSet` | Query by VEX entry ID | All links returned, sorted | +| `MapCycloneDx_WithLinks_IncludesEvidence` | CycloneDX export with links | Properties contain evidence metadata | +| `Validate_ValidSignature_Succeeds` | DSSE with valid signature | Validation passes | +| `Validate_InvalidSignature_Rejects` | DSSE with bad signature | Validation fails, link rejected | + +### Integration Tests + +| Test | Description | Expected | +|------|-------------|----------| +| `EndToEnd_BinaryDiffToVex_LinksEvidence` | Full pipeline from diff to VEX | VEX output contains evidence links | + +## Execution Log + +| Date (UTC) | Update | Owner | +|------------|--------|-------| +| 2026-01-13 | Sprint created from advisory analysis. | Project Mgmt | + +## Decisions & Risks + +- **APPROVED**: Evidence stored as URIs, not embedded content. +- **APPROVED**: Auto-link only for high-confidence findings (>= threshold). +- **RISK**: Signature validation may fail for offline evidence; add bypass option. +- **RISK**: VEX entry lookup requires correlation logic; may need component PURL matching. + +## Next Checkpoints + +- Task 1-4 complete -> Core linking operational +- Task 5-6 complete -> Auto-link and CycloneDX working +- Task 9 complete -> Sprint can be marked DONE +- Unblock Sprint 2 (CLI) diff --git a/docs/implplan/SPRINT_20260113_003_002_CLI_vex_evidence_integration.md b/docs/implplan/SPRINT_20260113_003_002_CLI_vex_evidence_integration.md new file mode 100644 index 000000000..91c6f3a3b --- /dev/null +++ b/docs/implplan/SPRINT_20260113_003_002_CLI_vex_evidence_integration.md @@ -0,0 +1,132 @@ +# Sprint 20260113_003_002_CLI - VEX Generation with Evidence Links + +## Topic & Scope + +- Extend `stella vex gen` command with evidence linking +- Add `--link-evidence` flag to include binary-diff evidence +- Display evidence summary in human-readable output +- Emit evidence metadata in JSON output +- **Working directory:** `src/Cli/StellaOps.Cli/Commands/` + +## Dependencies & Concurrency + +- **Depends on:** Sprint 003_001 (VEX Evidence Linker) +- Extends existing `VexGenCommandGroup.cs` + +## Documentation Prerequisites + +- `docs/README.md` +- `CLAUDE.md` Section 8 (Determinism Rules) +- Existing VEX CLI in `src/Cli/StellaOps.Cli/Commands/VexGenCommandGroup.cs` +- Sprint 003_001 models and interfaces + +## Delivery Tracker + +| # | Task ID | Status | Key dependency / next step | Owners | Task Definition | +|---|---------|--------|---------------------------|--------|-----------------| +| 1 | CLI-VEX-EVIDENCE-OPT-0001 | TODO | None | Guild - CLI | Add `--link-evidence` option to `stella vex gen` command. Default: true if evidence available. | +| 2 | CLI-VEX-EVIDENCE-HANDLER-0001 | TODO | Depends on OPT-0001, Sprint 001 | Guild - CLI | Extend VEX generation handler to call `IVexEvidenceLinker.GetLinksAsync()` and include in output. | +| 3 | CLI-VEX-EVIDENCE-JSON-0001 | TODO | Depends on HANDLER-0001 | Guild - CLI | Emit evidence links in JSON output under `evidence` key per vulnerability. | +| 4 | CLI-VEX-EVIDENCE-TABLE-0001 | TODO | Depends on HANDLER-0001 | Guild - CLI | Show evidence summary in table output: type, confidence, URI (truncated). | +| 5 | CLI-VEX-EVIDENCE-TESTS-0001 | TODO | Depends on all above | Guild - CLI | Unit tests for evidence flag, output formats, missing evidence handling. | + +## Technical Specification + +### Command Enhancement + +``` +stella vex gen [options] + +Existing options: + --output, -o Output format (json, table, cyclonedx) + --format, -f VEX format (openvex, cyclonedx) + +New options: + --link-evidence Include evidence links in output (default: true) + --evidence-threshold Minimum confidence for evidence (default: 0.8) + --show-evidence-uri Show full evidence URIs (default: truncated) +``` + +### Output Examples + +#### Table Output with Evidence + +``` +VEX Report for scan abc123 + ++----------------+-------------+----------------+------------+------------------+ +| CVE | Component | Status | Confidence | Evidence | ++----------------+-------------+----------------+------------+------------------+ +| CVE-2023-12345 | libssl.so.3 | not_affected | 0.95 | binary-diff [OK] | +| CVE-2023-67890 | libcrypto | affected | - | (none) | +| CVE-2024-11111 | nginx | not_affected | 0.88 | reachability | ++----------------+-------------+----------------+------------+------------------+ + +Evidence Details: + CVE-2023-12345: oci://registry/evidence@sha256:abc123... + Type: binary-diff, Predicate: stellaops.binarydiff.v1 + Signer: CN=StellaOps Signing Key +``` + +#### JSON Output with Evidence + +```json +{ + "scanId": "abc123", + "generatedAt": "2026-01-13T12:00:00Z", + "vulnerabilities": [ + { + "id": "CVE-2023-12345", + "component": "libssl.so.3", + "status": "not_affected", + "justification": "code_not_present", + "evidence": { + "type": "binary-diff", + "uri": "oci://registry/evidence@sha256:abc123...", + "confidence": 0.95, + "predicateType": "stellaops.binarydiff.v1", + "validatedSignature": true, + "rekorIndex": "12345678" + } + } + ] +} +``` + +### Implementation Notes + +```csharp +// Extend HandleVexGenAsync +if (linkEvidence) +{ + var linker = services.GetRequiredService(); + foreach (var entry in vexEntries) + { + var links = await linker.GetLinksAsync(entry.Id, ct); + if (links.PrimaryLink is not null && links.MaxConfidence >= evidenceThreshold) + { + entry.Evidence = links.PrimaryLink; + } + } +} +``` + +## Test Cases + +| Test | Description | Expected | +|------|-------------|----------| +| `VexGen_WithEvidence_IncludesLinks` | Evidence available | Links in output | +| `VexGen_NoEvidence_OmitsField` | No evidence | `evidence: null` | +| `VexGen_BelowThreshold_Filtered` | Low confidence evidence | Evidence omitted | +| `VexGen_TableFormat_ShowsSummary` | Table output | Evidence column populated | + +## Execution Log + +| Date (UTC) | Update | Owner | +|------------|--------|-------| +| 2026-01-13 | Sprint created from advisory analysis. | Project Mgmt | + +## Next Checkpoints + +- All tasks complete -> Sprint can be marked DONE +- Batch 003 complete -> Evidence chain operational diff --git a/docs/implplan/SPRINT_20260113_004_000_INDEX_golden_pairs_pilot.md b/docs/implplan/SPRINT_20260113_004_000_INDEX_golden_pairs_pilot.md new file mode 100644 index 000000000..00cd73e60 --- /dev/null +++ b/docs/implplan/SPRINT_20260113_004_000_INDEX_golden_pairs_pilot.md @@ -0,0 +1,273 @@ +# Sprint Batch 20260113_004 - Golden Pairs Pilot (Vendor Backport Corpus) + +## Executive Summary + +This sprint batch implements a **curated dataset infrastructure** for binary patch verification. "Golden pairs" are matched sets of stock (upstream) vs vendor-patched binaries tied to specific CVEs, enabling validation of the binary diff system's ability to detect vendor backports. + +**Scope:** Pilot corpus with 3 CVEs (Dirty Pipe, sudo Baron Samedit, PrintNightmare) +**Effort Estimate:** 5-6 story points across 3 sprints +**Priority:** Medium (validation infrastructure) + +## Background + +### Advisory Requirements + +The original advisory specified: + +> A curated dataset of **stock vs vendor-patched binaries** tied to authoritative **CVE + patch evidence** lets Stella Ops prove (with bytes) that a fix is present, powering deterministic VEX and "evidence-first" decisions. + +> **Starter CVEs (tiny pilot):** +> - **Linux:** Dirty Pipe (CVE-2022-0847) - kernel backport showcase +> - **Unix userland:** sudo "Baron Samedit" (CVE-2021-3156) - classic multi-distro patch +> - **Windows:** PrintNightmare (CVE-2021-34527) - PE + KB workflow + +### Why Golden Pairs Matter + +1. **Validation**: Ground truth for testing binary diff accuracy +2. **Regression Testing**: Detect if changes break patch detection +3. **Precision Metrics**: Measure actual false positive/negative rates +4. **Documentation**: Examples of vendor backport patterns + +### Existing Capabilities + +| Component | Status | Location | +|-----------|--------|----------| +| ELF Section Hash Extractor | IN PROGRESS | Batch 001 Sprint 001 | +| BinaryDiffV1 Predicate | IN PROGRESS | Batch 001 Sprint 002 | +| Function Fingerprinting | EXISTS | `src/BinaryIndex/__Libraries/.../FingerprintModels.cs` | +| Build-ID Index | EXISTS | `src/Scanner/.../Index/OfflineBuildIdIndex.cs` | + +### Gap Analysis + +| Capability | Status | +|------------|--------| +| Golden pairs data model | MISSING | +| Package mirror scripts | MISSING | +| Diff pipeline for corpus | MISSING | +| Validation harness | MISSING | + +## Sprint Index + +| Sprint | ID | Module | Topic | Status | Owner | +|--------|-----|--------|-------|--------|-------| +| 1 | SPRINT_20260113_004_001 | TOOLS | Golden Pairs Data Model & Schema | TODO | Guild - Tools | +| 2 | SPRINT_20260113_004_002 | TOOLS | Mirror & Diff Pipeline | TODO | Guild - Tools | +| 3 | SPRINT_20260113_004_003 | TOOLS | Pilot CVE Corpus (3 CVEs) | TODO | Guild - Tools | + +## Dependencies + +``` ++-----------------------------------------------------------------------+ +| Dependency Graph | ++-----------------------------------------------------------------------+ +| | +| Batch 001 (ELF Section Hashes) | +| | | +| v | +| Sprint 1 (Data Model) | +| | | +| v | +| Sprint 2 (Mirror & Diff Pipeline) | +| | | +| v | +| Sprint 3 (Pilot Corpus) | +| | ++-----------------------------------------------------------------------+ +``` + +**Cross-Batch Dependencies:** +- Batch 001 Sprint 001 (ELF Section Hashes) should be complete for validation +- Pipeline uses section hashes for diff validation + +## Acceptance Criteria (Batch-Level) + +### Must Have + +1. **Data Model** + - Schema for golden pair metadata (CVE, package, distro, versions) + - Support for ELF (Linux) and PE (Windows) binaries + - Storage for original + patched binaries with hashes + - Links to vendor advisories and patch commits + +2. **Mirror Scripts** + - Fetch pre-patch and post-patch package versions + - Support Debian/Ubuntu apt repos + - Hash verification on download + - Deterministic mirroring (reproducible) + +3. **Diff Pipeline** + - Run section hash extraction on pairs + - Produce comparison JSON report + - Compute match/mismatch metrics + - Validate against expected outcomes + +4. **Pilot Corpus (3 CVEs)** + - CVE-2022-0847 (Dirty Pipe): Linux kernel pair + - CVE-2021-3156 (Baron Samedit): sudo binary pair + - CVE-2021-34527 (PrintNightmare): Windows spoolsv.dll pair (if PE ready) + +### Should Have + +- Debug symbol extraction (dbgsym packages) +- Function-level diff report +- CI integration for regression testing + +### Deferred (Out of Scope) + +- Ghidra/Diaphora integration (separate sprint) +- Full multi-distro coverage +- Automated corpus updates + +## Technical Context + +### Repository Layout + +``` +src/Tools/GoldenPairs/ ++-- StellaOps.Tools.GoldenPairs/ +| +-- Models/ +| | +-- GoldenPairMetadata.cs +| | +-- BinaryArtifact.cs +| | +-- DiffReport.cs +| +-- Services/ +| | +-- PackageMirrorService.cs +| | +-- DiffPipelineService.cs +| | +-- ValidationService.cs +| +-- Program.cs ++-- __Tests/ + +-- StellaOps.Tools.GoldenPairs.Tests/ + +datasets/golden-pairs/ ++-- CVE-2022-0847/ +| +-- metadata.json +| +-- original/ +| | +-- vmlinux-5.16.11 +| | +-- vmlinux-5.16.11.sha256 +| +-- patched/ +| | +-- vmlinux-5.16.12 +| | +-- vmlinux-5.16.12.sha256 +| +-- diff-report.json +| +-- golden-diff.json (expected outcomes) +| +-- advisories/ +| +-- ubuntu-usn-####.md +| +-- kernel-commit.txt ++-- CVE-2021-3156/ +| +-- ... ++-- index.json (corpus manifest) ++-- README.md +``` + +### Metadata Schema + +```json +{ + "$schema": "https://stellaops.io/schemas/golden-pair-v1.schema.json", + "cve": "CVE-2022-0847", + "name": "Dirty Pipe", + "description": "Linux kernel pipe buffer flag handling vulnerability", + "severity": "high", + "artifact": { + "name": "vmlinux", + "format": "elf", + "architecture": "x86_64" + }, + "original": { + "package": "linux-image-5.16.11-generic", + "version": "5.16.11", + "distro": "Ubuntu 22.04", + "source": "apt://archive.ubuntu.com/ubuntu", + "sha256": "abc123...", + "buildId": "def456..." + }, + "patched": { + "package": "linux-image-5.16.12-generic", + "version": "5.16.12", + "distro": "Ubuntu 22.04", + "source": "apt://archive.ubuntu.com/ubuntu", + "sha256": "ghi789...", + "buildId": "jkl012..." + }, + "patch": { + "commit": "9d2231c5d74e13b2a0546fee6737ee4446017903", + "upstream": "https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=...", + "functions_changed": ["copy_page_to_iter_pipe", "push_pipe"] + }, + "advisories": [ + {"source": "ubuntu", "id": "USN-5317-1", "url": "https://ubuntu.com/security/notices/USN-5317-1"}, + {"source": "nvd", "id": "CVE-2022-0847", "url": "https://nvd.nist.gov/vuln/detail/CVE-2022-0847"} + ], + "expected_diff": { + "sections_changed": [".text"], + "sections_identical": [".rodata", ".data"], + "verdict": "patched", + "confidence_min": 0.9 + }, + "created_at": "2026-01-13T12:00:00Z", + "created_by": "StellaOps Golden Pairs Tool v1.0.0" +} +``` + +### Diff Report Schema + +```json +{ + "cve": "CVE-2022-0847", + "original": {"sha256": "...", "buildId": "..."}, + "patched": {"sha256": "...", "buildId": "..."}, + "sections": [ + {"name": ".text", "status": "modified", "original_hash": "...", "patched_hash": "...", "size_delta": 1024}, + {"name": ".rodata", "status": "identical", "hash": "..."}, + {"name": ".data", "status": "identical", "hash": "..."} + ], + "verdict": "patched", + "confidence": 0.95, + "matches_expected": true, + "analyzed_at": "2026-01-13T12:00:00Z", + "tool_version": "1.0.0" +} +``` + +## Risk Assessment + +| Risk | Likelihood | Impact | Mitigation | +|------|------------|--------|------------| +| Package availability | Medium | High | Cache packages locally; document alternatives | +| Kernel binary size | Medium | Medium | Extract specific objects, not full vmlinux | +| Windows PE complexity | High | Medium | Defer PrintNightmare if PE support not ready | +| Hash instability | Low | Medium | Pin to specific package versions | + +## Success Metrics + +- [ ] 3 CVE pairs with complete metadata +- [ ] Mirror scripts fetch correct versions +- [ ] Diff pipeline produces expected verdicts +- [ ] CI regression test passes +- [ ] Documentation complete + +## Documentation Prerequisites + +Before starting implementation, reviewers must read: + +- `docs/README.md` +- `CLAUDE.md` Section 8 (Code Quality & Determinism Rules) +- Batch 001 ELF section hash schema +- ELF specification for section analysis + +## Execution Log + +| Date (UTC) | Update | Owner | +|------------|--------|-------| +| 2026-01-13 | Sprint batch created from advisory analysis. | Project Mgmt | + +## Decisions & Risks + +- **APPROVED 2026-01-13**: Pilot with 3 CVEs; expand corpus in follow-up sprint. +- **APPROVED 2026-01-13**: Focus on ELF first; PE support conditional on Batch 001 progress. +- **APPROVED 2026-01-13**: Store binaries in datasets/, not in git LFS initially. +- **RISK**: Kernel binaries are large; consider extracting specific .ko modules instead. + +## Next Checkpoints + +- Sprint 1 complete -> Data model ready for population +- Sprint 2 complete -> Pipeline can process pairs +- Sprint 3 complete -> Pilot corpus validated, CI integrated diff --git a/docs/implplan/SPRINT_20260113_004_001_TOOLS_golden_pairs_data_model.md b/docs/implplan/SPRINT_20260113_004_001_TOOLS_golden_pairs_data_model.md new file mode 100644 index 000000000..d098a1896 --- /dev/null +++ b/docs/implplan/SPRINT_20260113_004_001_TOOLS_golden_pairs_data_model.md @@ -0,0 +1,346 @@ +# Sprint 20260113_004_001_TOOLS - Golden Pairs Data Model & Schema + +## Topic & Scope + +- Define data model for golden pair metadata +- Create JSON schema for validation +- Implement C# models for tooling +- Design storage structure for artifacts +- **Working directory:** `src/Tools/GoldenPairs/` + +## Dependencies & Concurrency + +- No blocking dependencies (foundational sprint) +- Sprint 2 (Pipeline) depends on this sprint's models +- Can proceed in parallel with Batch 001 + +## Documentation Prerequisites + +- `docs/README.md` +- `CLAUDE.md` Section 8 (Determinism Rules) +- ELF section types and flags +- PE section characteristics + +## Delivery Tracker + +| # | Task ID | Status | Key dependency / next step | Owners | Task Definition | +|---|---------|--------|---------------------------|--------|-----------------| +| 1 | GP-MODEL-METADATA-0001 | TODO | None | Guild - Tools | Define `GoldenPairMetadata` record with CVE, artifact, original/patched refs, patch info, advisories, expected diff. | +| 2 | GP-MODEL-ARTIFACT-0001 | TODO | None | Guild - Tools | Define `BinaryArtifact` record with package, version, distro, source, hashes, buildId, symbols availability. | +| 3 | GP-MODEL-DIFF-0001 | TODO | None | Guild - Tools | Define `GoldenDiffReport` record with section comparison, verdict, confidence, tool version. | +| 4 | GP-SCHEMA-JSON-0001 | TODO | Depends on MODEL-* | Guild - Tools | Create JSON Schema `golden-pair-v1.schema.json` for metadata validation. Publish to `docs/schemas/`. | +| 5 | GP-SCHEMA-INDEX-0001 | TODO | Depends on SCHEMA-JSON | Guild - Tools | Create corpus index schema `golden-pairs-index.schema.json` for dataset manifest. | +| 6 | GP-STORAGE-LAYOUT-0001 | TODO | Depends on MODEL-* | Guild - Tools | Document storage layout in `datasets/golden-pairs/README.md`. Include artifact naming conventions. | +| 7 | GP-MODEL-LOADER-0001 | TODO | Depends on all models | Guild - Tools | Implement `GoldenPairLoader` service to read/validate metadata from filesystem. | +| 8 | GP-MODEL-TESTS-0001 | TODO | Depends on all above | Guild - Tools | Unit tests for model serialization, schema validation, loader functionality. | + +## Technical Specification + +### Core Models + +```csharp +namespace StellaOps.Tools.GoldenPairs.Models; + +/// +/// Metadata for a golden pair (stock vs patched binary). +/// +public sealed record GoldenPairMetadata +{ + /// CVE identifier (e.g., "CVE-2022-0847"). + public required string Cve { get; init; } + + /// Human-readable vulnerability name. + public required string Name { get; init; } + + /// Brief description of the vulnerability. + public string? Description { get; init; } + + /// Severity level (critical, high, medium, low). + public required string Severity { get; init; } + + /// Target artifact information. + public required ArtifactInfo Artifact { get; init; } + + /// Original (unpatched) binary. + public required BinaryArtifact Original { get; init; } + + /// Patched binary. + public required BinaryArtifact Patched { get; init; } + + /// Patch commit/change information. + public required PatchInfo Patch { get; init; } + + /// Security advisories for this CVE. + public ImmutableArray Advisories { get; init; } = []; + + /// Expected diff results for validation. + public required ExpectedDiff ExpectedDiff { get; init; } + + /// When this pair was created. + public required DateTimeOffset CreatedAt { get; init; } + + /// Tool version that created this pair. + public required string CreatedBy { get; init; } +} + +/// +/// Information about the target artifact. +/// +public sealed record ArtifactInfo +{ + /// Artifact name (e.g., "vmlinux", "sudo", "spoolsv.dll"). + public required string Name { get; init; } + + /// Binary format (elf, pe, macho). + public required string Format { get; init; } + + /// CPU architecture (x86_64, aarch64, etc.). + public required string Architecture { get; init; } + + /// Operating system (linux, windows, darwin). + public string Os { get; init; } = "linux"; +} + +/// +/// A binary artifact in the golden pair. +/// +public sealed record BinaryArtifact +{ + /// Package name (e.g., "linux-image-5.16.11-generic"). + public required string Package { get; init; } + + /// Package version. + public required string Version { get; init; } + + /// Distribution (e.g., "Ubuntu 22.04", "Debian 11"). + public required string Distro { get; init; } + + /// Package source (apt://, https://, file://). + public required string Source { get; init; } + + /// SHA-256 hash of the binary. + public required string Sha256 { get; init; } + + /// ELF Build-ID or PE GUID (if available). + public string? BuildId { get; init; } + + /// Debug symbols available. + public bool HasDebugSymbols { get; init; } + + /// Path to debug symbols package. + public string? DebugSymbolsSource { get; init; } + + /// Relative path within the package. + public string? PathInPackage { get; init; } +} + +/// +/// Information about the security patch. +/// +public sealed record PatchInfo +{ + /// Commit hash of the fix. + public required string Commit { get; init; } + + /// URL to upstream commit. + public string? Upstream { get; init; } + + /// Functions changed by the patch. + public ImmutableArray FunctionsChanged { get; init; } = []; + + /// Files changed by the patch. + public ImmutableArray FilesChanged { get; init; } = []; + + /// Patch summary. + public string? Summary { get; init; } +} + +/// +/// Reference to a security advisory. +/// +public sealed record AdvisoryRef +{ + /// Advisory source (ubuntu, debian, nvd, msrc, etc.). + public required string Source { get; init; } + + /// Advisory identifier (e.g., "USN-5317-1"). + public required string Id { get; init; } + + /// URL to the advisory. + public required string Url { get; init; } +} + +/// +/// Expected diff results for validation. +/// +public sealed record ExpectedDiff +{ + /// Sections expected to be modified. + public ImmutableArray SectionsChanged { get; init; } = []; + + /// Sections expected to be identical. + public ImmutableArray SectionsIdentical { get; init; } = []; + + /// Expected verdict (patched, vanilla, unknown). + public required string Verdict { get; init; } + + /// Minimum confidence score expected. + public double ConfidenceMin { get; init; } = 0.9; +} +``` + +### Diff Report Model + +```csharp +/// +/// Report from comparing a golden pair. +/// +public sealed record GoldenDiffReport +{ + /// CVE being analyzed. + public required string Cve { get; init; } + + /// Original binary info. + public required ArtifactHashInfo Original { get; init; } + + /// Patched binary info. + public required ArtifactHashInfo Patched { get; init; } + + /// Section-by-section comparison. + public required ImmutableArray Sections { get; init; } + + /// Overall verdict. + public required string Verdict { get; init; } + + /// Confidence score (0.0-1.0). + public required double Confidence { get; init; } + + /// Whether result matches expected. + public required bool MatchesExpected { get; init; } + + /// Discrepancies from expected (if any). + public ImmutableArray Discrepancies { get; init; } = []; + + /// Analysis timestamp. + public required DateTimeOffset AnalyzedAt { get; init; } + + /// Tool version. + public required string ToolVersion { get; init; } +} + +public sealed record ArtifactHashInfo +{ + public required string Sha256 { get; init; } + public string? BuildId { get; init; } +} + +public sealed record SectionComparison +{ + public required string Name { get; init; } + public required string Status { get; init; } // identical, modified, added, removed + public string? OriginalHash { get; init; } + public string? PatchedHash { get; init; } + public long? SizeDelta { get; init; } +} +``` + +### JSON Schema (Excerpt) + +```json +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://stellaops.io/schemas/golden-pair-v1.schema.json", + "title": "GoldenPairMetadata", + "type": "object", + "required": ["cve", "name", "severity", "artifact", "original", "patched", "patch", "expectedDiff", "createdAt", "createdBy"], + "properties": { + "cve": { + "type": "string", + "pattern": "^CVE-\\d{4}-\\d{4,}$" + }, + "name": { "type": "string", "minLength": 1 }, + "severity": { "enum": ["critical", "high", "medium", "low"] }, + "artifact": { "$ref": "#/$defs/ArtifactInfo" }, + "original": { "$ref": "#/$defs/BinaryArtifact" }, + "patched": { "$ref": "#/$defs/BinaryArtifact" }, + "patch": { "$ref": "#/$defs/PatchInfo" }, + "advisories": { + "type": "array", + "items": { "$ref": "#/$defs/AdvisoryRef" } + }, + "expectedDiff": { "$ref": "#/$defs/ExpectedDiff" }, + "createdAt": { "type": "string", "format": "date-time" }, + "createdBy": { "type": "string" } + }, + "$defs": { + "ArtifactInfo": { + "type": "object", + "required": ["name", "format", "architecture"], + "properties": { + "name": { "type": "string" }, + "format": { "enum": ["elf", "pe", "macho"] }, + "architecture": { "type": "string" } + } + } + // ... additional definitions + } +} +``` + +### Storage Layout + +``` +datasets/golden-pairs/ ++-- index.json # Corpus manifest ++-- README.md # Documentation ++-- CVE-2022-0847/ +| +-- metadata.json # GoldenPairMetadata +| +-- original/ +| | +-- vmlinux # Unpatched binary +| | +-- vmlinux.sha256 # Hash file +| | +-- vmlinux.sections.json # Pre-computed section hashes +| +-- patched/ +| | +-- vmlinux # Patched binary +| | +-- vmlinux.sha256 +| | +-- vmlinux.sections.json +| +-- diff-report.json # Comparison output +| +-- advisories/ +| +-- USN-5317-1.txt # Advisory text ++-- CVE-2021-3156/ + +-- ... +``` + +## Determinism Requirements + +1. **Hashes**: SHA-256 lowercase hex, no prefix +2. **Timestamps**: UTC ISO-8601 +3. **Ordering**: Sections sorted by name; advisories sorted by source+id +4. **JSON**: Canonical formatting (sorted keys, 2-space indent) + +## Test Cases + +| Test | Description | Expected | +|------|-------------|----------| +| `Serialize_RoundTrip_Identical` | Serialize then deserialize | Identical metadata | +| `Validate_ValidSchema_Passes` | Valid JSON against schema | Validation passes | +| `Validate_MissingCve_Fails` | Missing required field | Validation fails | +| `Load_ExistingPair_ReturnsMetadata` | Load from filesystem | Correct metadata | +| `Load_MissingFiles_ReturnsError` | Missing artifact files | Error with details | + +## Execution Log + +| Date (UTC) | Update | Owner | +|------------|--------|-------| +| 2026-01-13 | Sprint created from advisory analysis. | Project Mgmt | + +## Decisions & Risks + +- **APPROVED**: Store binaries outside git, reference by hash. +- **APPROVED**: Pre-compute section hashes for faster diff pipeline. +- **RISK**: Large binaries may exceed storage limits; use compression. + +## Next Checkpoints + +- Task 1-3 complete -> Core models ready +- Task 4-6 complete -> Schema and storage documented +- Task 7-8 complete -> Sprint can be marked DONE diff --git a/docs/implplan/SPRINT_20260113_004_002_TOOLS_mirror_diff_pipeline.md b/docs/implplan/SPRINT_20260113_004_002_TOOLS_mirror_diff_pipeline.md new file mode 100644 index 000000000..c5905d469 --- /dev/null +++ b/docs/implplan/SPRINT_20260113_004_002_TOOLS_mirror_diff_pipeline.md @@ -0,0 +1,330 @@ +# Sprint 20260113_004_002_TOOLS - Mirror & Diff Pipeline + +## Topic & Scope + +- Implement package mirror service for Debian/Ubuntu +- Create diff pipeline service for golden pair validation +- Build validation harness for expected outcomes +- Support reproducible artifact fetching +- **Working directory:** `src/Tools/GoldenPairs/` + +## Dependencies & Concurrency + +- **Depends on:** Sprint 004_001 (Data Model) +- **Depends on:** Batch 001 Sprint 001 (ELF Section Hashes) +- Sprint 3 (Pilot Corpus) depends on this sprint + +## Documentation Prerequisites + +- `docs/README.md` +- `CLAUDE.md` Section 8 (Determinism Rules) +- Sprint 004_001 data models +- Batch 001 `ElfSectionHashExtractor` interface +- Debian/Ubuntu apt repository structure + +## Delivery Tracker + +| # | Task ID | Status | Key dependency / next step | Owners | Task Definition | +|---|---------|--------|---------------------------|--------|-----------------| +| 1 | GP-MIRROR-INTERFACE-0001 | TODO | None | Guild - Tools | Define `IPackageMirrorService` interface with `FetchAsync(artifact, destination, ct)` signature. Support verification and resume. | +| 2 | GP-MIRROR-APT-0001 | TODO | Depends on INTERFACE | Guild - Tools | Implement `AptPackageMirrorService` for Debian/Ubuntu. Parse Packages.gz, download .deb, extract target binary. | +| 3 | GP-MIRROR-VERIFY-0001 | TODO | Depends on APT | Guild - Tools | Implement hash verification: compare downloaded SHA-256 with metadata. Fail if mismatch. | +| 4 | GP-DIFF-INTERFACE-0001 | TODO | Sprint 001 models | Guild - Tools | Define `IDiffPipelineService` interface with `DiffAsync(pair, ct)` returning `GoldenDiffReport`. | +| 5 | GP-DIFF-IMPL-0001 | TODO | Depends on INTERFACE, Batch 001 | Guild - Tools | Implement `DiffPipelineService` that: loads metadata, extracts section hashes, compares, produces report. | +| 6 | GP-DIFF-VALIDATE-0001 | TODO | Depends on IMPL | Guild - Tools | Implement validation against `expectedDiff`: check sections changed/identical, verdict, confidence threshold. | +| 7 | GP-CLI-MIRROR-0001 | TODO | Depends on MIRROR-* | Guild - Tools | Add `golden-pairs mirror ` CLI command to fetch artifacts for a pair. | +| 8 | GP-CLI-DIFF-0001 | TODO | Depends on DIFF-* | Guild - Tools | Add `golden-pairs diff ` CLI command to run diff and validation. | +| 9 | GP-CLI-VALIDATE-0001 | TODO | Depends on all above | Guild - Tools | Add `golden-pairs validate` CLI command to run all pairs and produce summary. | +| 10 | GP-TESTS-0001 | TODO | Depends on all above | Guild - Tools | Unit and integration tests for mirror, diff, validation services. | + +## Technical Specification + +### Mirror Service Interface + +```csharp +namespace StellaOps.Tools.GoldenPairs.Services; + +/// +/// Service for mirroring package artifacts. +/// +public interface IPackageMirrorService +{ + /// + /// Fetches an artifact from its source. + /// + /// Artifact to fetch. + /// Local destination path. + /// Cancellation token. + /// Result with hash and path. + Task FetchAsync( + BinaryArtifact artifact, + string destination, + CancellationToken cancellationToken = default); + + /// + /// Verifies a local artifact against expected hash. + /// + Task VerifyAsync( + string path, + string expectedSha256, + CancellationToken cancellationToken = default); +} + +public sealed record MirrorResult +{ + public required bool Success { get; init; } + public required string LocalPath { get; init; } + public required string ActualSha256 { get; init; } + public bool HashMatches { get; init; } + public string? ErrorMessage { get; init; } + public long BytesDownloaded { get; init; } +} +``` + +### Apt Mirror Implementation + +```csharp +public class AptPackageMirrorService : IPackageMirrorService +{ + private readonly IHttpClientFactory _httpClientFactory; + private readonly ILogger _logger; + + public async Task FetchAsync( + BinaryArtifact artifact, + string destination, + CancellationToken ct = default) + { + // Parse source URI: apt://archive.ubuntu.com/ubuntu/pool/main/l/linux/... + var uri = ParseAptUri(artifact.Source); + + // Download .deb package + var debPath = Path.Combine(destination, $"{artifact.Package}.deb"); + await DownloadWithRetryAsync(uri, debPath, ct); + + // Extract target binary from .deb + var binaryPath = await ExtractFromDebAsync(debPath, artifact.PathInPackage, destination, ct); + + // Verify hash + var actualHash = await ComputeSha256Async(binaryPath, ct); + var hashMatches = string.Equals(actualHash, artifact.Sha256, StringComparison.OrdinalIgnoreCase); + + return new MirrorResult + { + Success = hashMatches, + LocalPath = binaryPath, + ActualSha256 = actualHash, + HashMatches = hashMatches, + ErrorMessage = hashMatches ? null : $"Hash mismatch: expected {artifact.Sha256}, got {actualHash}" + }; + } + + private async Task ExtractFromDebAsync( + string debPath, + string? pathInPackage, + string destination, + CancellationToken ct) + { + // .deb is ar archive containing data.tar.* with actual files + // Use ar + tar to extract, or SharpCompress library + // ... + } +} +``` + +### Diff Pipeline Interface + +```csharp +/// +/// Pipeline for diffing golden pairs. +/// +public interface IDiffPipelineService +{ + /// + /// Runs diff analysis on a golden pair. + /// + Task DiffAsync( + GoldenPairMetadata pair, + DiffOptions? options = null, + CancellationToken cancellationToken = default); + + /// + /// Validates a diff report against expected outcomes. + /// + ValidationResult Validate(GoldenDiffReport report, ExpectedDiff expected); +} + +public sealed record DiffOptions +{ + /// Sections to analyze (default: all). + public ImmutableArray? SectionFilter { get; init; } + + /// Skip hash computation if pre-computed hashes exist. + public bool UsePrecomputedHashes { get; init; } = true; + + /// Include function-level analysis if debug symbols available. + public bool IncludeFunctionAnalysis { get; init; } = false; +} + +public sealed record ValidationResult +{ + public required bool IsValid { get; init; } + public required ImmutableArray Errors { get; init; } + public required ImmutableArray Warnings { get; init; } +} +``` + +### Diff Pipeline Implementation + +```csharp +public class DiffPipelineService : IDiffPipelineService +{ + private readonly IElfSectionHashExtractor _elfExtractor; + private readonly TimeProvider _timeProvider; + private readonly ILogger _logger; + + public async Task DiffAsync( + GoldenPairMetadata pair, + DiffOptions? options = null, + CancellationToken ct = default) + { + options ??= new DiffOptions(); + + // Get or compute section hashes + var originalHashes = await GetSectionHashesAsync(pair, isOriginal: true, options, ct); + var patchedHashes = await GetSectionHashesAsync(pair, isOriginal: false, options, ct); + + // Compare sections + var sections = CompareSections(originalHashes, patchedHashes, options.SectionFilter); + + // Determine verdict + var (verdict, confidence) = DetermineVerdict(sections, pair.ExpectedDiff); + + // Validate against expected + var matchesExpected = ValidateAgainstExpected(sections, verdict, confidence, pair.ExpectedDiff); + + return new GoldenDiffReport + { + Cve = pair.Cve, + Original = new ArtifactHashInfo { Sha256 = pair.Original.Sha256, BuildId = pair.Original.BuildId }, + Patched = new ArtifactHashInfo { Sha256 = pair.Patched.Sha256, BuildId = pair.Patched.BuildId }, + Sections = sections, + Verdict = verdict, + Confidence = confidence, + MatchesExpected = matchesExpected.IsValid, + Discrepancies = matchesExpected.Errors, + AnalyzedAt = _timeProvider.GetUtcNow(), + ToolVersion = GetToolVersion() + }; + } + + private (string verdict, double confidence) DetermineVerdict( + ImmutableArray sections, + ExpectedDiff expected) + { + var textSection = sections.FirstOrDefault(s => s.Name == ".text"); + + if (textSection is null) + return ("unknown", 0.5); + + if (textSection.Status == "modified") + { + // .text changed -> likely patched + var otherChanges = sections.Count(s => s.Status == "modified" && s.Name != ".text"); + var confidence = otherChanges > 2 ? 0.7 : 0.95; // Too many changes = less certain + return ("patched", confidence); + } + + if (textSection.Status == "identical") + { + return ("vanilla", 0.9); + } + + return ("unknown", 0.5); + } +} +``` + +### CLI Commands + +``` +golden-pairs + +Commands: + mirror Fetch artifacts for a golden pair + diff Run diff analysis on a golden pair + validate Validate all golden pairs in corpus + list List all available golden pairs + +Examples: + golden-pairs mirror CVE-2022-0847 + golden-pairs diff CVE-2022-0847 --output json + golden-pairs validate --fail-fast +``` + +### CI Integration + +```yaml +# .gitea/workflows/golden-pairs-validation.yml +name: Golden Pairs Validation + +on: + push: + paths: + - 'datasets/golden-pairs/**' + - 'src/Tools/GoldenPairs/**' + schedule: + - cron: '0 0 * * 0' # Weekly + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-dotnet@v4 + - run: dotnet build src/Tools/GoldenPairs/ + - run: dotnet run --project src/Tools/GoldenPairs/ -- validate --output trx + - uses: dorny/test-reporter@v1 + with: + name: Golden Pairs + path: 'golden-pairs.trx' + reporter: dotnet-trx +``` + +## Determinism Requirements + +1. **Download order**: Single-threaded to ensure reproducibility +2. **Hash computation**: Identical algorithm as Batch 001 +3. **Timestamps**: From injected `TimeProvider` +4. **Report ordering**: Sections sorted by name + +## Test Cases + +| Test | Description | Expected | +|------|-------------|----------| +| `Mirror_ValidPackage_Downloads` | Download existing package | Success, hash matches | +| `Mirror_MissingPackage_Fails` | Download non-existent package | Failure with error message | +| `Mirror_HashMismatch_Fails` | Download with wrong hash | Failure, hash mismatch reported | +| `Diff_ModifiedText_ReturnsPatched` | Pair with .text changed | Verdict: patched | +| `Diff_IdenticalAll_ReturnsVanilla` | Pair with no changes | Verdict: vanilla | +| `Validate_MatchesExpected_Passes` | Diff matches expectedDiff | IsValid: true | +| `Validate_WrongVerdict_Fails` | Diff disagrees with expected | IsValid: false, error listed | + +## Execution Log + +| Date (UTC) | Update | Owner | +|------------|--------|-------| +| 2026-01-13 | Sprint created from advisory analysis. | Project Mgmt | + +## Decisions & Risks + +- **APPROVED**: Support apt:// sources first; add RPM later. +- **APPROVED**: Cache downloaded packages locally to avoid re-fetch. +- **RISK**: Apt repository structure may vary; handle exceptions gracefully. +- **RISK**: Some packages may be removed from mirrors; document fallbacks. + +## Next Checkpoints + +- Task 1-3 complete -> Mirror service operational +- Task 4-6 complete -> Diff pipeline operational +- Task 7-9 complete -> CLI usable +- Task 10 complete -> Sprint can be marked DONE diff --git a/docs/implplan/SPRINT_20260113_004_003_TOOLS_pilot_corpus.md b/docs/implplan/SPRINT_20260113_004_003_TOOLS_pilot_corpus.md new file mode 100644 index 000000000..b399a4cc6 --- /dev/null +++ b/docs/implplan/SPRINT_20260113_004_003_TOOLS_pilot_corpus.md @@ -0,0 +1,259 @@ +# Sprint 20260113_004_003_TOOLS - Pilot CVE Corpus (3 CVEs) + +## Topic & Scope + +- Populate pilot corpus with 3 CVE golden pairs +- CVE-2022-0847 (Dirty Pipe): Linux kernel +- CVE-2021-3156 (Baron Samedit): sudo userland +- CVE-2021-34527 (PrintNightmare): Windows PE (conditional) +- Document each pair with advisories and patch info +- **Working directory:** `datasets/golden-pairs/` + +## Dependencies & Concurrency + +- **Depends on:** Sprint 004_001 (Data Model) +- **Depends on:** Sprint 004_002 (Pipeline) +- **Depends on:** Batch 001 Sprint 001 (ELF Section Hashes) for validation +- Final sprint in batch + +## Documentation Prerequisites + +- Sprint 004_001 data models +- Sprint 004_002 pipeline services +- Vulnerability details for each CVE +- Package sources for target distros + +## Delivery Tracker + +| # | Task ID | Status | Key dependency / next step | Owners | Task Definition | +|---|---------|--------|---------------------------|--------|-----------------| +| 1 | GP-CORPUS-DIRTYPIPE-META-0001 | TODO | None | Guild - Tools | Create `CVE-2022-0847/metadata.json` with full golden pair metadata. Identify Ubuntu 22.04 kernel package versions. | +| 2 | GP-CORPUS-DIRTYPIPE-FETCH-0001 | TODO | Depends on META, Sprint 002 | Guild - Tools | Fetch vmlinux binaries for pre-patch (5.16.11) and post-patch (5.16.12) versions using mirror service. | +| 3 | GP-CORPUS-DIRTYPIPE-DIFF-0001 | TODO | Depends on FETCH | Guild - Tools | Run diff pipeline, validate .text section change, verify verdict matches expected. | +| 4 | GP-CORPUS-DIRTYPIPE-DOCS-0001 | TODO | Depends on all above | Guild - Tools | Document advisory links, patch commit, functions changed. Archive advisory PDFs. | +| 5 | GP-CORPUS-BARON-META-0001 | TODO | None | Guild - Tools | Create `CVE-2021-3156/metadata.json`. Identify Debian 11 sudo package versions. | +| 6 | GP-CORPUS-BARON-FETCH-0001 | TODO | Depends on META, Sprint 002 | Guild - Tools | Fetch sudo binaries for pre-patch and post-patch versions. | +| 7 | GP-CORPUS-BARON-DIFF-0001 | TODO | Depends on FETCH | Guild - Tools | Run diff pipeline, validate, verify verdict. | +| 8 | GP-CORPUS-BARON-DOCS-0001 | TODO | Depends on all above | Guild - Tools | Document advisory links, patch commit. | +| 9 | GP-CORPUS-PRINT-META-0001 | TODO (CONDITIONAL) | PE support ready | Guild - Tools | Create `CVE-2021-34527/metadata.json` if PE section hashing available. | +| 10 | GP-CORPUS-INDEX-0001 | TODO | Depends on all pairs | Guild - Tools | Create `index.json` corpus manifest listing all pairs with summary. | +| 11 | GP-CORPUS-README-0001 | TODO | Depends on INDEX | Guild - Tools | Create `README.md` with corpus documentation, usage instructions, extension guide. | +| 12 | GP-CORPUS-CI-0001 | TODO | Depends on all above | Guild - Tools | Add CI workflow to validate corpus on changes. Integrate with test reporting. | + +## Technical Specification + +### CVE-2022-0847 (Dirty Pipe) + +**Vulnerability:** Linux kernel pipe buffer flag handling allows privilege escalation. + +**Target:** +- Binary: `vmlinux` (or specific .ko module `fs/pipe.c`) +- Architecture: x86_64 +- Format: ELF + +**Package Sources (Ubuntu 22.04):** +- Pre-patch: `linux-image-5.16.11-generic` from `archive.ubuntu.com` +- Post-patch: `linux-image-5.16.12-generic` + +**Patch Info:** +- Commit: `9d2231c5d74e13b2a0546fee6737ee4446017903` +- Functions: `copy_page_to_iter_pipe`, `push_pipe` +- Files: `fs/pipe.c`, `lib/iov_iter.c` + +**Expected Diff:** +- `.text`: MODIFIED (vulnerability fix) +- `.rodata`: IDENTICAL or MODIFIED (string changes) +- Verdict: `patched` +- Confidence: >= 0.9 + +**Advisories:** +- USN-5317-1: https://ubuntu.com/security/notices/USN-5317-1 +- NVD: https://nvd.nist.gov/vuln/detail/CVE-2022-0847 + +### CVE-2021-3156 (Baron Samedit) + +**Vulnerability:** Heap-based buffer overflow in sudo sudoedit. + +**Target:** +- Binary: `/usr/bin/sudo` +- Architecture: x86_64 +- Format: ELF + +**Package Sources (Debian 11):** +- Pre-patch: `sudo_1.9.5p2-3` from `snapshot.debian.org` +- Post-patch: `sudo_1.9.5p2-3+deb11u1` + +**Patch Info:** +- Functions: `set_cmnd`, `sudoedit_setup` +- Files: `src/sudoers.c`, `src/sudoedit.c` + +**Expected Diff:** +- `.text`: MODIFIED +- Verdict: `patched` + +**Advisories:** +- DSA-4839-1: https://www.debian.org/security/2021/dsa-4839 +- NVD: https://nvd.nist.gov/vuln/detail/CVE-2021-3156 + +### CVE-2021-34527 (PrintNightmare) - CONDITIONAL + +**Vulnerability:** Windows Print Spooler remote code execution. + +**Target:** +- Binary: `spoolsv.dll` or `localspl.dll` +- Architecture: x64 +- Format: PE + +**Condition:** Only include if PE section hashing from Batch 001 is available. + +**Package Sources:** +- Microsoft Update Catalog KB5004945 +- Or: Extract from Windows ISO + +**Expected Diff:** +- `.text`: MODIFIED +- Verdict: `patched` + +### Metadata Template + +```json +{ + "cve": "CVE-2022-0847", + "name": "Dirty Pipe", + "description": "A flaw was found in the way the pipe buffer flag was handled in the Linux kernel. An unprivileged local user could exploit this flaw to overwrite data in arbitrary read-only files.", + "severity": "high", + "artifact": { + "name": "vmlinux", + "format": "elf", + "architecture": "x86_64", + "os": "linux" + }, + "original": { + "package": "linux-image-5.16.11-generic", + "version": "5.16.11", + "distro": "Ubuntu 22.04", + "source": "apt://archive.ubuntu.com/ubuntu/pool/main/l/linux/linux-image-5.16.11-generic_5.16.11-amd64.deb", + "sha256": "TODO_COMPUTE_AFTER_FETCH", + "buildId": "TODO_EXTRACT_AFTER_FETCH", + "hasDebugSymbols": false, + "pathInPackage": "/boot/vmlinux-5.16.11-generic" + }, + "patched": { + "package": "linux-image-5.16.12-generic", + "version": "5.16.12", + "distro": "Ubuntu 22.04", + "source": "apt://archive.ubuntu.com/ubuntu/pool/main/l/linux/linux-image-5.16.12-generic_5.16.12-amd64.deb", + "sha256": "TODO_COMPUTE_AFTER_FETCH", + "buildId": "TODO_EXTRACT_AFTER_FETCH", + "hasDebugSymbols": false, + "pathInPackage": "/boot/vmlinux-5.16.12-generic" + }, + "patch": { + "commit": "9d2231c5d74e13b2a0546fee6737ee4446017903", + "upstream": "https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=9d2231c5d74e13b2a0546fee6737ee4446017903", + "functionsChanged": ["copy_page_to_iter_pipe", "push_pipe"], + "filesChanged": ["fs/pipe.c", "lib/iov_iter.c"], + "summary": "Fix PIPE_BUF_FLAG_CAN_MERGE handling to prevent arbitrary file overwrites" + }, + "advisories": [ + { + "source": "ubuntu", + "id": "USN-5317-1", + "url": "https://ubuntu.com/security/notices/USN-5317-1" + }, + { + "source": "nvd", + "id": "CVE-2022-0847", + "url": "https://nvd.nist.gov/vuln/detail/CVE-2022-0847" + } + ], + "expectedDiff": { + "sectionsChanged": [".text"], + "sectionsIdentical": [".rodata", ".data", ".bss"], + "verdict": "patched", + "confidenceMin": 0.9 + }, + "createdAt": "2026-01-13T12:00:00Z", + "createdBy": "StellaOps Golden Pairs Tool v1.0.0" +} +``` + +### Corpus Index + +```json +{ + "version": "1.0.0", + "generatedAt": "2026-01-13T12:00:00Z", + "pairs": [ + { + "cve": "CVE-2022-0847", + "name": "Dirty Pipe", + "severity": "high", + "format": "elf", + "status": "validated", + "lastValidated": "2026-01-13T12:00:00Z" + }, + { + "cve": "CVE-2021-3156", + "name": "Baron Samedit", + "severity": "high", + "format": "elf", + "status": "validated", + "lastValidated": "2026-01-13T12:00:00Z" + } + ], + "summary": { + "total": 2, + "validated": 2, + "failed": 0 + } +} +``` + +## Validation Workflow + +```bash +# 1. Fetch artifacts +golden-pairs mirror CVE-2022-0847 +golden-pairs mirror CVE-2021-3156 + +# 2. Run diff analysis +golden-pairs diff CVE-2022-0847 --output json > CVE-2022-0847/diff-report.json +golden-pairs diff CVE-2021-3156 --output json > CVE-2021-3156/diff-report.json + +# 3. Validate all +golden-pairs validate --all +# Expected output: +# CVE-2022-0847: PASS (verdict=patched, confidence=0.95) +# CVE-2021-3156: PASS (verdict=patched, confidence=0.92) +# Summary: 2/2 passed +``` + +## Test Cases + +| Test | Description | Expected | +|------|-------------|----------| +| `DirtyPipe_Validate_Passes` | Full pipeline for CVE-2022-0847 | Verdict: patched, matches expected | +| `BaronSamedit_Validate_Passes` | Full pipeline for CVE-2021-3156 | Verdict: patched, matches expected | +| `Index_AllPairs_Listed` | Load index.json | All pairs enumerated | +| `CI_Workflow_Succeeds` | Run validation in CI | All tests pass | + +## Execution Log + +| Date (UTC) | Update | Owner | +|------------|--------|-------| +| 2026-01-13 | Sprint created from advisory analysis. | Project Mgmt | + +## Decisions & Risks + +- **APPROVED**: Start with ELF only; PrintNightmare conditional on PE support. +- **APPROVED**: Use Debian snapshot archive for reproducible sudo packages. +- **RISK**: Kernel binaries are very large; consider extracting specific .ko modules. +- **RISK**: Package removal from archives; cache locally after first fetch. + +## Next Checkpoints + +- Task 1-4 complete -> Dirty Pipe pair validated +- Task 5-8 complete -> Baron Samedit pair validated +- Task 10-12 complete -> Corpus published, CI integrated +- Sprint and Batch complete diff --git a/docs/modules/attestor/rekor-verification-design.md b/docs/modules/attestor/rekor-verification-design.md index 369641b25..be7df9acb 100644 --- a/docs/modules/attestor/rekor-verification-design.md +++ b/docs/modules/attestor/rekor-verification-design.md @@ -1,19 +1,20 @@ # Rekor Verification Technical Design **Document ID**: DOCS-ATTEST-REKOR-001 -**Version**: 1.0 -**Last Updated**: 2025-12-14 +**Version**: 2.0 +**Last Updated**: 2026-01-13 **Status**: Draft --- ## 1. OVERVIEW -This document provides the comprehensive technical design for Rekor transparency log verification in StellaOps. It covers three key capabilities: +This document provides the comprehensive technical design for Rekor transparency log verification in StellaOps. It covers four key capabilities: 1. **Merkle Proof Verification** - Cryptographic verification of inclusion proofs 2. **Durable Retry Queue** - Reliable submission with failure recovery 3. **Time Skew Validation** - Replay protection via timestamp validation +4. **Tile-Based Verification (v2)** - Support for Rekor v2 Sunlight format ### Related Sprints @@ -22,6 +23,7 @@ This document provides the comprehensive technical design for Rekor transparency | SPRINT_3000_0001_0001 | P0 | Merkle Proof Verification | | SPRINT_3000_0001_0002 | P1 | Rekor Retry Queue & Metrics | | SPRINT_3000_0001_0003 | P2 | Time Skew Validation | +| SPRINT_3000_0001_0004 | P1 | Rekor v2 Tile-Based Verification | --- @@ -405,6 +407,225 @@ public TimeSkewResult Validate(DateTimeOffset integratedTime, DateTimeOffset loc } ``` +### 3.4 Tile-Based Verification (Rekor v2) + +Rekor v2 introduces a tile-based log structure following the Sunlight/C2SP `tlog-tiles` specification. This enables offline-capable verification and more efficient proof computation. + +#### 3.4.1 Architecture Overview + +In tile-based logs, the Merkle tree is stored in fixed-size chunks (tiles) of 256 entries each: + +``` + Tile Structure (256 entries/tile) + ─────────────────────────────────────────────────────────── + Level 2 (root) + [Tile] + / \ + Level 1 (intermediate) + [Tile 0] [Tile 1] ... + / \ + Level 0 (leaves) + [Tile 0] [Tile 1] [Tile 2] [Tile 3] ... + + Each tile contains up to 256 hashes (32 bytes each = 8KB max) +``` + +#### 3.4.2 Log Version Configuration + +StellaOps supports automatic version detection and explicit version selection: + +```csharp +public enum RekorLogVersion +{ + Auto = 0, // Auto-detect based on endpoint availability + V1 = 1, // Traditional Trillian-based Rekor (API proofs) + V2 = 2 // Tile-based Sunlight format +} +``` + +**Version Selection Logic:** + +| Version | PreferTileProofs | Result | +|---------|------------------|--------| +| V2 | (any) | Always use tile proofs | +| V1 | (any) | Always use API proofs | +| Auto | true | Prefer tile proofs if available | +| Auto | false | Use API proofs (default) | + +#### 3.4.3 Checkpoint Format + +V2 checkpoints follow the `c2sp.org/tlog-tiles` format: + +``` +rekor.sigstore.dev - 2605736670972794746 + + + +- rekor.sigstore.dev +``` + +**Checkpoint Components:** +- **Line 1**: Origin identifier (log name + instance) +- **Line 2**: Tree size (number of leaves) +- **Line 3**: Root hash (base64-encoded SHA-256) +- **Blank line**: Separator +- **Signature lines**: One or more `- ` lines + +#### 3.4.4 Tile Path Calculation + +Tiles are fetched via URL paths following the scheme: + +``` +GET {tile_base_url}/tile/{level}/{index:03d}[.p/{partial_width}] + +Examples: +- /tile/0/000 # Level 0, tile 0 (entries 0-255) +- /tile/0/001 # Level 0, tile 1 (entries 256-511) +- /tile/1/000 # Level 1, tile 0 (intermediate hashes) +- /tile/0/042.p/128 # Partial tile with 128 entries +``` + +#### 3.4.5 Implementation Classes + +**IRekorTileClient Interface:** + +```csharp +public interface IRekorTileClient +{ + Task GetCheckpointAsync( + RekorBackend backend, + CancellationToken cancellationToken = default); + + Task GetTileAsync( + RekorBackend backend, + int level, + long index, + CancellationToken cancellationToken = default); + + Task GetEntryAsync( + RekorBackend backend, + long logIndex, + CancellationToken cancellationToken = default); + + Task ComputeInclusionProofAsync( + RekorBackend backend, + long logIndex, + long treeSize, + CancellationToken cancellationToken = default); +} +``` + +**RekorTileData Model:** + +```csharp +public sealed class RekorTileData +{ + public required int Level { get; init; } + public required long Index { get; init; } + public required int Width { get; init; } // Number of hashes (max 256) + public required byte[] Hashes { get; init; } // Width * 32 bytes + + public byte[] GetHash(int position) + { + if (position < 0 || position >= Width) + throw new ArgumentOutOfRangeException(nameof(position)); + + var result = new byte[32]; + Array.Copy(Hashes, position * 32, result, 0, 32); + return result; + } +} +``` + +#### 3.4.6 Proof Computation Algorithm + +Computing an inclusion proof from tiles: + +```python +def compute_inclusion_proof(log_index, tree_size, tile_client): + """Compute inclusion proof by fetching necessary tiles.""" + proof_path = [] + level = 0 + index = log_index + size = tree_size + + while size > 1: + tile_index = index // 256 + position_in_tile = index % 256 + + # Determine sibling position + if index % 2 == 1: + sibling_pos = position_in_tile - 1 + else: + sibling_pos = position_in_tile + 1 if position_in_tile + 1 < size else None + + if sibling_pos is not None: + tile = tile_client.get_tile(level, tile_index) + proof_path.append(tile.get_hash(sibling_pos)) + + index = index // 2 + size = (size + 1) // 2 + level += 1 + + return proof_path +``` + +#### 3.4.7 Configuration + +```yaml +attestor: + rekor: + primary: + url: https://rekor.sigstore.dev + # Version: Auto, V1, or V2 + version: Auto + # Custom tile base URL (optional, defaults to {url}/tile/) + tile_base_url: "" + # Log ID for multi-log environments (hex-encoded SHA-256) + log_id: "c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d" + # Prefer tile proofs when version is Auto + prefer_tile_proofs: false +``` + +**Environment Variables:** + +```bash +# Rekor v2 Configuration +REKOR_SERVER_URL=https://rekor.sigstore.dev +REKOR_VERSION=Auto # Auto, V1, or V2 +REKOR_TILE_BASE_URL= # Optional custom tile endpoint +REKOR_LOG_ID=c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d +REKOR_PREFER_TILE_PROOFS=false +``` + +#### 3.4.8 Offline Verification Benefits + +Tile-based verification enables true offline capability: + +1. **Pre-fetch tiles**: Download all necessary tiles during online phase +2. **Bundle checkpoint**: Include signed checkpoint with offline kit +3. **Local proof computation**: Compute proofs entirely from local tile data +4. **No API dependency**: Verification works without Rekor connectivity + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Offline Verification │ +├─────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ Checkpoint │────►│ Tile Cache │────►│ Proof │ │ +│ │ (signed) │ │ (local) │ │ Verifier │ │ +│ └─────────────┘ └──────────────┘ └──────────────┘ │ +│ │ +│ Advantages: │ +│ - No network round-trips for proof fetching │ +│ - Deterministic verification (same tiles = same proof) │ +│ - Caching efficiency (tiles are immutable) │ +│ - Air-gap compatible │ +│ │ +└─────────────────────────────────────────────────────────────┘ +``` + --- ## 4. DATA FLOW @@ -688,4 +909,7 @@ attestor: - [RFC 6962: Certificate Transparency](https://datatracker.ietf.org/doc/html/rfc6962) - [Sigstore Rekor](https://github.com/sigstore/rekor) - [Transparency.dev Checkpoint Format](https://github.com/transparency-dev/formats) +- [C2SP tlog-tiles Specification](https://c2sp.org/tlog-tiles) - Tile-based transparency log format +- [Sunlight CT Log](https://github.com/FiloSottile/sunlight) - Reference implementation for tile-based logs +- [Sigstore Rekor v2 Announcement](https://blog.sigstore.dev/) - Official Rekor v2 migration information - [Advisory: Rekor Integration Technical Reference](../../../product/advisories/14-Dec-2025%20-%20Rekor%20Integration%20Technical%20Reference.md) diff --git a/docs/modules/scanner/binary-diff-attestation.md b/docs/modules/scanner/binary-diff-attestation.md new file mode 100644 index 000000000..73b64f510 --- /dev/null +++ b/docs/modules/scanner/binary-diff-attestation.md @@ -0,0 +1,355 @@ +# Binary Diff Attestation + +## Overview + +Binary Diff Attestation enables verification of binary-level changes between container images, producing cryptographically signed evidence of what changed at the ELF/PE section level. This capability is essential for: + +- **Vendor backport detection**: Identify when a vendor has patched a binary without changing version numbers +- **Supply chain verification**: Prove that expected changes (and no unexpected changes) occurred between releases +- **VEX evidence generation**: Provide concrete evidence for "not_affected" or "fixed" vulnerability status claims +- **Audit trail**: Maintain verifiable records of binary modifications across deployments + +### Relationship to SBOM and VEX + +Binary diff attestations complement SBOM and VEX documents: + +| Artifact | Purpose | Granularity | +|----------|---------|-------------| +| SBOM | Inventory of components | Package/library level | +| VEX | Exploitability status | Vulnerability level | +| Binary Diff Attestation | Change evidence | Section/function level | + +The attestation provides the *evidence* that supports VEX claims. For example, a VEX statement claiming a CVE is "fixed" due to a vendor backport can reference the binary diff attestation showing the `.text` section hash changed. + +## Architecture + +### Component Diagram + +``` +┌──────────────────────────────────────────────────────────────────────────────┐ +│ Binary Diff Attestation Flow │ +├──────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ OCI │ │ Layer │ │ Binary │ │ Section │ │ +│ │ Registry │───▶│ Extraction │───▶│ Detection │───▶│ Hash │ │ +│ │ Client │ │ │ │ │ │ Extractor │ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ └──────┬──────┘ │ +│ │ │ +│ Base Image ─────────────────────────────────────┐ │ │ +│ Target Image ───────────────────────────────────┤ ▼ │ +│ │ ┌─────────────┐ │ +│ └─▶│ Diff │ │ +│ │ Computation │ │ +│ └──────┬──────┘ │ +│ │ │ +│ ▼ │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ DSSE │◀───│ Predicate │◀───│ Finding │◀───│ Verdict │ │ +│ │ Signer │ │ Builder │ │ Aggregation │ │ Classifier │ │ +│ └──────┬──────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌─────────────┐ ┌─────────────┐ │ +│ │ Rekor │ │ File │ │ +│ │ Submission │ │ Output │ │ +│ └─────────────┘ └─────────────┘ │ +│ │ +└──────────────────────────────────────────────────────────────────────────────┘ +``` + +### Key Components + +| Component | Location | Responsibility | +|-----------|----------|----------------| +| `ElfSectionHashExtractor` | `Scanner.Analyzers.Native` | Extract per-section SHA-256 hashes from ELF binaries | +| `BinaryDiffService` | `Cli.Services` | Orchestrate diff computation between two images | +| `BinaryDiffPredicateBuilder` | `Attestor.StandardPredicates` | Construct BinaryDiffV1 in-toto predicates | +| `BinaryDiffDsseSigner` | `Attestor.StandardPredicates` | Sign predicates with DSSE envelopes | + +### Data Flow + +1. **Image Resolution**: Resolve base and target image references to manifest digests +2. **Layer Extraction**: Download and extract layers from both images +3. **Binary Identification**: Identify ELF binaries in both filesystems +4. **Section Hash Computation**: Compute SHA-256 for each target section in each binary +5. **Diff Computation**: Compare section hashes between base and target +6. **Verdict Classification**: Classify changes as patched/vanilla/unknown +7. **Predicate Construction**: Build BinaryDiffV1 predicate with findings +8. **DSSE Signing**: Sign predicate and optionally submit to Rekor + +## ELF Section Hashing + +### Target Sections + +The following ELF sections are analyzed for hash computation: + +| Section | Purpose | Backport Relevance | +|---------|---------|-------------------| +| `.text` | Executable code | **High** - Patched functions modify this section | +| `.rodata` | Read-only data (strings, constants) | Medium - String constants may change with patches | +| `.data` | Initialized global/static variables | Low - Rarely changes for security patches | +| `.symtab` | Symbol table (function names, addresses) | **High** - Function signature changes | +| `.dynsym` | Dynamic symbols (exports) | **High** - Exported API changes | + +### Hash Algorithm + +**Primary**: SHA-256 +- Industry standard, widely supported +- Collision-resistant for security applications + +**Optional**: BLAKE3-256 +- Faster computation for large binaries +- Enabled via configuration + +### Hash Computation + +``` +For each ELF binary: + 1. Parse ELF header + 2. Locate section headers + 3. For each target section: + a. Read section contents + b. Compute SHA-256(contents) + c. Store: {name, offset, size, sha256} + 4. Sort sections by name (lexicographic) + 5. Return ElfSectionHashSet +``` + +### Determinism Guarantees + +All operations produce deterministic output: + +| Aspect | Guarantee | +|--------|-----------| +| Section ordering | Sorted lexicographically by name | +| Hash format | Lowercase hexadecimal, no prefix | +| Timestamps | From injected `TimeProvider` | +| JSON serialization | RFC 8785 canonical JSON | + +## BinaryDiffV1 Predicate + +### Schema Overview + +The `BinaryDiffV1` predicate follows in-toto attestation format: + +```json +{ + "_type": "https://in-toto.io/Statement/v1", + "subject": [ + { + "name": "docker://repo/app@sha256:target...", + "digest": { "sha256": "target..." } + } + ], + "predicateType": "stellaops.binarydiff.v1", + "predicate": { + "inputs": { + "base": { "digest": "sha256:base..." }, + "target": { "digest": "sha256:target..." } + }, + "findings": [...], + "metadata": {...} + } +} +``` + +### Predicate Fields + +| Field | Type | Description | +|-------|------|-------------| +| `subjects` | array | Target image references with digests | +| `inputs.base` | object | Base image reference | +| `inputs.target` | object | Target image reference | +| `findings` | array | Per-binary diff findings | +| `metadata` | object | Tool version, timestamp, config | + +### Finding Structure + +Each finding represents a binary comparison: + +```json +{ + "path": "/usr/lib/libssl.so.3", + "changeType": "modified", + "binaryFormat": "elf", + "sectionDeltas": [ + { "section": ".text", "status": "modified" }, + { "section": ".rodata", "status": "identical" } + ], + "confidence": 0.95, + "verdict": "patched" +} +``` + +### Verdicts + +| Verdict | Meaning | Confidence Threshold | +|---------|---------|---------------------| +| `patched` | Binary shows evidence of security patch | >= 0.90 | +| `vanilla` | Binary matches upstream/unmodified | >= 0.95 | +| `unknown` | Cannot determine patch status | < 0.90 | +| `incompatible` | Cannot compare (different architecture, etc.) | N/A | + +## DSSE Attestation + +### Envelope Structure + +```json +{ + "payloadType": "stellaops.binarydiff.v1", + "payload": "", + "signatures": [ + { + "keyid": "...", + "sig": "" + } + ] +} +``` + +### Signature Algorithm + +- **Default**: Ed25519 +- **Alternative**: ECDSA P-256, RSA-PSS (via `ICryptoProviderRegistry`) +- **Keyless**: Sigstore Fulcio certificate chain + +### Rekor Submission + +When Rekor is enabled: + +1. DSSE envelope is submitted to Rekor transparency log +2. Inclusion proof is retrieved +3. Rekor metadata is stored in result + +```json +{ + "rekorLogIndex": 12345678, + "rekorEntryId": "abc123...", + "integratedTime": "2026-01-13T12:00:00Z" +} +``` + +### Verification + +Binary diff attestations can be verified with: + +```bash +# Using cosign +cosign verify-attestation \ + --type stellaops.binarydiff.v1 \ + --certificate-identity-regexp '.*' \ + --certificate-oidc-issuer-regexp '.*' \ + docker://repo/app:1.0.1 + +# Using stella CLI +stella verify attestation ./binarydiff.dsse.json \ + --type stellaops.binarydiff.v1 +``` + +## Integration Points + +### VEX Mapping + +Binary diff evidence can support VEX claims: + +```json +{ + "vulnerability": "CVE-2024-1234", + "status": "fixed", + "justification": "vulnerable_code_not_present", + "detail": "Vendor backport applied; evidence in binary diff attestation", + "evidence": { + "attestationRef": "sha256:dsse-envelope-hash...", + "finding": { + "path": "/usr/lib/libssl.so.3", + "verdict": "patched", + "confidence": 0.95 + } + } +} +``` + +### Policy Engine + +Policy rules can reference binary diff evidence: + +```rego +# Accept high-confidence patch verdicts as mitigation +allow contains decision if { + input.binaryDiff.findings[_].verdict == "patched" + input.binaryDiff.findings[_].confidence >= 0.90 + decision := { + "action": "accept", + "reason": "Binary diff shows patched code", + "evidence": input.binaryDiff.attestationRef + } +} +``` + +### SBOM Properties + +Section hashes appear in SBOM component properties: + +```json +{ + "type": "library", + "name": "libssl.so.3", + "properties": [ + {"name": "evidence:section:.text:sha256", "value": "abc123..."}, + {"name": "evidence:section:.rodata:sha256", "value": "def456..."}, + {"name": "evidence:extractor-version", "value": "1.0.0"} + ] +} +``` + +## Configuration + +### Scanner Options + +```yaml +scanner: + native: + sectionHashes: + enabled: true + algorithms: + - sha256 + - blake3 # optional + sections: + - .text + - .rodata + - .data + - .symtab + - .dynsym + maxSectionSize: 104857600 # 100MB limit +``` + +### CLI Options + +See [CLI Reference](../../API_CLI_REFERENCE.md#stella-scan-diff) for full option documentation. + +## Limitations and Future Work + +### Current Limitations + +1. **ELF only**: PE and Mach-O support planned for M2 +2. **Single platform**: Multi-platform diff requires multiple invocations +3. **No function-level analysis**: Section-level granularity only +4. **Confidence scoring**: Based on section changes, not semantic analysis + +### Roadmap + +| Milestone | Capability | +|-----------|------------| +| M2 | PE section analysis for Windows containers | +| M2 | Mach-O section analysis for macOS binaries | +| M3 | Vendor backport corpus with curated test fixtures | +| M3 | Function-level diff using DWARF debug info | +| M4 | ML-based verdict classification | + +## References + +- [BinaryDiffV1 JSON Schema](../../schemas/binarydiff-v1.schema.json) +- [in-toto Attestation Specification](https://github.com/in-toto/attestation) +- [DSSE Envelope Specification](https://github.com/secure-systems-lab/dsse) +- [ELF Specification](https://refspecs.linuxfoundation.org/elf/elf.pdf) diff --git a/docs/operations/rekor-policy.md b/docs/operations/rekor-policy.md index 154e1adb3..19bfbac03 100644 --- a/docs/operations/rekor-policy.md +++ b/docs/operations/rekor-policy.md @@ -184,6 +184,18 @@ attestor: # Rekor server URL (default: public Sigstore Rekor) serverUrl: "https://rekor.sigstore.dev" + # Log version: Auto, V1, or V2 (V2 uses tile-based Sunlight format) + version: Auto + + # Log ID for multi-log environments (hex-encoded SHA-256) + logId: "c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d" + + # Tile base URL for V2 (optional, defaults to {serverUrl}/tile/) + tileBaseUrl: "" + + # Prefer tile proofs when version is Auto + preferTileProofs: false + # Submission tier: graph-only | with-edges tier: graph-only @@ -225,7 +237,9 @@ attestor: ## Related Documentation +- [Rekor Verification Technical Design](../modules/attestor/rekor-verification-design.md) - Full technical design including v2 tile support - [Attestor AGENTS.md](../../src/Attestor/StellaOps.Attestor/AGENTS.md) - [Scanner Score Proofs API](../api/scanner-score-proofs-api.md) - [Offline Kit Specification](../OFFLINE_KIT.md) - [Sigstore Rekor Documentation](https://docs.sigstore.dev/rekor/overview/) +- [C2SP tlog-tiles Specification](https://c2sp.org/tlog-tiles) - Tile-based transparency log format (v2) diff --git a/docs/schemas/binarydiff-v1.schema.json b/docs/schemas/binarydiff-v1.schema.json new file mode 100644 index 000000000..cb3f01930 --- /dev/null +++ b/docs/schemas/binarydiff-v1.schema.json @@ -0,0 +1,344 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://stellaops.io/schemas/binarydiff-v1.schema.json", + "title": "BinaryDiffV1", + "description": "In-toto predicate schema for binary-level diff attestations between container images", + "type": "object", + "required": ["predicateType", "inputs", "findings", "metadata"], + "additionalProperties": false, + "properties": { + "predicateType": { + "const": "stellaops.binarydiff.v1", + "description": "Predicate type identifier" + }, + "inputs": { + "$ref": "#/$defs/BinaryDiffInputs", + "description": "Base and target image references" + }, + "findings": { + "type": "array", + "items": { + "$ref": "#/$defs/BinaryDiffFinding" + }, + "description": "Per-binary diff findings" + }, + "metadata": { + "$ref": "#/$defs/BinaryDiffMetadata", + "description": "Analysis metadata" + } + }, + "$defs": { + "BinaryDiffInputs": { + "type": "object", + "required": ["base", "target"], + "additionalProperties": false, + "properties": { + "base": { + "$ref": "#/$defs/ImageReference", + "description": "Base image reference" + }, + "target": { + "$ref": "#/$defs/ImageReference", + "description": "Target image reference" + } + } + }, + "ImageReference": { + "type": "object", + "required": ["digest"], + "additionalProperties": false, + "properties": { + "reference": { + "type": "string", + "description": "Full image reference (e.g., docker://repo/image:tag)", + "examples": ["docker://registry.example.com/app:1.0.0"] + }, + "digest": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$", + "description": "Image digest in sha256:hex format" + }, + "manifestDigest": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$", + "description": "Platform-specific manifest digest" + }, + "platform": { + "$ref": "#/$defs/Platform" + } + } + }, + "Platform": { + "type": "object", + "required": ["os", "architecture"], + "additionalProperties": false, + "properties": { + "os": { + "type": "string", + "description": "Operating system (e.g., linux, windows)", + "examples": ["linux", "windows"] + }, + "architecture": { + "type": "string", + "description": "CPU architecture (e.g., amd64, arm64)", + "examples": ["amd64", "arm64", "386"] + }, + "variant": { + "type": "string", + "description": "Architecture variant (e.g., v8 for arm64)", + "examples": ["v7", "v8"] + } + } + }, + "BinaryDiffFinding": { + "type": "object", + "required": ["path", "changeType", "binaryFormat"], + "additionalProperties": false, + "properties": { + "path": { + "type": "string", + "description": "File path within the container filesystem", + "examples": ["/usr/lib/libssl.so.3", "/usr/bin/openssl"] + }, + "changeType": { + "type": "string", + "enum": ["added", "removed", "modified", "unchanged"], + "description": "Type of change detected" + }, + "binaryFormat": { + "type": "string", + "enum": ["elf", "pe", "macho", "unknown"], + "description": "Binary format detected" + }, + "layerDigest": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$", + "description": "Layer digest that introduced this file/change" + }, + "baseHashes": { + "$ref": "#/$defs/SectionHashSet", + "description": "Section hashes from base image binary" + }, + "targetHashes": { + "$ref": "#/$defs/SectionHashSet", + "description": "Section hashes from target image binary" + }, + "sectionDeltas": { + "type": "array", + "items": { + "$ref": "#/$defs/SectionDelta" + }, + "description": "Per-section comparison results" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score for verdict (0.0-1.0)" + }, + "verdict": { + "type": "string", + "enum": ["patched", "vanilla", "unknown", "incompatible"], + "description": "Classification of the binary change" + } + } + }, + "SectionHashSet": { + "type": "object", + "additionalProperties": false, + "properties": { + "buildId": { + "type": "string", + "pattern": "^[a-f0-9]+$", + "description": "GNU Build-ID from .note.gnu.build-id section" + }, + "fileHash": { + "type": "string", + "pattern": "^[a-f0-9]{64}$", + "description": "SHA-256 hash of the entire file" + }, + "extractorVersion": { + "type": "string", + "description": "Version of the section hash extractor" + }, + "sections": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/SectionInfo" + }, + "description": "Map of section name to section info" + } + } + }, + "SectionInfo": { + "type": "object", + "required": ["sha256", "size"], + "additionalProperties": false, + "properties": { + "sha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$", + "description": "SHA-256 hash of section contents" + }, + "blake3": { + "type": "string", + "pattern": "^[a-f0-9]{64}$", + "description": "Optional BLAKE3-256 hash of section contents" + }, + "size": { + "type": "integer", + "minimum": 0, + "description": "Section size in bytes" + }, + "offset": { + "type": "integer", + "minimum": 0, + "description": "Section offset in file" + }, + "type": { + "type": "string", + "description": "ELF section type (e.g., SHT_PROGBITS)" + }, + "flags": { + "type": "string", + "description": "ELF section flags (e.g., SHF_ALLOC | SHF_EXECINSTR)" + } + } + }, + "SectionDelta": { + "type": "object", + "required": ["section", "status"], + "additionalProperties": false, + "properties": { + "section": { + "type": "string", + "description": "Section name (e.g., .text, .rodata)", + "examples": [".text", ".rodata", ".data", ".symtab", ".dynsym"] + }, + "status": { + "type": "string", + "enum": ["identical", "modified", "added", "removed"], + "description": "Section comparison status" + }, + "baseSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$", + "description": "SHA-256 of section in base binary" + }, + "targetSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$", + "description": "SHA-256 of section in target binary" + }, + "sizeDelta": { + "type": "integer", + "description": "Size difference (target - base) in bytes" + } + } + }, + "BinaryDiffMetadata": { + "type": "object", + "required": ["toolVersion", "analysisTimestamp"], + "additionalProperties": false, + "properties": { + "toolVersion": { + "type": "string", + "description": "Version of the binary diff tool", + "examples": ["1.0.0", "2026.01.0"] + }, + "analysisTimestamp": { + "type": "string", + "format": "date-time", + "description": "UTC timestamp of analysis (ISO-8601)" + }, + "configDigest": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$", + "description": "SHA-256 of analysis configuration for reproducibility" + }, + "totalBinaries": { + "type": "integer", + "minimum": 0, + "description": "Total number of binaries analyzed" + }, + "modifiedBinaries": { + "type": "integer", + "minimum": 0, + "description": "Number of binaries with modifications" + }, + "analyzedSections": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of section names analyzed", + "examples": [[".text", ".rodata", ".data", ".symtab", ".dynsym"]] + }, + "hashAlgorithms": { + "type": "array", + "items": { + "type": "string", + "enum": ["sha256", "blake3"] + }, + "description": "Hash algorithms used" + } + } + } + }, + "examples": [ + { + "predicateType": "stellaops.binarydiff.v1", + "inputs": { + "base": { + "reference": "docker://registry.example.com/app:1.0.0", + "digest": "sha256:abc123def456789012345678901234567890123456789012345678901234abcd", + "platform": { + "os": "linux", + "architecture": "amd64" + } + }, + "target": { + "reference": "docker://registry.example.com/app:1.0.1", + "digest": "sha256:def456abc789012345678901234567890123456789012345678901234567efgh", + "platform": { + "os": "linux", + "architecture": "amd64" + } + } + }, + "findings": [ + { + "path": "/usr/lib/libssl.so.3", + "changeType": "modified", + "binaryFormat": "elf", + "sectionDeltas": [ + { + "section": ".text", + "status": "modified", + "baseSha256": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "targetSha256": "fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321", + "sizeDelta": 256 + }, + { + "section": ".rodata", + "status": "identical", + "baseSha256": "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", + "targetSha256": "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", + "sizeDelta": 0 + } + ], + "confidence": 0.95, + "verdict": "patched" + } + ], + "metadata": { + "toolVersion": "1.0.0", + "analysisTimestamp": "2026-01-13T12:00:00Z", + "totalBinaries": 156, + "modifiedBinaries": 3, + "analyzedSections": [".text", ".rodata", ".data", ".symtab", ".dynsym"], + "hashAlgorithms": ["sha256"] + } + } + ] +} diff --git a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Core.Tests/Rekor/RekorReceiptTests.cs b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Core.Tests/Rekor/RekorReceiptTests.cs new file mode 100644 index 000000000..3efb51f10 --- /dev/null +++ b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Core.Tests/Rekor/RekorReceiptTests.cs @@ -0,0 +1,262 @@ +// ----------------------------------------------------------------------------- +// RekorReceiptTests.cs +// Description: Unit tests for standardized Rekor receipt schema. +// ----------------------------------------------------------------------------- + +using System.Text.Json; +using FluentAssertions; +using StellaOps.Attestor.Core.Rekor; +using Xunit; + +namespace StellaOps.Attestor.Core.Tests.Rekor; + +[Trait("Category", "Unit")] +[Trait("Category", "Rekor")] +public sealed class RekorReceiptTests +{ + [Fact] + public void RekorReceipt_SerializesToValidJson() + { + // Arrange + var receipt = CreateValidReceipt(); + + // Act + var json = JsonSerializer.Serialize(receipt, new JsonSerializerOptions { WriteIndented = true }); + + // Assert + json.Should().NotBeNullOrEmpty(); + json.Should().Contain("\"schemaVersion\":"); + json.Should().Contain("\"uuid\":"); + json.Should().Contain("\"logIndex\":"); + json.Should().Contain("\"checkpoint\":"); + json.Should().Contain("\"inclusionProof\":"); + } + + [Fact] + public void RekorReceipt_RoundtripsCorrectly() + { + // Arrange + var original = CreateValidReceipt(); + + // Act + var json = JsonSerializer.Serialize(original); + var deserialized = JsonSerializer.Deserialize(json); + + // Assert + deserialized.Should().NotBeNull(); + deserialized!.Uuid.Should().Be(original.Uuid); + deserialized.LogIndex.Should().Be(original.LogIndex); + deserialized.LogId.Should().Be(original.LogId); + deserialized.IntegratedTime.Should().Be(original.IntegratedTime); + deserialized.EntryKind.Should().Be(original.EntryKind); + deserialized.EntryBodyHash.Should().Be(original.EntryBodyHash); + deserialized.Checkpoint.Origin.Should().Be(original.Checkpoint.Origin); + deserialized.InclusionProof.LeafHash.Should().Be(original.InclusionProof.LeafHash); + } + + [Fact] + public void RekorReceipt_IntegratedTimeUtc_ConvertsCorrectly() + { + // Arrange + var unixTime = 1704067200L; // 2024-01-01 00:00:00 UTC + var receipt = CreateValidReceipt() with { IntegratedTime = unixTime }; + + // Act + var utc = receipt.IntegratedTimeUtc; + + // Assert + utc.Year.Should().Be(2024); + utc.Month.Should().Be(1); + utc.Day.Should().Be(1); + utc.Hour.Should().Be(0); + utc.Minute.Should().Be(0); + utc.Second.Should().Be(0); + } + + [Fact] + public void RekorReceipt_EntryUrl_FormsCorrectly() + { + // Arrange + var receipt = CreateValidReceipt() with + { + LogUrl = "https://rekor.sigstore.dev", + Uuid = "abc123def456" + }; + + // Act + var entryUrl = receipt.EntryUrl; + + // Assert + entryUrl.Should().Be("https://rekor.sigstore.dev/api/v1/log/entries/abc123def456"); + } + + [Fact] + public void RekorReceipt_EntryUrl_HandlesTrailingSlash() + { + // Arrange + var receipt = CreateValidReceipt() with + { + LogUrl = "https://rekor.sigstore.dev/", + Uuid = "abc123" + }; + + // Act + var entryUrl = receipt.EntryUrl; + + // Assert + entryUrl.Should().Be("https://rekor.sigstore.dev/api/v1/log/entries/abc123"); + } + + [Fact] + public void RekorCheckpointV2_TimestampUtc_ConvertsCorrectly() + { + // Arrange + var checkpoint = new RekorCheckpointV2 + { + Origin = "test-origin", + Size = 1000, + RootHash = "abc123", + Timestamp = 1704067200L, + Signature = "sig123" + }; + + // Act + var utc = checkpoint.TimestampUtc; + + // Assert + utc.Year.Should().Be(2024); + } + + [Fact] + public void RekorInclusionProofV2_SerializesHashesCorrectly() + { + // Arrange + var proof = new RekorInclusionProofV2 + { + LogIndex = 1000, + TreeSize = 2000, + RootHash = "root123", + LeafHash = "leaf456", + Hashes = ["hash1", "hash2", "hash3"] + }; + + // Act + var json = JsonSerializer.Serialize(proof); + var deserialized = JsonSerializer.Deserialize(json); + + // Assert + deserialized.Should().NotBeNull(); + deserialized!.Hashes.Should().HaveCount(3); + deserialized.Hashes.Should().ContainInOrder("hash1", "hash2", "hash3"); + } + + [Fact] + public void RekorReceiptVerificationResult_WhenValid_IsHealthy() + { + // Arrange + var result = new RekorReceiptVerificationResult + { + IsValid = true, + CheckpointSignatureValid = true, + InclusionProofValid = true, + EntryHashValid = true, + TimeSkewAcceptable = true, + VerifiedAt = DateTimeOffset.UtcNow + }; + + // Assert + result.IsValid.Should().BeTrue(); + result.Errors.Should().BeEmpty(); + } + + [Fact] + public void RekorReceiptVerificationResult_WhenInvalid_ContainsErrors() + { + // Arrange + var result = new RekorReceiptVerificationResult + { + IsValid = false, + CheckpointSignatureValid = false, + InclusionProofValid = true, + EntryHashValid = true, + TimeSkewAcceptable = true, + Errors = ["Checkpoint signature verification failed"], + VerifiedAt = DateTimeOffset.UtcNow + }; + + // Assert + result.IsValid.Should().BeFalse(); + result.Errors.Should().Contain("Checkpoint signature verification failed"); + } + + [Fact] + public void RekorReceiptVerificationOptions_HasSensibleDefaults() + { + // Arrange & Act + var options = new RekorReceiptVerificationOptions(); + + // Assert + options.MaxClockSkewSeconds.Should().Be(300); // 5 minutes + options.AllowOfflineVerification.Should().BeTrue(); + options.MaxOfflineCheckpointAgeHours.Should().Be(24); + options.RequireCheckpointSignature.Should().BeTrue(); + } + + [Fact] + public void RekorReceipt_IncludesOptionalPolicyFields() + { + // Arrange + var receipt = CreateValidReceipt() with + { + PolicyHash = "sha256:policy123", + GraphRevision = "rev-456", + IdempotencyKey = "idem-789" + }; + + // Act + var json = JsonSerializer.Serialize(receipt); + var deserialized = JsonSerializer.Deserialize(json); + + // Assert + deserialized!.PolicyHash.Should().Be("sha256:policy123"); + deserialized.GraphRevision.Should().Be("rev-456"); + deserialized.IdempotencyKey.Should().Be("idem-789"); + } + + [Fact] + public void RekorReceipt_SchemaVersion_DefaultsTo1_0_0() + { + // Arrange + var receipt = CreateValidReceipt(); + + // Assert + receipt.SchemaVersion.Should().Be("1.0.0"); + } + + private static RekorReceipt CreateValidReceipt() => new() + { + Uuid = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + LogIndex = 12345, + LogId = "rekor.sigstore.dev - 2605736670972794746", + LogUrl = "https://rekor.sigstore.dev", + IntegratedTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), + EntryKind = "dsse", + EntryBodyHash = "sha256:abcdef123456", + Checkpoint = new RekorCheckpointV2 + { + Origin = "rekor.sigstore.dev - 2605736670972794746", + Size = 50000, + RootHash = "abc123def456", + Timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), + Signature = "MEUCIQDtest..." + }, + InclusionProof = new RekorInclusionProofV2 + { + LogIndex = 12345, + TreeSize = 50000, + RootHash = "abc123def456", + LeafHash = "leaf789xyz", + Hashes = ["hash1", "hash2", "hash3"] + } + }; +} diff --git a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Core.Tests/Transparency/TransparencyStatusProviderTests.cs b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Core.Tests/Transparency/TransparencyStatusProviderTests.cs new file mode 100644 index 000000000..5be25b6ef --- /dev/null +++ b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Core.Tests/Transparency/TransparencyStatusProviderTests.cs @@ -0,0 +1,249 @@ +// ----------------------------------------------------------------------------- +// TransparencyStatusProviderTests.cs +// Description: Unit tests for transparency status provider. +// ----------------------------------------------------------------------------- + +using FluentAssertions; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using StellaOps.Attestor.Core.Transparency; +using Xunit; +using OptionsFactory = Microsoft.Extensions.Options.Options; + +namespace StellaOps.Attestor.Core.Tests.Transparency; + +[Trait("Category", "Unit")] +[Trait("Category", "Transparency")] +public sealed class TransparencyStatusProviderTests : IDisposable +{ + private readonly FakeTimeProvider _timeProvider; + private readonly TransparencyStatusProvider _provider; + + public TransparencyStatusProviderTests() + { + _timeProvider = new FakeTimeProvider(DateTimeOffset.UtcNow); + var options = OptionsFactory.Create(new TransparencyStatusOptions + { + MaxCheckpointAgeHours = 24, + CriticalCheckpointAgeHours = 72, + RekorBackendUrl = "https://rekor.sigstore.dev" + }); + + _provider = new TransparencyStatusProvider( + NullLogger.Instance, + options, + _timeProvider); + } + + [Fact] + public async Task GetStatusAsync_WhenNeverSynced_ReturnsUnknown() + { + // Act + var status = await _provider.GetStatusAsync(); + + // Assert + status.Status.Should().Be(TransparencyStatusLevel.Unknown); + status.LastSyncAt.Should().BeNull(); + status.Message.Should().Contain("never synced"); + } + + [Fact] + public async Task GetStatusAsync_WhenRecentlySync_ReturnsHealthy() + { + // Arrange + var syncTime = _timeProvider.GetUtcNow().AddHours(-1); + _provider.RecordSync(syncTime, 12345); + + // Act + var status = await _provider.GetStatusAsync(); + + // Assert + status.Status.Should().Be(TransparencyStatusLevel.Healthy); + status.LastSyncAt.Should().Be(syncTime); + status.LastSyncAgeHours.Should().BeApproximately(1, 0.1); + status.LastCheckpointTreeSize.Should().Be(12345); + } + + [Fact] + public async Task GetStatusAsync_WhenSyncStale_ReturnsDegraded() + { + // Arrange - sync 30 hours ago (exceeds 24h threshold) + var syncTime = _timeProvider.GetUtcNow().AddHours(-30); + _provider.RecordSync(syncTime, 12345); + + // Act + var status = await _provider.GetStatusAsync(); + + // Assert + status.Status.Should().Be(TransparencyStatusLevel.Degraded); + status.LastSyncAgeHours.Should().BeApproximately(30, 0.1); + status.Message.Should().Contain("stale"); + } + + [Fact] + public async Task GetStatusAsync_WhenSyncCriticallyStale_ReturnsUnhealthy() + { + // Arrange - sync 80 hours ago (exceeds 72h critical threshold) + var syncTime = _timeProvider.GetUtcNow().AddHours(-80); + _provider.RecordSync(syncTime, 12345); + + // Act + var status = await _provider.GetStatusAsync(); + + // Assert + status.Status.Should().Be(TransparencyStatusLevel.Unhealthy); + status.Message.Should().Contain("critically stale"); + } + + [Fact] + public async Task GetStatusAsync_WhenOfflineModeWithFreshCheckpoint_ReturnsOffline() + { + // Arrange - create provider without backend URL (offline mode) + var offlineOptions = OptionsFactory.Create(new TransparencyStatusOptions + { + MaxCheckpointAgeHours = 24, + RekorBackendUrl = null // Offline mode + }); + + using var offlineProvider = new TransparencyStatusProvider( + NullLogger.Instance, + offlineOptions, + _timeProvider); + + var syncTime = _timeProvider.GetUtcNow().AddHours(-1); + offlineProvider.RecordSync(syncTime, 12345); + + // Act + var status = await offlineProvider.GetStatusAsync(); + + // Assert + status.Status.Should().Be(TransparencyStatusLevel.Offline); + status.OfflineMode.Should().BeTrue(); + status.Message.Should().Contain("offline mode"); + } + + [Fact] + public void RecordSubmission_TracksMetrics() + { + // Arrange + var latency1 = TimeSpan.FromMilliseconds(100); + var latency2 = TimeSpan.FromMilliseconds(200); + var latency3 = TimeSpan.FromMilliseconds(150); + + // Act + _provider.RecordSubmission(true, latency1); + _provider.RecordSubmission(true, latency2); + _provider.RecordSubmission(false, latency3); + + // Assert + var status = _provider.GetStatusAsync().Result; + status.Metrics.Should().NotBeNull(); + status.Metrics!.SubmissionsLastHour.Should().Be(3); + status.Metrics.SuccessfulSubmissionsLastHour.Should().Be(2); + status.Metrics.FailedSubmissionsLastHour.Should().Be(1); + status.Metrics.AvgSubmissionLatencyMs.Should().Be(150); // (100+200)/2 = 150 (only successful) + } + + [Fact] + public void RecordVerification_TracksMetrics() + { + // Act + _provider.RecordVerification(true, false); + _provider.RecordVerification(true, true); + _provider.RecordVerification(false, false); + + // Assert + var status = _provider.GetStatusAsync().Result; + status.Metrics.Should().NotBeNull(); + status.Metrics!.VerificationsLastHour.Should().Be(3); + status.Metrics.SuccessfulVerificationsLastHour.Should().Be(2); + status.Metrics.OfflineVerificationsLastHour.Should().Be(1); + } + + [Fact] + public async Task GetStatusAsync_ReportsQueueDepths() + { + // Arrange + _provider.UpdateQueueDepths(submissionQueue: 5, deadLetterQueue: 2); + + // Act + var status = await _provider.GetStatusAsync(); + + // Assert + status.SubmissionQueueDepth.Should().Be(5); + status.DeadLetterQueueDepth.Should().Be(2); + } + + [Fact] + public async Task GetStatusAsync_ReportsConfiguration() + { + // Act + var status = await _provider.GetStatusAsync(); + + // Assert + status.MaxCheckpointAgeHours.Should().Be(24); + status.RekorBackend.Should().Be("https://rekor.sigstore.dev"); + status.EnforcementEnabled.Should().BeFalse(); // default + } + + [Fact] + public async Task IsCheckpointFresh_WhenWithinThreshold_ReturnsTrue() + { + // Arrange + var syncTime = _timeProvider.GetUtcNow().AddHours(-12); + _provider.RecordSync(syncTime, 12345); + + // Act + var status = await _provider.GetStatusAsync(); + + // Assert + status.IsCheckpointFresh.Should().BeTrue(); + } + + [Fact] + public async Task IsCheckpointFresh_WhenExceedsThreshold_ReturnsFalse() + { + // Arrange + var syncTime = _timeProvider.GetUtcNow().AddHours(-30); + _provider.RecordSync(syncTime, 12345); + + // Act + var status = await _provider.GetStatusAsync(); + + // Assert + status.IsCheckpointFresh.Should().BeFalse(); + } + + [Fact] + public async Task IsHealthy_WhenHealthyOrDegraded_ReturnsTrue() + { + // Arrange - fresh sync (healthy) + var syncTime = _timeProvider.GetUtcNow().AddHours(-1); + _provider.RecordSync(syncTime, 12345); + + // Act + var status = await _provider.GetStatusAsync(); + + // Assert + status.IsHealthy.Should().BeTrue(); + } + + public void Dispose() + { + _provider.Dispose(); + } + + private sealed class FakeTimeProvider : TimeProvider + { + private DateTimeOffset _utcNow; + + public FakeTimeProvider(DateTimeOffset utcNow) + { + _utcNow = utcNow; + } + + public override DateTimeOffset GetUtcNow() => _utcNow; + + public void Advance(TimeSpan duration) => _utcNow = _utcNow.Add(duration); + } +} diff --git a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Core/Options/AttestorOptions.cs b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Core/Options/AttestorOptions.cs index 78ad01b56..cd0d57e01 100644 --- a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Core/Options/AttestorOptions.cs +++ b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Core/Options/AttestorOptions.cs @@ -121,6 +121,30 @@ public sealed class AttestorOptions public int PollIntervalMs { get; set; } = 250; public int MaxAttempts { get; set; } = 60; + + /// + /// Log version to use: Auto, V1, or V2. + /// V2 uses tile-based (Sunlight) log structure. + /// Default: Auto (backward compatible). + /// + public string Version { get; set; } = "Auto"; + + /// + /// Base URL for tile fetching in Rekor v2. + /// If not specified, defaults to {Url}/tile/. + /// + public string? TileBaseUrl { get; set; } + + /// + /// Log ID (SHA-256 of log's public key) for multi-log environments. + /// Production Rekor: c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d + /// + public string? LogId { get; set; } + + /// + /// When true and Version is Auto, prefer tile-based proofs over v1 proofs. + /// + public bool PreferTileProofs { get; set; } = false; } public sealed class RekorMirrorOptions : RekorBackendOptions diff --git a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Core/Rekor/IRekorTileClient.cs b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Core/Rekor/IRekorTileClient.cs new file mode 100644 index 000000000..28cff7695 --- /dev/null +++ b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Core/Rekor/IRekorTileClient.cs @@ -0,0 +1,208 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace StellaOps.Attestor.Core.Rekor; + +/// +/// Client for fetching proofs from Rekor v2 tile-based logs. +/// Tile-based logs store the Merkle tree in fixed-size chunks (tiles) +/// that can be fetched directly for offline-capable verification. +/// +public interface IRekorTileClient +{ + /// + /// Fetches the latest signed checkpoint from the tile log. + /// The checkpoint contains the current tree size and root hash. + /// + /// Rekor backend configuration + /// Cancellation token + /// The checkpoint response, or null if not available + Task GetCheckpointAsync( + RekorBackend backend, + CancellationToken cancellationToken = default); + + /// + /// Fetches a tile from the log. + /// Tiles are fixed-size chunks of the Merkle tree. + /// + /// Rekor backend configuration + /// The tree level (0 = leaves) + /// The tile index at this level + /// Cancellation token + /// The tile data, or null if not found + Task GetTileAsync( + RekorBackend backend, + int level, + long index, + CancellationToken cancellationToken = default); + + /// + /// Fetches an entry from the log by its index. + /// + /// Rekor backend configuration + /// The log index of the entry + /// Cancellation token + /// The entry data, or null if not found + Task GetEntryAsync( + RekorBackend backend, + long logIndex, + CancellationToken cancellationToken = default); + + /// + /// Computes an inclusion proof for an entry using tile data. + /// This fetches the necessary tiles and constructs the proof path. + /// + /// Rekor backend configuration + /// The log index of the entry + /// The tree size for the proof (from checkpoint) + /// Cancellation token + /// The computed proof, or null if tiles are unavailable + Task ComputeInclusionProofAsync( + RekorBackend backend, + long logIndex, + long treeSize, + CancellationToken cancellationToken = default); +} + +/// +/// Checkpoint from a Rekor v2 tile-based log. +/// +public sealed class RekorTileCheckpoint +{ + /// + /// The log origin identifier. + /// + public required string Origin { get; init; } + + /// + /// Current tree size (number of entries). + /// + public required long TreeSize { get; init; } + + /// + /// Root hash of the Merkle tree at this size. + /// + public required byte[] RootHash { get; init; } + + /// + /// Raw checkpoint note text for signature verification. + /// + public required string RawCheckpoint { get; init; } + + /// + /// Signatures on the checkpoint. + /// + public required IReadOnlyList Signatures { get; init; } +} + +/// +/// A signature on a Rekor checkpoint. +/// +public sealed class RekorCheckpointSignature +{ + /// + /// Key ID or hint for the signing key. + /// + public required string KeyHint { get; init; } + + /// + /// The signature bytes. + /// + public required byte[] Signature { get; init; } +} + +/// +/// Data from a Merkle tree tile. +/// +public sealed class RekorTileData +{ + /// + /// The level in the tree (0 = leaf level). + /// + public required int Level { get; init; } + + /// + /// The tile index at this level. + /// + public required long Index { get; init; } + + /// + /// The tile width (number of entries in this tile, may be partial). + /// + public required int Width { get; init; } + + /// + /// The hash data in this tile. + /// Each hash is 32 bytes (SHA-256). + /// + public required byte[] Hashes { get; init; } + + /// + /// Gets the hash at the given position within the tile. + /// + public byte[] GetHash(int position) + { + if (position < 0 || position >= Width) + { + throw new ArgumentOutOfRangeException(nameof(position)); + } + + var result = new byte[32]; + Array.Copy(Hashes, position * 32, result, 0, 32); + return result; + } +} + +/// +/// An entry from a Rekor tile-based log. +/// +public sealed class RekorTileEntry +{ + /// + /// The log index of this entry. + /// + public required long LogIndex { get; init; } + + /// + /// The entry data (typically the leaf hash input). + /// + public required byte[] Data { get; init; } + + /// + /// The integrated time when this entry was added. + /// + public DateTimeOffset? IntegratedTime { get; init; } +} + +/// +/// An inclusion proof computed from tile data. +/// +public sealed class RekorTileInclusionProof +{ + /// + /// The log index of the entry. + /// + public required long LogIndex { get; init; } + + /// + /// The tree size for this proof. + /// + public required long TreeSize { get; init; } + + /// + /// The leaf hash of the entry. + /// + public required byte[] LeafHash { get; init; } + + /// + /// The proof path (sibling hashes from leaf to root). + /// + public required IReadOnlyList Path { get; init; } + + /// + /// The expected root hash for verification. + /// + public required byte[] RootHash { get; init; } +} diff --git a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Core/Rekor/RekorBackend.cs b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Core/Rekor/RekorBackend.cs index f872a5b13..1e4f87ec9 100644 --- a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Core/Rekor/RekorBackend.cs +++ b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Core/Rekor/RekorBackend.cs @@ -2,15 +2,82 @@ using System; namespace StellaOps.Attestor.Core.Rekor; +/// +/// Specifies the Rekor log version/format to use. +/// +public enum RekorLogVersion +{ + /// + /// Automatically detect log version from server capabilities. + /// + Auto = 0, + + /// + /// Rekor v1 with Trillian-backed Merkle tree. + /// + V1 = 1, + + /// + /// Rekor v2 with tile-based (Sunlight) log structure. + /// Provides cheaper operation and simpler verification. + /// + V2 = 2 +} + public sealed class RekorBackend { public required string Name { get; init; } public required Uri Url { get; init; } + /// + /// Log version to use. Default is Auto for backward compatibility. + /// Set to V2 to explicitly opt into tile-based verification. + /// + public RekorLogVersion Version { get; init; } = RekorLogVersion.Auto; + + /// + /// Base URL for tile fetching in Rekor v2. + /// If not specified, tiles are fetched from {Url}/tile/. + /// Only used when Version is V2 or Auto detects v2 capabilities. + /// + public Uri? TileBaseUrl { get; init; } + + /// + /// Log ID (SHA-256 of the log's public key) for multi-log environments. + /// Used to match entries to the correct log when verifying bundles. + /// Production Rekor: c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d + /// + public string? LogId { get; init; } + + /// + /// Whether to prefer tile-based proofs when available. + /// When true and Version is Auto, will attempt tile fetching first. + /// + public bool PreferTileProofs { get; init; } = false; + public TimeSpan ProofTimeout { get; init; } = TimeSpan.FromSeconds(15); public TimeSpan PollInterval { get; init; } = TimeSpan.FromMilliseconds(250); public int MaxAttempts { get; init; } = 60; + + /// + /// Returns the effective tile base URL, defaulting to {Url}/tile/ if not specified. + /// + public Uri GetEffectiveTileBaseUrl() + { + if (TileBaseUrl is not null) + { + return TileBaseUrl; + } + + var baseUri = Url.ToString().TrimEnd('/'); + return new Uri($"{baseUri}/tile/", UriKind.Absolute); + } + + /// + /// Known log ID for the public Sigstore Rekor production instance. + /// + public const string SigstoreProductionLogId = "c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d"; } diff --git a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Core/Rekor/RekorReceipt.cs b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Core/Rekor/RekorReceipt.cs new file mode 100644 index 000000000..a02c2dffd --- /dev/null +++ b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Core/Rekor/RekorReceipt.cs @@ -0,0 +1,429 @@ +// ----------------------------------------------------------------------------- +// RekorReceipt.cs +// Description: Standardized Rekor transparency log receipt per Sigstore conventions. +// Implements receipt schema standardization from SBOM-VEX-policy advisory. +// References: https://docs.sigstore.dev/logging/overview/, Rekor v2 GA +// ----------------------------------------------------------------------------- + +using System.Text.Json.Serialization; + +namespace StellaOps.Attestor.Core.Rekor; + +/// +/// Standardized Rekor transparency log receipt following Sigstore conventions. +/// +/// +/// This receipt format aligns with: +/// - Rekor v2 GA specification (https://blog.sigstore.dev/rekor-v2-ga/) +/// - Sigstore bundle format (https://docs.sigstore.dev/bundle/) +/// - RFC 6962 certificate transparency log semantics +/// +/// Design principles: +/// - All fields use deterministic JSON property names +/// - Timestamps use Unix seconds for interoperability +/// - Hashes use lowercase hex encoding +/// - Inclusion proofs follow RFC 6962 structure +/// +public sealed record RekorReceipt +{ + /// + /// Schema version for this receipt format. + /// + [JsonPropertyName("schemaVersion")] + public string SchemaVersion { get; init; } = "1.0.0"; + + /// + /// Unique entry identifier (64-character hex string derived from entry hash). + /// + [JsonPropertyName("uuid")] + public required string Uuid { get; init; } + + /// + /// Log index (position in the log, monotonically increasing). + /// + [JsonPropertyName("logIndex")] + public required long LogIndex { get; init; } + + /// + /// Log ID identifying the specific Rekor instance/shard. + /// + [JsonPropertyName("logId")] + public required string LogId { get; init; } + + /// + /// Base URL of the Rekor log instance. + /// + [JsonPropertyName("logUrl")] + public required string LogUrl { get; init; } + + /// + /// Unix timestamp (seconds) when the entry was integrated into the log. + /// + [JsonPropertyName("integratedTime")] + public required long IntegratedTime { get; init; } + + /// + /// Entry kind (e.g., "intoto", "hashedrekord", "dsse"). + /// + [JsonPropertyName("entryKind")] + public required string EntryKind { get; init; } + + /// + /// Entry API version within the kind. + /// + [JsonPropertyName("entryVersion")] + public string EntryVersion { get; init; } = "0.0.2"; + + /// + /// SHA-256 hash of the canonicalized entry body (lowercase hex). + /// + [JsonPropertyName("entryBodyHash")] + public required string EntryBodyHash { get; init; } + + /// + /// Signed checkpoint (signed tree head) in note format. + /// + [JsonPropertyName("checkpoint")] + public required RekorCheckpointV2 Checkpoint { get; init; } + + /// + /// Inclusion proof demonstrating entry is in the log. + /// + [JsonPropertyName("inclusionProof")] + public required RekorInclusionProofV2 InclusionProof { get; init; } + + /// + /// Optional SET (Signed Entry Timestamp) for backward compatibility. + /// + [JsonPropertyName("signedEntryTimestamp")] + public string? SignedEntryTimestamp { get; init; } + + /// + /// Policy hash linking this receipt to a specific policy evaluation. + /// + [JsonPropertyName("policyHash")] + public string? PolicyHash { get; init; } + + /// + /// Graph revision ID for reachability context. + /// + [JsonPropertyName("graphRevision")] + public string? GraphRevision { get; init; } + + /// + /// Idempotency key used for submission (for deduplication tracking). + /// + [JsonPropertyName("idempotencyKey")] + public string? IdempotencyKey { get; init; } + + // Computed properties + + /// + /// Gets the integrated time as a DateTimeOffset (UTC). + /// + [JsonIgnore] + public DateTimeOffset IntegratedTimeUtc => + DateTimeOffset.FromUnixTimeSeconds(IntegratedTime); + + /// + /// Gets the full entry URL for direct access. + /// + [JsonIgnore] + public string EntryUrl => $"{LogUrl.TrimEnd('/')}/api/v1/log/entries/{Uuid}"; +} + +/// +/// Rekor v2 checkpoint (signed tree head) following note format. +/// +/// +/// Checkpoint format per Rekor v2 specification: +/// - Origin identifies the log +/// - Size is the tree size at checkpoint +/// - RootHash is the Merkle root +/// - Signature is over the checkpoint note body +/// +public sealed record RekorCheckpointV2 +{ + /// + /// Origin line identifying the log (e.g., "rekor.sigstore.dev - 2605736670972794746"). + /// + [JsonPropertyName("origin")] + public required string Origin { get; init; } + + /// + /// Tree size at time of checkpoint. + /// + [JsonPropertyName("size")] + public required long Size { get; init; } + + /// + /// Merkle tree root hash (lowercase hex). + /// + [JsonPropertyName("rootHash")] + public required string RootHash { get; init; } + + /// + /// Unix timestamp (seconds) of the checkpoint. + /// + [JsonPropertyName("timestamp")] + public required long Timestamp { get; init; } + + /// + /// Base64-encoded signature over the checkpoint note. + /// + [JsonPropertyName("signature")] + public required string Signature { get; init; } + + /// + /// Key ID or hint for signature verification. + /// + [JsonPropertyName("keyHint")] + public string? KeyHint { get; init; } + + /// + /// Raw note body for signature verification (base64-encoded). + /// + [JsonPropertyName("noteBody")] + public string? NoteBody { get; init; } + + /// + /// Gets the timestamp as a DateTimeOffset (UTC). + /// + [JsonIgnore] + public DateTimeOffset TimestampUtc => + DateTimeOffset.FromUnixTimeSeconds(Timestamp); +} + +/// +/// Rekor v2 inclusion proof following RFC 6962. +/// +/// +/// Inclusion proof structure: +/// - LeafHash is H(0x00 || entry) +/// - Hashes are the sibling nodes from leaf to root +/// - TreeSize and LogIndex define the proof context +/// +public sealed record RekorInclusionProofV2 +{ + /// + /// Log index of the entry being proven. + /// + [JsonPropertyName("logIndex")] + public required long LogIndex { get; init; } + + /// + /// Tree size at time of proof generation. + /// + [JsonPropertyName("treeSize")] + public required long TreeSize { get; init; } + + /// + /// Root hash at time of proof (lowercase hex). + /// + [JsonPropertyName("rootHash")] + public required string RootHash { get; init; } + + /// + /// Leaf hash (SHA-256 of 0x00 || entry body, lowercase hex). + /// + [JsonPropertyName("leafHash")] + public required string LeafHash { get; init; } + + /// + /// Inclusion proof hashes from leaf to root (lowercase hex, ordered). + /// + [JsonPropertyName("hashes")] + public required IReadOnlyList Hashes { get; init; } + + /// + /// Checkpoint reference containing the signed tree head. + /// + [JsonPropertyName("checkpoint")] + public string? CheckpointRef { get; init; } +} + +/// +/// Result of verifying a Rekor receipt. +/// +public sealed record RekorReceiptVerificationResult +{ + /// + /// Whether the receipt is valid. + /// + public required bool IsValid { get; init; } + + /// + /// Whether the checkpoint signature verified. + /// + public required bool CheckpointSignatureValid { get; init; } + + /// + /// Whether the inclusion proof verified against the root. + /// + public required bool InclusionProofValid { get; init; } + + /// + /// Whether the entry hash matches the leaf. + /// + public required bool EntryHashValid { get; init; } + + /// + /// Time skew in seconds (positive = receipt ahead of local clock). + /// + public double TimeSkewSeconds { get; init; } + + /// + /// Whether time skew is within acceptable bounds. + /// + public required bool TimeSkewAcceptable { get; init; } + + /// + /// Any verification errors encountered. + /// + public IReadOnlyList Errors { get; init; } = []; + + /// + /// Verification diagnostics for debugging. + /// + public IReadOnlyDictionary Diagnostics { get; init; } = + new Dictionary(); + + /// + /// When the verification was performed (UTC). + /// + public required DateTimeOffset VerifiedAt { get; init; } + + /// + /// Whether this was verified in offline mode. + /// + public bool OfflineVerification { get; init; } +} + +/// +/// Options for Rekor receipt verification. +/// +public sealed record RekorReceiptVerificationOptions +{ + /// + /// Maximum allowed clock skew in seconds (default: 300 = 5 minutes). + /// + public int MaxClockSkewSeconds { get; init; } = 300; + + /// + /// Whether to allow offline verification using cached checkpoints. + /// + public bool AllowOfflineVerification { get; init; } = true; + + /// + /// Path to offline checkpoint bundle for air-gapped verification. + /// + public string? OfflineCheckpointBundlePath { get; init; } + + /// + /// Maximum checkpoint age in hours for offline verification (default: 24). + /// + public int MaxOfflineCheckpointAgeHours { get; init; } = 24; + + /// + /// Whether to require checkpoint signature verification. + /// + public bool RequireCheckpointSignature { get; init; } = true; + + /// + /// Trusted public keys for checkpoint verification (PEM or base64 DER). + /// + public IReadOnlyList TrustedPublicKeys { get; init; } = []; + + /// + /// Trusted log IDs (if empty, all known logs are trusted). + /// + public IReadOnlyList TrustedLogIds { get; init; } = []; +} + +/// +/// Service for verifying Rekor receipts. +/// +public interface IRekorReceiptVerifier +{ + /// + /// Verifies a Rekor receipt. + /// + Task VerifyAsync( + RekorReceipt receipt, + RekorReceiptVerificationOptions? options = null, + CancellationToken cancellationToken = default); + + /// + /// Verifies the inclusion proof without network access. + /// + RekorReceiptVerificationResult VerifyInclusionProofOffline( + RekorReceipt receipt, + byte[] entryBody, + RekorReceiptVerificationOptions? options = null); +} + +/// +/// Factory for creating Rekor receipts from submission responses. +/// +public static class RekorReceiptFactory +{ + /// + /// Creates a standardized receipt from a submission response. + /// + public static RekorReceipt FromSubmissionResponse( + RekorSubmissionResponse response, + string logId, + string logUrl, + string entryKind, + string entryBodyHash, + string? policyHash = null, + string? graphRevision = null, + string? idempotencyKey = null) + { + ArgumentNullException.ThrowIfNull(response); + ArgumentException.ThrowIfNullOrEmpty(logId); + ArgumentException.ThrowIfNullOrEmpty(logUrl); + ArgumentException.ThrowIfNullOrEmpty(entryKind); + ArgumentException.ThrowIfNullOrEmpty(entryBodyHash); + + if (response.Proof?.Checkpoint is null) + { + throw new ArgumentException("Response must include checkpoint proof", nameof(response)); + } + + if (response.Proof?.Inclusion is null) + { + throw new ArgumentException("Response must include inclusion proof", nameof(response)); + } + + return new RekorReceipt + { + Uuid = response.Uuid, + LogIndex = response.Index ?? throw new ArgumentException("Response must include index"), + LogId = logId, + LogUrl = logUrl, + IntegratedTime = response.IntegratedTime ?? throw new ArgumentException("Response must include integrated time"), + EntryKind = entryKind, + EntryBodyHash = entryBodyHash, + Checkpoint = new RekorCheckpointV2 + { + Origin = response.Proof.Checkpoint.Origin ?? logId, + Size = response.Proof.Checkpoint.Size, + RootHash = response.Proof.Checkpoint.RootHash ?? throw new ArgumentException("Checkpoint must include root hash"), + Timestamp = response.Proof.Checkpoint.Timestamp?.ToUnixTimeSeconds() ?? response.IntegratedTime.Value, + Signature = "" // Will be populated from actual response + }, + InclusionProof = new RekorInclusionProofV2 + { + LogIndex = response.Index.Value, + TreeSize = response.Proof.Checkpoint.Size, + RootHash = response.Proof.Checkpoint.RootHash, + LeafHash = response.Proof.Inclusion.LeafHash ?? throw new ArgumentException("Inclusion proof must include leaf hash"), + Hashes = response.Proof.Inclusion.Path + }, + PolicyHash = policyHash, + GraphRevision = graphRevision, + IdempotencyKey = idempotencyKey + }; + } +} diff --git a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Core/Transparency/TransparencyServiceExtensions.cs b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Core/Transparency/TransparencyServiceExtensions.cs new file mode 100644 index 000000000..b69e32d6e --- /dev/null +++ b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Core/Transparency/TransparencyServiceExtensions.cs @@ -0,0 +1,46 @@ +// ----------------------------------------------------------------------------- +// TransparencyServiceExtensions.cs +// Description: DI extensions for transparency status services. +// ----------------------------------------------------------------------------- + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace StellaOps.Attestor.Core.Transparency; + +/// +/// Extension methods for registering transparency services. +/// +public static class TransparencyServiceExtensions +{ + /// + /// Adds transparency status services to the service collection. + /// + /// The service collection. + /// Optional configuration action. + /// The service collection for chaining. + public static IServiceCollection AddTransparencyStatus( + this IServiceCollection services, + Action? configure = null) + { + ArgumentNullException.ThrowIfNull(services); + + // Register options + if (configure is not null) + { + services.Configure(configure); + } + else + { + services.AddOptions(); + } + + // Register provider + services.TryAddSingleton(); + + // Ensure TimeProvider is available + services.TryAddSingleton(TimeProvider.System); + + return services; + } +} diff --git a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Core/Transparency/TransparencyStatus.cs b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Core/Transparency/TransparencyStatus.cs new file mode 100644 index 000000000..565da75ed --- /dev/null +++ b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Core/Transparency/TransparencyStatus.cs @@ -0,0 +1,425 @@ +// ----------------------------------------------------------------------------- +// TransparencyStatus.cs +// Description: Transparency log freshness status for health endpoints. +// Implements "last sync" freshness badge from SBOM-VEX-policy advisory. +// ----------------------------------------------------------------------------- + +using System.Text.Json.Serialization; + +namespace StellaOps.Attestor.Core.Transparency; + +/// +/// Transparency log freshness status for health endpoints and status bars. +/// +/// +/// Implements the "last sync" freshness badge pattern: +/// - Shows when transparency log was last successfully synced +/// - Indicates whether operations are using verified or degraded mode +/// - Provides clear guidance for air-gapped environments +/// +public sealed record TransparencyStatus +{ + /// + /// Overall transparency status. + /// + [JsonPropertyName("status")] + public required TransparencyStatusLevel Status { get; init; } + + /// + /// Human-readable status message. + /// + [JsonPropertyName("message")] + public required string Message { get; init; } + + /// + /// When the transparency log was last successfully synced (UTC). + /// + [JsonPropertyName("lastSyncAt")] + public DateTimeOffset? LastSyncAt { get; init; } + + /// + /// Age of the last sync in hours. + /// + [JsonPropertyName("lastSyncAgeHours")] + public double? LastSyncAgeHours { get; init; } + + /// + /// When the checkpoint was last verified (UTC). + /// + [JsonPropertyName("lastCheckpointVerifiedAt")] + public DateTimeOffset? LastCheckpointVerifiedAt { get; init; } + + /// + /// Latest verified checkpoint tree size. + /// + [JsonPropertyName("lastCheckpointTreeSize")] + public long? LastCheckpointTreeSize { get; init; } + + /// + /// Whether the service is operating in offline/air-gapped mode. + /// + [JsonPropertyName("offlineMode")] + public bool OfflineMode { get; init; } + + /// + /// Whether transparency verification is enforced (vs. best-effort). + /// + [JsonPropertyName("enforcementEnabled")] + public bool EnforcementEnabled { get; init; } + + /// + /// Configured maximum checkpoint age before warning (hours). + /// + [JsonPropertyName("maxCheckpointAgeHours")] + public int MaxCheckpointAgeHours { get; init; } + + /// + /// Primary Rekor backend URL. + /// + [JsonPropertyName("rekorBackend")] + public string? RekorBackend { get; init; } + + /// + /// Mirror Rekor backend URL (for air-gapped or fallback). + /// + [JsonPropertyName("rekorMirror")] + public string? RekorMirror { get; init; } + + /// + /// Submission queue depth (pending entries awaiting transparency anchoring). + /// + [JsonPropertyName("submissionQueueDepth")] + public int SubmissionQueueDepth { get; init; } + + /// + /// Number of entries in dead-letter queue (failed submissions). + /// + [JsonPropertyName("deadLetterQueueDepth")] + public int DeadLetterQueueDepth { get; init; } + + /// + /// Metrics for recent operations. + /// + [JsonPropertyName("metrics")] + public TransparencyMetrics? Metrics { get; init; } + + /// + /// Health check details for each backend. + /// + [JsonPropertyName("backends")] + public IReadOnlyList Backends { get; init; } = []; + + /// + /// Whether the status indicates healthy operation. + /// + [JsonIgnore] + public bool IsHealthy => Status is TransparencyStatusLevel.Healthy or TransparencyStatusLevel.Degraded; + + /// + /// Whether the checkpoint is considered fresh. + /// + [JsonIgnore] + public bool IsCheckpointFresh => + LastSyncAgeHours.HasValue && LastSyncAgeHours.Value <= MaxCheckpointAgeHours; +} + +/// +/// Transparency status level for health indicators. +/// +public enum TransparencyStatusLevel +{ + /// + /// All transparency backends are healthy and synced. + /// + Healthy, + + /// + /// Operating with stale checkpoint or fallback backend. + /// + Degraded, + + /// + /// Operating in offline mode with acceptable checkpoint age. + /// + Offline, + + /// + /// Transparency verification is unavailable or severely degraded. + /// + Unhealthy, + + /// + /// Transparency status is unknown (not yet initialized). + /// + Unknown +} + +/// +/// Metrics for transparency operations. +/// +public sealed record TransparencyMetrics +{ + /// + /// Total submissions in the last hour. + /// + [JsonPropertyName("submissionsLastHour")] + public int SubmissionsLastHour { get; init; } + + /// + /// Successful submissions in the last hour. + /// + [JsonPropertyName("successfulSubmissionsLastHour")] + public int SuccessfulSubmissionsLastHour { get; init; } + + /// + /// Failed submissions in the last hour. + /// + [JsonPropertyName("failedSubmissionsLastHour")] + public int FailedSubmissionsLastHour { get; init; } + + /// + /// Total verifications in the last hour. + /// + [JsonPropertyName("verificationsLastHour")] + public int VerificationsLastHour { get; init; } + + /// + /// Successful verifications in the last hour. + /// + [JsonPropertyName("successfulVerificationsLastHour")] + public int SuccessfulVerificationsLastHour { get; init; } + + /// + /// Average submission latency in milliseconds. + /// + [JsonPropertyName("avgSubmissionLatencyMs")] + public double AvgSubmissionLatencyMs { get; init; } + + /// + /// P95 submission latency in milliseconds. + /// + [JsonPropertyName("p95SubmissionLatencyMs")] + public double P95SubmissionLatencyMs { get; init; } + + /// + /// Offline verifications in the last hour. + /// + [JsonPropertyName("offlineVerificationsLastHour")] + public int OfflineVerificationsLastHour { get; init; } +} + +/// +/// Status of a single transparency backend. +/// +public sealed record TransparencyBackendStatus +{ + /// + /// Backend identifier (e.g., "rekor.sigstore.dev", "rekor-mirror.internal"). + /// + [JsonPropertyName("id")] + public required string Id { get; init; } + + /// + /// Backend URL. + /// + [JsonPropertyName("url")] + public required string Url { get; init; } + + /// + /// Whether this is the primary backend. + /// + [JsonPropertyName("primary")] + public bool Primary { get; init; } + + /// + /// Backend health status. + /// + [JsonPropertyName("status")] + public required BackendHealthStatus Status { get; init; } + + /// + /// When the backend was last checked. + /// + [JsonPropertyName("lastCheckedAt")] + public DateTimeOffset? LastCheckedAt { get; init; } + + /// + /// Latest response latency in milliseconds. + /// + [JsonPropertyName("latencyMs")] + public double? LatencyMs { get; init; } + + /// + /// Error message if unhealthy. + /// + [JsonPropertyName("error")] + public string? Error { get; init; } + + /// + /// Latest checkpoint tree size from this backend. + /// + [JsonPropertyName("treeSize")] + public long? TreeSize { get; init; } +} + +/// +/// Health status of a backend. +/// +public enum BackendHealthStatus +{ + /// + /// Backend is healthy and responding. + /// + Healthy, + + /// + /// Backend is responding slowly. + /// + Slow, + + /// + /// Backend is unreachable or erroring. + /// + Unhealthy, + + /// + /// Backend status is unknown. + /// + Unknown +} + +/// +/// Service for retrieving transparency status. +/// +public interface ITransparencyStatusProvider +{ + /// + /// Gets the current transparency status. + /// + Task GetStatusAsync(CancellationToken cancellationToken = default); + + /// + /// Forces a refresh of the transparency status (e.g., recheck backends). + /// + Task RefreshAsync(CancellationToken cancellationToken = default); + + /// + /// Records a successful submission for metrics. + /// + void RecordSubmission(bool success, TimeSpan latency); + + /// + /// Records a verification attempt for metrics. + /// + void RecordVerification(bool success, bool offline); + + /// + /// Updates the last sync timestamp. + /// + void RecordSync(DateTimeOffset syncTime, long treeSize); +} + +/// +/// Configuration for transparency status provider. +/// +public sealed record TransparencyStatusOptions +{ + /// + /// Maximum checkpoint age in hours before status becomes degraded (default: 24). + /// + public int MaxCheckpointAgeHours { get; init; } = 24; + + /// + /// Maximum checkpoint age in hours before status becomes unhealthy (default: 72). + /// + public int CriticalCheckpointAgeHours { get; init; } = 72; + + /// + /// Backend health check interval in seconds (default: 60). + /// + public int HealthCheckIntervalSeconds { get; init; } = 60; + + /// + /// Backend timeout in seconds (default: 10). + /// + public int BackendTimeoutSeconds { get; init; } = 10; + + /// + /// Latency threshold for "slow" status in milliseconds (default: 2000). + /// + public int SlowLatencyThresholdMs { get; init; } = 2000; + + /// + /// Whether to enable enforcement mode (fail operations without transparency). + /// + public bool EnforcementEnabled { get; init; } = false; + + /// + /// Primary Rekor backend URL. + /// + public string? RekorBackendUrl { get; init; } + + /// + /// Mirror Rekor backend URL. + /// + public string? RekorMirrorUrl { get; init; } +} + +/// +/// Interface for checking transparency backend health. +/// Implemented in infrastructure layer with HTTP client support. +/// +public interface ITransparencyBackendHealthChecker +{ + /// + /// Checks the health of a transparency backend. + /// + /// The backend URL to check. + /// Timeout in seconds. + /// Cancellation token. + /// Health check result. + Task CheckHealthAsync( + string url, + int timeoutSeconds, + CancellationToken cancellationToken = default); +} + +/// +/// Result of a backend health check. +/// +public sealed record BackendHealthCheckResult +{ + /// + /// Whether the backend is healthy. + /// + public required bool IsHealthy { get; init; } + + /// + /// Response latency in milliseconds. + /// + public required double LatencyMs { get; init; } + + /// + /// Error message if unhealthy. + /// + public string? Error { get; init; } + + /// + /// Creates a healthy result. + /// + public static BackendHealthCheckResult Healthy(double latencyMs) => new() + { + IsHealthy = true, + LatencyMs = latencyMs + }; + + /// + /// Creates an unhealthy result. + /// + public static BackendHealthCheckResult Unhealthy(string error, double latencyMs = 0) => new() + { + IsHealthy = false, + LatencyMs = latencyMs, + Error = error + }; +} diff --git a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Core/Transparency/TransparencyStatusProvider.cs b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Core/Transparency/TransparencyStatusProvider.cs new file mode 100644 index 000000000..472509cf1 --- /dev/null +++ b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Core/Transparency/TransparencyStatusProvider.cs @@ -0,0 +1,347 @@ +// ----------------------------------------------------------------------------- +// TransparencyStatusProvider.cs +// Description: Default implementation of transparency status provider. +// Tracks sync times, metrics, and backend health for freshness indicators. +// ----------------------------------------------------------------------------- + +using System.Collections.Concurrent; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace StellaOps.Attestor.Core.Transparency; + +/// +/// Default implementation of . +/// +public sealed class TransparencyStatusProvider : ITransparencyStatusProvider, IDisposable +{ + private readonly ILogger _logger; + private readonly TransparencyStatusOptions _options; + private readonly TimeProvider _timeProvider; + private readonly ITransparencyBackendHealthChecker? _healthChecker; + + private readonly object _lock = new(); + private DateTimeOffset? _lastSyncAt; + private long _lastTreeSize; + private DateTimeOffset? _lastCheckpointVerifiedAt; + + // Metrics tracking (thread-safe) + private readonly ConcurrentQueue _submissionMetrics = new(); + private readonly ConcurrentQueue _verificationMetrics = new(); + + // Backend status cache + private readonly ConcurrentDictionary _backendStatuses = new(); + private DateTimeOffset _lastHealthCheck = DateTimeOffset.MinValue; + + // Queue depth tracking + private int _submissionQueueDepth; + private int _deadLetterQueueDepth; + + public TransparencyStatusProvider( + ILogger logger, + IOptions options, + TimeProvider timeProvider, + ITransparencyBackendHealthChecker? healthChecker = null) + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _options = options?.Value ?? throw new ArgumentNullException(nameof(options)); + _timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider)); + _healthChecker = healthChecker; + } + + /// + public async Task GetStatusAsync(CancellationToken cancellationToken = default) + { + var now = _timeProvider.GetUtcNow(); + + // Check if we need to refresh backend health + if (now - _lastHealthCheck > TimeSpan.FromSeconds(_options.HealthCheckIntervalSeconds)) + { + await RefreshBackendHealthAsync(cancellationToken); + } + + return BuildStatus(now); + } + + /// + public async Task RefreshAsync(CancellationToken cancellationToken = default) + { + await RefreshBackendHealthAsync(cancellationToken); + return BuildStatus(_timeProvider.GetUtcNow()); + } + + /// + public void RecordSubmission(bool success, TimeSpan latency) + { + var entry = new MetricEntry( + _timeProvider.GetUtcNow(), + success, + false, + latency.TotalMilliseconds); + + _submissionMetrics.Enqueue(entry); + PruneOldMetrics(_submissionMetrics); + + _logger.LogDebug( + "Recorded transparency submission: success={Success}, latency={LatencyMs}ms", + success, + latency.TotalMilliseconds); + } + + /// + public void RecordVerification(bool success, bool offline) + { + var entry = new MetricEntry( + _timeProvider.GetUtcNow(), + success, + offline, + 0); + + _verificationMetrics.Enqueue(entry); + PruneOldMetrics(_verificationMetrics); + + _logger.LogDebug( + "Recorded transparency verification: success={Success}, offline={Offline}", + success, + offline); + } + + /// + public void RecordSync(DateTimeOffset syncTime, long treeSize) + { + lock (_lock) + { + _lastSyncAt = syncTime; + _lastTreeSize = treeSize; + _lastCheckpointVerifiedAt = _timeProvider.GetUtcNow(); + } + + _logger.LogInformation( + "Recorded transparency sync: time={SyncTime}, treeSize={TreeSize}", + syncTime, + treeSize); + } + + /// + /// Updates the queue depths for status reporting. + /// + public void UpdateQueueDepths(int submissionQueue, int deadLetterQueue) + { + Interlocked.Exchange(ref _submissionQueueDepth, submissionQueue); + Interlocked.Exchange(ref _deadLetterQueueDepth, deadLetterQueue); + } + + private TransparencyStatus BuildStatus(DateTimeOffset now) + { + double? lastSyncAgeHours = null; + DateTimeOffset? lastSync; + long lastTreeSize; + DateTimeOffset? lastCheckpointVerified; + + lock (_lock) + { + lastSync = _lastSyncAt; + lastTreeSize = _lastTreeSize; + lastCheckpointVerified = _lastCheckpointVerifiedAt; + + if (_lastSyncAt.HasValue) + { + lastSyncAgeHours = (now - _lastSyncAt.Value).TotalHours; + } + } + + var (status, message) = DetermineStatus(lastSyncAgeHours); + var metrics = CalculateMetrics(now); + var backends = _backendStatuses.Values.ToList(); + + return new TransparencyStatus + { + Status = status, + Message = message, + LastSyncAt = lastSync, + LastSyncAgeHours = lastSyncAgeHours, + LastCheckpointVerifiedAt = lastCheckpointVerified, + LastCheckpointTreeSize = lastTreeSize > 0 ? lastTreeSize : null, + OfflineMode = string.IsNullOrEmpty(_options.RekorBackendUrl), + EnforcementEnabled = _options.EnforcementEnabled, + MaxCheckpointAgeHours = _options.MaxCheckpointAgeHours, + RekorBackend = _options.RekorBackendUrl, + RekorMirror = _options.RekorMirrorUrl, + SubmissionQueueDepth = _submissionQueueDepth, + DeadLetterQueueDepth = _deadLetterQueueDepth, + Metrics = metrics, + Backends = backends + }; + } + + private (TransparencyStatusLevel, string) DetermineStatus(double? lastSyncAgeHours) + { + // No backend configured - offline mode + if (string.IsNullOrEmpty(_options.RekorBackendUrl)) + { + if (lastSyncAgeHours is null) + { + return (TransparencyStatusLevel.Offline, "Operating in offline mode - no checkpoint synced"); + } + + if (lastSyncAgeHours <= _options.MaxCheckpointAgeHours) + { + return (TransparencyStatusLevel.Offline, $"Operating in offline mode - checkpoint is {lastSyncAgeHours:F1}h old"); + } + + return (TransparencyStatusLevel.Unhealthy, $"Offline mode with stale checkpoint ({lastSyncAgeHours:F1}h old)"); + } + + // No sync ever + if (lastSyncAgeHours is null) + { + return (TransparencyStatusLevel.Unknown, "Transparency log never synced"); + } + + // Fresh checkpoint + if (lastSyncAgeHours <= _options.MaxCheckpointAgeHours) + { + return (TransparencyStatusLevel.Healthy, $"Transparency log synced {lastSyncAgeHours:F1}h ago"); + } + + // Stale but acceptable + if (lastSyncAgeHours <= _options.CriticalCheckpointAgeHours) + { + return (TransparencyStatusLevel.Degraded, $"Transparency log checkpoint is stale ({lastSyncAgeHours:F1}h old)"); + } + + // Critical staleness + return (TransparencyStatusLevel.Unhealthy, $"Transparency log checkpoint is critically stale ({lastSyncAgeHours:F1}h old)"); + } + + private TransparencyMetrics CalculateMetrics(DateTimeOffset now) + { + var oneHourAgo = now.AddHours(-1); + + var recentSubmissions = _submissionMetrics + .Where(m => m.Timestamp >= oneHourAgo) + .ToList(); + + var recentVerifications = _verificationMetrics + .Where(m => m.Timestamp >= oneHourAgo) + .ToList(); + + var successfulSubmissions = recentSubmissions.Where(m => m.Success).ToList(); + var latencies = successfulSubmissions.Select(m => m.LatencyMs).OrderBy(l => l).ToList(); + + return new TransparencyMetrics + { + SubmissionsLastHour = recentSubmissions.Count, + SuccessfulSubmissionsLastHour = successfulSubmissions.Count, + FailedSubmissionsLastHour = recentSubmissions.Count - successfulSubmissions.Count, + VerificationsLastHour = recentVerifications.Count, + SuccessfulVerificationsLastHour = recentVerifications.Count(m => m.Success), + AvgSubmissionLatencyMs = latencies.Count > 0 ? latencies.Average() : 0, + P95SubmissionLatencyMs = latencies.Count > 0 ? Percentile(latencies, 95) : 0, + OfflineVerificationsLastHour = recentVerifications.Count(m => m.Offline) + }; + } + + private async Task RefreshBackendHealthAsync(CancellationToken cancellationToken) + { + _lastHealthCheck = _timeProvider.GetUtcNow(); + + var tasks = new List(); + + if (!string.IsNullOrEmpty(_options.RekorBackendUrl)) + { + tasks.Add(CheckBackendHealthAsync("primary", _options.RekorBackendUrl, true, cancellationToken)); + } + + if (!string.IsNullOrEmpty(_options.RekorMirrorUrl)) + { + tasks.Add(CheckBackendHealthAsync("mirror", _options.RekorMirrorUrl, false, cancellationToken)); + } + + if (tasks.Count > 0) + { + await Task.WhenAll(tasks); + } + } + + private async Task CheckBackendHealthAsync( + string id, + string url, + bool primary, + CancellationToken cancellationToken) + { + var status = new TransparencyBackendStatus + { + Id = id, + Url = url, + Primary = primary, + Status = BackendHealthStatus.Unknown, + LastCheckedAt = _timeProvider.GetUtcNow() + }; + + if (_healthChecker is null) + { + _backendStatuses[id] = status; + return; + } + + try + { + var result = await _healthChecker.CheckHealthAsync( + url, + _options.BackendTimeoutSeconds, + cancellationToken); + + var healthStatus = result.IsHealthy + ? (result.LatencyMs > _options.SlowLatencyThresholdMs ? BackendHealthStatus.Slow : BackendHealthStatus.Healthy) + : BackendHealthStatus.Unhealthy; + + status = status with + { + Status = healthStatus, + LatencyMs = result.LatencyMs, + Error = result.Error + }; + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to check transparency backend health: {Url}", url); + status = status with + { + Status = BackendHealthStatus.Unhealthy, + Error = ex.Message + }; + } + + _backendStatuses[id] = status; + } + + private void PruneOldMetrics(ConcurrentQueue queue) + { + var cutoff = _timeProvider.GetUtcNow().AddHours(-2); + + while (queue.TryPeek(out var entry) && entry.Timestamp < cutoff) + { + queue.TryDequeue(out _); + } + } + + private static double Percentile(List values, int percentile) + { + if (values.Count == 0) return 0; + + var index = (int)Math.Ceiling(percentile / 100.0 * values.Count) - 1; + return values[Math.Max(0, Math.Min(index, values.Count - 1))]; + } + + public void Dispose() + { + // No unmanaged resources to dispose + } + + private sealed record MetricEntry( + DateTimeOffset Timestamp, + bool Success, + bool Offline, + double LatencyMs); +} diff --git a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/Rekor/HttpRekorTileClient.cs b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/Rekor/HttpRekorTileClient.cs new file mode 100644 index 000000000..029b5f0f9 --- /dev/null +++ b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/Rekor/HttpRekorTileClient.cs @@ -0,0 +1,469 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using StellaOps.Attestor.Core.Rekor; + +namespace StellaOps.Attestor.Infrastructure.Rekor; + +/// +/// HTTP client for fetching proofs from Rekor v2 tile-based logs. +/// Implements the Sunlight/C2SP tlog-tiles specification. +/// +internal sealed class HttpRekorTileClient : IRekorTileClient +{ + private const int TileHeight = 8; // Standard tile height (2^8 = 256 entries per tile) + private const int TileWidth = 1 << TileHeight; // 256 entries per full tile + private const int HashSize = 32; // SHA-256 + + private readonly HttpClient _httpClient; + private readonly ILogger _logger; + + public HttpRekorTileClient(HttpClient httpClient, ILogger logger) + { + _httpClient = httpClient; + _logger = logger; + } + + /// + public async Task GetCheckpointAsync( + RekorBackend backend, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(backend); + + var checkpointUrl = new Uri(backend.GetEffectiveTileBaseUrl(), "../checkpoint"); + _logger.LogDebug("Fetching checkpoint from {Url}", checkpointUrl); + + try + { + using var request = new HttpRequestMessage(HttpMethod.Get, checkpointUrl); + using var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false); + + if (response.StatusCode == HttpStatusCode.NotFound) + { + _logger.LogDebug("Checkpoint not found at {Url}", checkpointUrl); + return null; + } + + response.EnsureSuccessStatusCode(); + + var content = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); + return ParseCheckpoint(content); + } + catch (HttpRequestException ex) + { + _logger.LogWarning(ex, "Failed to fetch checkpoint from {Url}", checkpointUrl); + return null; + } + } + + /// + public async Task GetTileAsync( + RekorBackend backend, + int level, + long index, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(backend); + + // Tile path format: tile/{level}/{index...} where index is split into directories + var tilePath = FormatTilePath(level, index); + var tileUrl = new Uri(backend.GetEffectiveTileBaseUrl(), tilePath); + + _logger.LogDebug("Fetching tile at level {Level} index {Index} from {Url}", level, index, tileUrl); + + try + { + using var request = new HttpRequestMessage(HttpMethod.Get, tileUrl); + using var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false); + + if (response.StatusCode == HttpStatusCode.NotFound) + { + _logger.LogDebug("Tile not found at {Url}", tileUrl); + return null; + } + + response.EnsureSuccessStatusCode(); + + var data = await response.Content.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(false); + var width = data.Length / HashSize; + + return new RekorTileData + { + Level = level, + Index = index, + Width = width, + Hashes = data + }; + } + catch (HttpRequestException ex) + { + _logger.LogWarning(ex, "Failed to fetch tile from {Url}", tileUrl); + return null; + } + } + + /// + public async Task GetEntryAsync( + RekorBackend backend, + long logIndex, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(backend); + + // Entry path format: tile/entries/{index...} + var entryPath = FormatEntryPath(logIndex); + var entryUrl = new Uri(backend.GetEffectiveTileBaseUrl(), entryPath); + + _logger.LogDebug("Fetching entry at index {Index} from {Url}", logIndex, entryUrl); + + try + { + using var request = new HttpRequestMessage(HttpMethod.Get, entryUrl); + using var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false); + + if (response.StatusCode == HttpStatusCode.NotFound) + { + _logger.LogDebug("Entry not found at {Url}", entryUrl); + return null; + } + + response.EnsureSuccessStatusCode(); + + var data = await response.Content.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(false); + + return new RekorTileEntry + { + LogIndex = logIndex, + Data = data, + IntegratedTime = null // Would need to parse from entry format + }; + } + catch (HttpRequestException ex) + { + _logger.LogWarning(ex, "Failed to fetch entry from {Url}", entryUrl); + return null; + } + } + + /// + public async Task ComputeInclusionProofAsync( + RekorBackend backend, + long logIndex, + long treeSize, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(backend); + + if (logIndex < 0 || logIndex >= treeSize) + { + _logger.LogWarning("Invalid log index {Index} for tree size {Size}", logIndex, treeSize); + return null; + } + + _logger.LogDebug("Computing inclusion proof for index {Index} in tree of size {Size}", logIndex, treeSize); + + try + { + // Fetch the leaf tile to get the leaf hash + var leafTileIndex = logIndex / TileWidth; + var leafTile = await GetTileAsync(backend, 0, leafTileIndex, cancellationToken).ConfigureAwait(false); + + if (leafTile is null) + { + _logger.LogWarning("Failed to fetch leaf tile for index {Index}", logIndex); + return null; + } + + var positionInTile = (int)(logIndex % TileWidth); + if (positionInTile >= leafTile.Width) + { + _logger.LogWarning("Position {Position} exceeds tile width {Width}", positionInTile, leafTile.Width); + return null; + } + + var leafHash = leafTile.GetHash(positionInTile); + + // Compute the proof path by fetching required tiles + var path = await ComputeProofPathAsync(backend, logIndex, treeSize, cancellationToken).ConfigureAwait(false); + + if (path is null) + { + return null; + } + + // Compute expected root hash from path + var rootHash = ComputeRootFromPath(leafHash, logIndex, treeSize, path); + + return new RekorTileInclusionProof + { + LogIndex = logIndex, + TreeSize = treeSize, + LeafHash = leafHash, + Path = path, + RootHash = rootHash + }; + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to compute inclusion proof for index {Index}", logIndex); + return null; + } + } + + private async Task?> ComputeProofPathAsync( + RekorBackend backend, + long logIndex, + long treeSize, + CancellationToken cancellationToken) + { + var path = new List(); + var index = logIndex; + var size = treeSize; + var level = 0; + + while (size > 1) + { + var siblingIndex = index ^ 1; // XOR to get sibling + var tileIndex = siblingIndex / TileWidth; + var positionInTile = (int)(siblingIndex % TileWidth); + + // Only add sibling if it exists in the tree + if (siblingIndex < size) + { + var tile = await GetTileAsync(backend, level, tileIndex, cancellationToken).ConfigureAwait(false); + + if (tile is null || positionInTile >= tile.Width) + { + // For partial trees, compute ephemeral hash if needed + _logger.LogDebug("Sibling at level {Level} index {Index} not in tile, tree may be partial", level, siblingIndex); + + // For now, return null if we can't get the sibling + // A full implementation would handle partial tiles + return null; + } + + path.Add(tile.GetHash(positionInTile)); + } + + index /= 2; + size = (size + 1) / 2; + level++; + } + + return path; + } + + private static byte[] ComputeRootFromPath(byte[] leafHash, long logIndex, long treeSize, IReadOnlyList path) + { + var current = leafHash; + var index = logIndex; + var size = treeSize; + var pathIndex = 0; + + while (size > 1 && pathIndex < path.Count) + { + var siblingIndex = index ^ 1; + + if (siblingIndex < size) + { + var sibling = path[pathIndex++]; + + // Hash order depends on position + current = (index & 1) == 0 + ? HashPair(current, sibling) + : HashPair(sibling, current); + } + + index /= 2; + size = (size + 1) / 2; + } + + return current; + } + + private static byte[] HashPair(byte[] left, byte[] right) + { + using var sha256 = System.Security.Cryptography.SHA256.Create(); + + // RFC 6962: H(0x01 || left || right) + var input = new byte[1 + left.Length + right.Length]; + input[0] = 0x01; + Array.Copy(left, 0, input, 1, left.Length); + Array.Copy(right, 0, input, 1 + left.Length, right.Length); + + return sha256.ComputeHash(input); + } + + private RekorTileCheckpoint? ParseCheckpoint(string content) + { + // Checkpoint format (Go signed note format): + // + // + // + // [optional extension lines] + // + // ... + + var lines = content.Split('\n', StringSplitOptions.None); + + if (lines.Length < 4) + { + _logger.LogWarning("Checkpoint has too few lines: {Count}", lines.Length); + return null; + } + + var origin = lines[0]; + if (!long.TryParse(lines[1], NumberStyles.None, CultureInfo.InvariantCulture, out var treeSize)) + { + _logger.LogWarning("Invalid tree size in checkpoint: {Line}", lines[1]); + return null; + } + + byte[] rootHash; + try + { + rootHash = Convert.FromBase64String(lines[2]); + } + catch (FormatException) + { + _logger.LogWarning("Invalid root hash base64 in checkpoint: {Line}", lines[2]); + return null; + } + + // Find the blank line that separates checkpoint from signatures + var signatureStartIndex = -1; + for (var i = 3; i < lines.Length; i++) + { + if (string.IsNullOrWhiteSpace(lines[i])) + { + signatureStartIndex = i + 1; + break; + } + } + + var signatures = new List(); + if (signatureStartIndex > 0) + { + for (var i = signatureStartIndex; i < lines.Length; i++) + { + var sigLine = lines[i]; + if (string.IsNullOrWhiteSpace(sigLine)) + { + continue; + } + + // Signature format: + var parts = sigLine.Split(' ', 2); + if (parts.Length >= 2) + { + try + { + signatures.Add(new RekorCheckpointSignature + { + KeyHint = parts[0], + Signature = Convert.FromBase64String(parts[1]) + }); + } + catch (FormatException) + { + _logger.LogDebug("Skipping invalid signature line: {Line}", sigLine); + } + } + } + } + + // Extract raw checkpoint (everything before signatures) + var rawCheckpointEnd = signatureStartIndex > 0 ? signatureStartIndex - 1 : lines.Length; + var rawCheckpoint = string.Join('\n', lines[..rawCheckpointEnd]); + + return new RekorTileCheckpoint + { + Origin = origin, + TreeSize = treeSize, + RootHash = rootHash, + RawCheckpoint = rawCheckpoint, + Signatures = signatures + }; + } + + private static string FormatTilePath(int level, long index) + { + // Tile path uses base-1000 directory structure for scalability + // e.g., tile/0/x001/234 for level 0, index 1234 + var sb = new StringBuilder(); + sb.Append(level.ToString(CultureInfo.InvariantCulture)); + sb.Append('/'); + + if (index == 0) + { + sb.Append("000"); + } + else + { + var parts = new List(); + var remaining = index; + while (remaining > 0) + { + parts.Add((remaining % 1000).ToString("D3", CultureInfo.InvariantCulture)); + remaining /= 1000; + } + + parts.Reverse(); + // First part doesn't need leading zeros padding to 3 digits if it's the most significant + if (parts.Count > 0) + { + parts[0] = parts[0].TrimStart('0'); + if (string.IsNullOrEmpty(parts[0])) + { + parts[0] = "0"; + } + } + + sb.Append(string.Join('/', parts)); + } + + return sb.ToString(); + } + + private static string FormatEntryPath(long index) + { + // Entry path: entries/{index...} + var sb = new StringBuilder("entries/"); + + if (index == 0) + { + sb.Append("000"); + } + else + { + var parts = new List(); + var remaining = index; + while (remaining > 0) + { + parts.Add((remaining % 1000).ToString("D3", CultureInfo.InvariantCulture)); + remaining /= 1000; + } + + parts.Reverse(); + if (parts.Count > 0) + { + parts[0] = parts[0].TrimStart('0'); + if (string.IsNullOrEmpty(parts[0])) + { + parts[0] = "0"; + } + } + + sb.Append(string.Join('/', parts)); + } + + return sb.ToString(); + } +} diff --git a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/Rekor/RekorBackendResolver.cs b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/Rekor/RekorBackendResolver.cs index 819bcd38e..2285c9c79 100644 --- a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/Rekor/RekorBackendResolver.cs +++ b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/Rekor/RekorBackendResolver.cs @@ -47,9 +47,43 @@ internal static class RekorBackendResolver { Name = name, Url = new Uri(options.Url, UriKind.Absolute), + Version = ParseLogVersion(options.Version), + TileBaseUrl = string.IsNullOrWhiteSpace(options.TileBaseUrl) + ? null + : new Uri(options.TileBaseUrl, UriKind.Absolute), + LogId = options.LogId, + PreferTileProofs = options.PreferTileProofs, ProofTimeout = TimeSpan.FromMilliseconds(options.ProofTimeoutMs), PollInterval = TimeSpan.FromMilliseconds(options.PollIntervalMs), MaxAttempts = options.MaxAttempts }; } + + /// + /// Parses the log version string to the enum value. + /// + private static RekorLogVersion ParseLogVersion(string? version) + { + if (string.IsNullOrWhiteSpace(version)) + { + return RekorLogVersion.Auto; + } + + return version.Trim().ToUpperInvariant() switch + { + "AUTO" => RekorLogVersion.Auto, + "V1" or "1" => RekorLogVersion.V1, + "V2" or "2" => RekorLogVersion.V2, + _ => RekorLogVersion.Auto + }; + } + + /// + /// Determines if the backend should use tile-based verification. + /// + public static bool ShouldUseTileProofs(RekorBackend backend) + { + return backend.Version == RekorLogVersion.V2 || + (backend.Version == RekorLogVersion.Auto && backend.PreferTileProofs); + } } diff --git a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/ServiceCollectionExtensions.cs b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/ServiceCollectionExtensions.cs index 9b40ad90f..d1e8d9f58 100644 --- a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/ServiceCollectionExtensions.cs +++ b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Infrastructure/ServiceCollectionExtensions.cs @@ -96,6 +96,20 @@ public static class ServiceCollectionExtensions }); services.AddSingleton(sp => sp.GetRequiredService()); + // Rekor v2 tile-based client for Sunlight/tile log format + services.AddHttpClient((sp, client) => + { + var options = sp.GetRequiredService>().Value; + var timeoutMs = options.Rekor.Primary.ProofTimeoutMs; + if (timeoutMs <= 0) + { + timeoutMs = 15_000; + } + + client.Timeout = TimeSpan.FromMilliseconds(timeoutMs); + }); + services.AddSingleton(sp => sp.GetRequiredService()); + services.AddHttpClient((sp, client) => { var options = sp.GetRequiredService>().Value; diff --git a/src/Attestor/__Tests/StellaOps.Attestor.Infrastructure.Tests/HttpRekorTileClientTests.cs b/src/Attestor/__Tests/StellaOps.Attestor.Infrastructure.Tests/HttpRekorTileClientTests.cs new file mode 100644 index 000000000..96197dc5a --- /dev/null +++ b/src/Attestor/__Tests/StellaOps.Attestor.Infrastructure.Tests/HttpRekorTileClientTests.cs @@ -0,0 +1,313 @@ +using System; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.Extensions.Logging.Abstractions; +using StellaOps.Attestor.Core.Rekor; +using StellaOps.Attestor.Infrastructure.Rekor; +using StellaOps.TestKit; +using Xunit; + +namespace StellaOps.Attestor.Infrastructure.Tests; + +public sealed class HttpRekorTileClientTests +{ + [Trait("Category", TestCategories.Unit)] + [Fact] + public async Task GetCheckpointAsync_ValidCheckpoint_ParsesCorrectly() + { + // Arrange + var checkpoint = """ + rekor.sigstore.dev - 2605736670972794746 + 12345678 + rMj3G9LfM9C6Xt0qpV3pHbM2q5lPvKjS0mOmV8jXwAk= + + - rekor.sigstore.dev ABC123signature== + """; + + var client = CreateClient(new CheckpointHandler(checkpoint)); + var backend = CreateBackend(); + + // Act + var result = await client.GetCheckpointAsync(backend, CancellationToken.None); + + // Assert + result.Should().NotBeNull(); + result!.Origin.Should().Be("rekor.sigstore.dev - 2605736670972794746"); + result.TreeSize.Should().Be(12345678); + result.RootHash.Should().NotBeNullOrEmpty(); + result.Signatures.Should().HaveCount(1); + } + + [Trait("Category", TestCategories.Unit)] + [Fact] + public async Task GetCheckpointAsync_NotFound_ReturnsNull() + { + // Arrange + var client = CreateClient(new NotFoundHandler()); + var backend = CreateBackend(); + + // Act + var result = await client.GetCheckpointAsync(backend, CancellationToken.None); + + // Assert + result.Should().BeNull(); + } + + [Trait("Category", TestCategories.Unit)] + [Fact] + public async Task GetTileAsync_ValidTile_ReturnsTileData() + { + // Arrange - 256 hashes (32 bytes each) = 8192 bytes + var tileData = new byte[32 * 4]; // 4 hashes for simplicity + Random.Shared.NextBytes(tileData); + + var client = CreateClient(new TileHandler(tileData)); + var backend = CreateBackend(); + + // Act + var result = await client.GetTileAsync(backend, level: 0, index: 0, CancellationToken.None); + + // Assert + result.Should().NotBeNull(); + result!.Level.Should().Be(0); + result.Index.Should().Be(0); + result.Width.Should().Be(4); + result.Hashes.Should().Equal(tileData); + } + + [Trait("Category", TestCategories.Unit)] + [Fact] + public async Task GetTileAsync_NotFound_ReturnsNull() + { + // Arrange + var client = CreateClient(new NotFoundHandler()); + var backend = CreateBackend(); + + // Act + var result = await client.GetTileAsync(backend, level: 0, index: 999999, CancellationToken.None); + + // Assert + result.Should().BeNull(); + } + + [Trait("Category", TestCategories.Unit)] + [Fact] + public void RekorTileData_GetHash_ReturnsCorrectHash() + { + // Arrange + var hash1 = new byte[32]; + var hash2 = new byte[32]; + Random.Shared.NextBytes(hash1); + Random.Shared.NextBytes(hash2); + + var hashes = new byte[64]; + Array.Copy(hash1, 0, hashes, 0, 32); + Array.Copy(hash2, 0, hashes, 32, 32); + + var tile = new RekorTileData + { + Level = 0, + Index = 0, + Width = 2, + Hashes = hashes + }; + + // Act & Assert + tile.GetHash(0).Should().Equal(hash1); + tile.GetHash(1).Should().Equal(hash2); + } + + [Trait("Category", TestCategories.Unit)] + [Fact] + public void RekorTileData_GetHash_OutOfRange_Throws() + { + // Arrange + var tile = new RekorTileData + { + Level = 0, + Index = 0, + Width = 2, + Hashes = new byte[64] + }; + + // Act & Assert + var action = () => tile.GetHash(2); + action.Should().Throw(); + } + + [Trait("Category", TestCategories.Unit)] + [Fact] + public void RekorBackend_GetEffectiveTileBaseUrl_WithoutConfig_ReturnsDefault() + { + // Arrange + var backend = new RekorBackend + { + Name = "test", + Url = new Uri("https://rekor.sigstore.dev"), + Version = RekorLogVersion.V2 + }; + + // Act + var result = backend.GetEffectiveTileBaseUrl(); + + // Assert + result.Should().Be(new Uri("https://rekor.sigstore.dev/tile/")); + } + + [Trait("Category", TestCategories.Unit)] + [Fact] + public void RekorBackend_GetEffectiveTileBaseUrl_WithConfig_ReturnsConfigured() + { + // Arrange + var backend = new RekorBackend + { + Name = "test", + Url = new Uri("https://rekor.sigstore.dev"), + Version = RekorLogVersion.V2, + TileBaseUrl = new Uri("https://tiles.rekor.sigstore.dev/") + }; + + // Act + var result = backend.GetEffectiveTileBaseUrl(); + + // Assert + result.Should().Be(new Uri("https://tiles.rekor.sigstore.dev/")); + } + + [Trait("Category", TestCategories.Unit)] + [Theory] + [InlineData(RekorLogVersion.V2, false, true)] + [InlineData(RekorLogVersion.V1, false, false)] + [InlineData(RekorLogVersion.V1, true, false)] + [InlineData(RekorLogVersion.Auto, false, false)] + [InlineData(RekorLogVersion.Auto, true, true)] + public void ShouldUseTileProofs_ReturnsExpected(RekorLogVersion version, bool preferTiles, bool expected) + { + // Arrange + var backend = new RekorBackend + { + Name = "test", + Url = new Uri("https://rekor.sigstore.dev"), + Version = version, + PreferTileProofs = preferTiles + }; + + // Act + var result = RekorBackendResolver.ShouldUseTileProofs(backend); + + // Assert + result.Should().Be(expected); + } + + [Trait("Category", TestCategories.Unit)] + [Fact] + public async Task GetEntryAsync_NotFound_ReturnsNull() + { + // Arrange + var client = CreateClient(new NotFoundHandler()); + var backend = CreateBackend(); + + // Act + var result = await client.GetEntryAsync(backend, logIndex: 12345, CancellationToken.None); + + // Assert + result.Should().BeNull(); + } + + [Trait("Category", TestCategories.Unit)] + [Fact] + public async Task ComputeInclusionProofAsync_InvalidIndex_ReturnsNull() + { + // Arrange + var client = CreateClient(new NotFoundHandler()); + var backend = CreateBackend(); + + // Act - index >= treeSize + var result = await client.ComputeInclusionProofAsync(backend, logIndex: 100, treeSize: 50, CancellationToken.None); + + // Assert + result.Should().BeNull(); + } + + private static HttpRekorTileClient CreateClient(HttpMessageHandler handler) + { + var httpClient = new HttpClient(handler) + { + BaseAddress = new Uri("https://rekor.sigstore.dev") + }; + + return new HttpRekorTileClient(httpClient, NullLogger.Instance); + } + + private static RekorBackend CreateBackend() + { + return new RekorBackend + { + Name = "primary", + Url = new Uri("https://rekor.sigstore.dev"), + Version = RekorLogVersion.V2 + }; + } + + private sealed class CheckpointHandler : HttpMessageHandler + { + private readonly string _checkpoint; + + public CheckpointHandler(string checkpoint) + { + _checkpoint = checkpoint; + } + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + var path = request.RequestUri?.AbsolutePath ?? string.Empty; + + if (path.Contains("checkpoint", StringComparison.OrdinalIgnoreCase)) + { + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(_checkpoint, Encoding.UTF8, "text/plain") + }); + } + + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound)); + } + } + + private sealed class TileHandler : HttpMessageHandler + { + private readonly byte[] _tileData; + + public TileHandler(byte[] tileData) + { + _tileData = tileData; + } + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + var path = request.RequestUri?.AbsolutePath ?? string.Empty; + + if (path.Contains("tile/", StringComparison.OrdinalIgnoreCase) && !path.Contains("checkpoint", StringComparison.OrdinalIgnoreCase)) + { + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new ByteArrayContent(_tileData) + }); + } + + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound)); + } + } + + private sealed class NotFoundHandler : HttpMessageHandler + { + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound)); + } + } +} diff --git a/src/Attestor/__Tests/StellaOps.Attestor.Infrastructure.Tests/RekorBackendResolverTests.cs b/src/Attestor/__Tests/StellaOps.Attestor.Infrastructure.Tests/RekorBackendResolverTests.cs index ac601b9c8..72ae581e4 100644 --- a/src/Attestor/__Tests/StellaOps.Attestor.Infrastructure.Tests/RekorBackendResolverTests.cs +++ b/src/Attestor/__Tests/StellaOps.Attestor.Infrastructure.Tests/RekorBackendResolverTests.cs @@ -1,6 +1,7 @@ using System; using FluentAssertions; using StellaOps.Attestor.Core.Options; +using StellaOps.Attestor.Core.Rekor; using StellaOps.Attestor.Infrastructure.Rekor; using StellaOps.TestKit; using Xunit; @@ -35,6 +36,155 @@ public sealed class RekorBackendResolverTests backend.Url.Should().Be(new Uri("https://rekor.primary.example")); } + [Trait("Category", TestCategories.Unit)] + [Theory] + [InlineData("Auto", RekorLogVersion.Auto)] + [InlineData("auto", RekorLogVersion.Auto)] + [InlineData("V1", RekorLogVersion.V1)] + [InlineData("v1", RekorLogVersion.V1)] + [InlineData("1", RekorLogVersion.V1)] + [InlineData("V2", RekorLogVersion.V2)] + [InlineData("v2", RekorLogVersion.V2)] + [InlineData("2", RekorLogVersion.V2)] + [InlineData("", RekorLogVersion.Auto)] + [InlineData(null, RekorLogVersion.Auto)] + [InlineData("invalid", RekorLogVersion.Auto)] + public void ResolveBackend_ParsesVersionCorrectly(string? versionString, RekorLogVersion expected) + { + var options = new AttestorOptions + { + Rekor = new AttestorOptions.RekorOptions + { + Primary = new AttestorOptions.RekorBackendOptions + { + Url = "https://rekor.sigstore.dev", + Version = versionString ?? "Auto" + } + } + }; + + var backend = RekorBackendResolver.ResolveBackend(options, "primary", allowFallbackToPrimary: false); + + backend.Version.Should().Be(expected); + } + + [Trait("Category", TestCategories.Unit)] + [Fact] + public void ResolveBackend_WithTileBaseUrl_SetsProperty() + { + var options = new AttestorOptions + { + Rekor = new AttestorOptions.RekorOptions + { + Primary = new AttestorOptions.RekorBackendOptions + { + Url = "https://rekor.sigstore.dev", + Version = "V2", + TileBaseUrl = "https://rekor.sigstore.dev/tile/" + } + } + }; + + var backend = RekorBackendResolver.ResolveBackend(options, "primary", allowFallbackToPrimary: false); + + backend.Version.Should().Be(RekorLogVersion.V2); + backend.TileBaseUrl.Should().Be(new Uri("https://rekor.sigstore.dev/tile/")); + } + + [Trait("Category", TestCategories.Unit)] + [Fact] + public void ResolveBackend_WithLogId_SetsProperty() + { + var options = new AttestorOptions + { + Rekor = new AttestorOptions.RekorOptions + { + Primary = new AttestorOptions.RekorBackendOptions + { + Url = "https://rekor.sigstore.dev", + LogId = RekorBackend.SigstoreProductionLogId + } + } + }; + + var backend = RekorBackendResolver.ResolveBackend(options, "primary", allowFallbackToPrimary: false); + + backend.LogId.Should().Be(RekorBackend.SigstoreProductionLogId); + } + + [Trait("Category", TestCategories.Unit)] + [Fact] + public void ResolveBackend_WithPreferTileProofs_SetsProperty() + { + var options = new AttestorOptions + { + Rekor = new AttestorOptions.RekorOptions + { + Primary = new AttestorOptions.RekorBackendOptions + { + Url = "https://rekor.sigstore.dev", + PreferTileProofs = true + } + } + }; + + var backend = RekorBackendResolver.ResolveBackend(options, "primary", allowFallbackToPrimary: false); + + backend.PreferTileProofs.Should().BeTrue(); + } + + [Trait("Category", TestCategories.Unit)] + [Theory] + [InlineData(RekorLogVersion.V2, false, true)] + [InlineData(RekorLogVersion.V1, true, false)] + [InlineData(RekorLogVersion.Auto, true, true)] + [InlineData(RekorLogVersion.Auto, false, false)] + public void ShouldUseTileProofs_ReturnsCorrectValue(RekorLogVersion version, bool preferTileProofs, bool expected) + { + var backend = new RekorBackend + { + Name = "test", + Url = new Uri("https://rekor.sigstore.dev"), + Version = version, + PreferTileProofs = preferTileProofs + }; + + var result = RekorBackendResolver.ShouldUseTileProofs(backend); + + result.Should().Be(expected); + } + + [Trait("Category", TestCategories.Unit)] + [Fact] + public void GetEffectiveTileBaseUrl_WithoutTileBaseUrl_ReturnsDefault() + { + var backend = new RekorBackend + { + Name = "test", + Url = new Uri("https://rekor.sigstore.dev") + }; + + var result = backend.GetEffectiveTileBaseUrl(); + + result.Should().Be(new Uri("https://rekor.sigstore.dev/tile/")); + } + + [Trait("Category", TestCategories.Unit)] + [Fact] + public void GetEffectiveTileBaseUrl_WithTileBaseUrl_ReturnsConfigured() + { + var backend = new RekorBackend + { + Name = "test", + Url = new Uri("https://rekor.sigstore.dev"), + TileBaseUrl = new Uri("https://custom.tile.endpoint/v2/tile/") + }; + + var result = backend.GetEffectiveTileBaseUrl(); + + result.Should().Be(new Uri("https://custom.tile.endpoint/v2/tile/")); + } + [Trait("Category", TestCategories.Unit)] [Fact] public void ResolveBackend_UnknownBackend_ThrowsWhenFallbackDisabled() diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.SourceIntel/ChangelogParser.cs b/src/Concelier/__Libraries/StellaOps.Concelier.SourceIntel/ChangelogParser.cs index fe4949e05..0f2bed3c9 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.SourceIntel/ChangelogParser.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.SourceIntel/ChangelogParser.cs @@ -147,7 +147,7 @@ public static partial class ChangelogParser } currentDate = ParseRpmDate(headerMatch.Groups[1].Value); - currentVersion = headerMatch.Groups[2].Value; + currentVersion = headerMatch.Groups[2].Value.Trim(); currentCves.Clear(); currentBugs.Clear(); currentDescription.Clear(); diff --git a/src/Doctor/__Plugins/StellaOps.Doctor.Plugin.Notify/Checks/EmailConfiguredCheck.cs b/src/Doctor/__Plugins/StellaOps.Doctor.Plugin.Notify/Checks/EmailConfiguredCheck.cs new file mode 100644 index 000000000..9bdf77f03 --- /dev/null +++ b/src/Doctor/__Plugins/StellaOps.Doctor.Plugin.Notify/Checks/EmailConfiguredCheck.cs @@ -0,0 +1,161 @@ +using Microsoft.Extensions.Configuration; +using StellaOps.Doctor.Models; +using StellaOps.Doctor.Plugins; + +namespace StellaOps.Doctor.Plugin.Notify.Checks; + +/// +/// Checks if email (SMTP) notification channel is properly configured. +/// +public sealed class EmailConfiguredCheck : IDoctorCheck +{ + private const string PluginId = "stellaops.doctor.notify"; + private const string CategoryName = "Notifications"; + + /// + public string CheckId => "check.notify.email.configured"; + + /// + public string Name => "Email Configuration"; + + /// + public string Description => "Verify email (SMTP) notification channel is properly configured"; + + /// + public DoctorSeverity DefaultSeverity => DoctorSeverity.Warn; + + /// + public IReadOnlyList Tags => ["notify", "email", "smtp", "quick", "configuration"]; + + /// + public TimeSpan EstimatedDuration => TimeSpan.FromMilliseconds(100); + + /// + public bool CanRun(DoctorPluginContext context) + { + var emailConfig = context.Configuration.GetSection("Notify:Channels:Email"); + return emailConfig.Exists(); + } + + /// + public Task RunAsync(DoctorPluginContext context, CancellationToken ct) + { + var builder = context.CreateResult(CheckId, PluginId, CategoryName); + var emailConfig = context.Configuration.GetSection("Notify:Channels:Email"); + + var smtpHost = emailConfig["SmtpHost"] ?? emailConfig["Host"]; + var smtpPort = emailConfig.GetValue("SmtpPort") ?? emailConfig.GetValue("Port") ?? 0; + var fromAddress = emailConfig["FromAddress"] ?? emailConfig["From"]; + var enabled = emailConfig.GetValue("Enabled", true); + var useSsl = emailConfig.GetValue("UseSsl", true); + var username = emailConfig["Username"]; + + var hasHost = !string.IsNullOrWhiteSpace(smtpHost); + var hasFrom = !string.IsNullOrWhiteSpace(fromAddress); + var hasValidPort = smtpPort > 0 && smtpPort <= 65535; + + if (!hasHost) + { + return Task.FromResult(builder + .Fail("SMTP host is not configured") + .WithEvidence("Email configuration status", eb => eb + .Add("SmtpHost", "(not set)") + .Add("SmtpPort", smtpPort > 0 ? smtpPort.ToString() : "(not set)") + .Add("FromAddress", hasFrom ? fromAddress! : "(not set)") + .Add("Enabled", enabled.ToString())) + .WithCauses( + "SMTP host not set in configuration", + "Missing Notify:Channels:Email:SmtpHost setting") + .WithRemediation(rb => rb + .AddStep(1, "Add SMTP configuration", + "# Add to appsettings.json:\n" + + "# \"Notify\": { \"Channels\": { \"Email\": {\n" + + "# \"SmtpHost\": \"smtp.example.com\",\n" + + "# \"SmtpPort\": 587,\n" + + "# \"FromAddress\": \"noreply@example.com\",\n" + + "# \"UseSsl\": true\n" + + "# } } }", + CommandType.FileEdit) + .AddStep(2, "Or set via environment variables", + "export Notify__Channels__Email__SmtpHost=\"smtp.example.com\"\n" + + "export Notify__Channels__Email__SmtpPort=\"587\"\n" + + "export Notify__Channels__Email__FromAddress=\"noreply@example.com\"", + CommandType.Shell)) + .WithVerification($"stella doctor --check {CheckId}") + .Build()); + } + + if (!hasValidPort) + { + return Task.FromResult(builder + .Warn("SMTP port is not configured or invalid") + .WithEvidence("Email configuration status", eb => eb + .Add("SmtpHost", smtpHost!) + .Add("SmtpPort", smtpPort > 0 ? smtpPort.ToString() : "(not set or invalid)") + .Add("FromAddress", hasFrom ? fromAddress! : "(not set)") + .Add("Enabled", enabled.ToString()) + .Add("Note", "Common ports: 25 (unencrypted), 465 (SSL), 587 (TLS/STARTTLS)")) + .WithCauses( + "SMTP port not specified", + "Invalid port number") + .WithRemediation(rb => rb + .AddStep(1, "Set SMTP port", + "# Common SMTP ports:\n# 25 - Standard SMTP (often blocked)\n# 465 - SMTP over SSL\n# 587 - SMTP with STARTTLS (recommended)", + CommandType.Manual)) + .WithVerification($"stella doctor --check {CheckId}") + .Build()); + } + + if (!hasFrom) + { + return Task.FromResult(builder + .Warn("From address is not configured") + .WithEvidence("Email configuration status", eb => eb + .Add("SmtpHost", smtpHost!) + .Add("SmtpPort", smtpPort.ToString()) + .Add("FromAddress", "(not set)") + .Add("Enabled", enabled.ToString())) + .WithCauses( + "From address not configured", + "Emails may be rejected without a valid sender") + .WithRemediation(rb => rb + .AddStep(1, "Set from address", + "# Add Notify:Channels:Email:FromAddress to configuration", + CommandType.FileEdit)) + .WithVerification($"stella doctor --check {CheckId}") + .Build()); + } + + if (!enabled) + { + return Task.FromResult(builder + .Warn("Email channel is configured but disabled") + .WithEvidence("Email configuration status", eb => eb + .Add("SmtpHost", smtpHost!) + .Add("SmtpPort", smtpPort.ToString()) + .Add("FromAddress", fromAddress!) + .Add("Enabled", "false") + .Add("UseSsl", useSsl.ToString()) + .Add("HasCredentials", !string.IsNullOrWhiteSpace(username) ? "yes" : "no")) + .WithCauses( + "Email notifications explicitly disabled") + .WithRemediation(rb => rb + .AddStep(1, "Enable email notifications", + "# Set Notify:Channels:Email:Enabled to true", + CommandType.FileEdit)) + .WithVerification($"stella doctor --check {CheckId}") + .Build()); + } + + return Task.FromResult(builder + .Pass("Email notification channel is properly configured") + .WithEvidence("Email configuration status", eb => eb + .Add("SmtpHost", smtpHost!) + .Add("SmtpPort", smtpPort.ToString()) + .Add("FromAddress", fromAddress!) + .Add("Enabled", "true") + .Add("UseSsl", useSsl.ToString()) + .Add("HasCredentials", !string.IsNullOrWhiteSpace(username) ? "yes" : "no")) + .Build()); + } +} diff --git a/src/Doctor/__Plugins/StellaOps.Doctor.Plugin.Notify/Checks/EmailConnectivityCheck.cs b/src/Doctor/__Plugins/StellaOps.Doctor.Plugin.Notify/Checks/EmailConnectivityCheck.cs new file mode 100644 index 000000000..b00e8d8f9 --- /dev/null +++ b/src/Doctor/__Plugins/StellaOps.Doctor.Plugin.Notify/Checks/EmailConnectivityCheck.cs @@ -0,0 +1,186 @@ +using System.Globalization; +using System.Net.Sockets; +using Microsoft.Extensions.Configuration; +using StellaOps.Doctor.Models; +using StellaOps.Doctor.Plugins; + +namespace StellaOps.Doctor.Plugin.Notify.Checks; + +/// +/// Checks if the configured SMTP server is reachable. +/// +public sealed class EmailConnectivityCheck : IDoctorCheck +{ + private const string PluginId = "stellaops.doctor.notify"; + private const string CategoryName = "Notifications"; + + /// + public string CheckId => "check.notify.email.connectivity"; + + /// + public string Name => "Email Connectivity"; + + /// + public string Description => "Verify SMTP server is reachable"; + + /// + public DoctorSeverity DefaultSeverity => DoctorSeverity.Warn; + + /// + public IReadOnlyList Tags => ["notify", "email", "smtp", "connectivity", "network"]; + + /// + public TimeSpan EstimatedDuration => TimeSpan.FromSeconds(5); + + /// + public bool CanRun(DoctorPluginContext context) + { + var emailConfig = context.Configuration.GetSection("Notify:Channels:Email"); + var smtpHost = emailConfig["SmtpHost"] ?? emailConfig["Host"]; + var smtpPort = emailConfig.GetValue("SmtpPort") ?? emailConfig.GetValue("Port") ?? 0; + + return !string.IsNullOrWhiteSpace(smtpHost) && smtpPort > 0; + } + + /// + public async Task RunAsync(DoctorPluginContext context, CancellationToken ct) + { + var emailConfig = context.Configuration.GetSection("Notify:Channels:Email"); + var smtpHost = emailConfig["SmtpHost"] ?? emailConfig["Host"]!; + var smtpPort = emailConfig.GetValue("SmtpPort") ?? emailConfig.GetValue("Port") ?? 587; + + var builder = context.CreateResult(CheckId, PluginId, CategoryName); + + try + { + using var tcpClient = new TcpClient(); + + // Set connection timeout + using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(ct); + timeoutCts.CancelAfter(TimeSpan.FromSeconds(10)); + + await tcpClient.ConnectAsync(smtpHost, smtpPort, timeoutCts.Token); + + if (tcpClient.Connected) + { + // Try to read the SMTP banner + using var stream = tcpClient.GetStream(); + stream.ReadTimeout = 5000; + + var buffer = new byte[1024]; + string? banner = null; + + try + { + var bytesRead = await stream.ReadAsync(buffer, timeoutCts.Token); + if (bytesRead > 0) + { + banner = System.Text.Encoding.ASCII.GetString(buffer, 0, bytesRead).Trim(); + } + } + catch + { + // Banner read failed, but connection succeeded + } + + var isSmtp = banner?.StartsWith("220", StringComparison.Ordinal) == true; + + if (isSmtp) + { + return builder + .Pass("SMTP server is reachable and responding") + .WithEvidence("SMTP connectivity test", eb => eb + .Add("SmtpHost", smtpHost) + .Add("SmtpPort", smtpPort.ToString(CultureInfo.InvariantCulture)) + .Add("Banner", banner?.Length > 100 ? banner[..100] + "..." : banner ?? "(none)")) + .Build(); + } + + return builder + .Info("Connection to SMTP port succeeded but banner not recognized") + .WithEvidence("SMTP connectivity test", eb => eb + .Add("SmtpHost", smtpHost) + .Add("SmtpPort", smtpPort.ToString(CultureInfo.InvariantCulture)) + .Add("Banner", banner ?? "(none)") + .Add("Note", "Connection succeeded but response doesn't look like SMTP")) + .Build(); + } + + return builder + .Fail("Failed to connect to SMTP server") + .WithEvidence("SMTP connectivity test", eb => eb + .Add("SmtpHost", smtpHost) + .Add("SmtpPort", smtpPort.ToString(CultureInfo.InvariantCulture))) + .WithCauses( + "SMTP server not running", + "Wrong host or port", + "Firewall blocking connection") + .WithRemediation(rb => rb + .AddStep(1, "Test port connectivity", + $"nc -zv {smtpHost} {smtpPort}", + CommandType.Shell) + .AddStep(2, "Test with telnet", + $"telnet {smtpHost} {smtpPort}", + CommandType.Shell)) + .WithVerification($"stella doctor --check {CheckId}") + .Build(); + } + catch (OperationCanceledException) when (ct.IsCancellationRequested) + { + throw; + } + catch (OperationCanceledException) + { + return builder + .Fail("SMTP connection timed out") + .WithEvidence("SMTP connectivity test", eb => eb + .Add("SmtpHost", smtpHost) + .Add("SmtpPort", smtpPort.ToString(CultureInfo.InvariantCulture)) + .Add("Error", "Connection timeout (10s)")) + .WithCauses( + "SMTP server not responding", + "Network latency too high", + "Firewall blocking connection", + "Wrong host or port") + .WithRemediation(rb => rb + .AddStep(1, "Test DNS resolution", + $"nslookup {smtpHost}", + CommandType.Shell) + .AddStep(2, "Test port connectivity", + $"nc -zv -w 10 {smtpHost} {smtpPort}", + CommandType.Shell) + .AddStep(3, "Check firewall rules", + "# Ensure outbound connections to SMTP ports are allowed", + CommandType.Manual)) + .WithVerification($"stella doctor --check {CheckId}") + .Build(); + } + catch (SocketException ex) + { + return builder + .Fail($"Cannot connect to SMTP server: {ex.Message}") + .WithEvidence("SMTP connectivity test", eb => eb + .Add("SmtpHost", smtpHost) + .Add("SmtpPort", smtpPort.ToString(CultureInfo.InvariantCulture)) + .Add("SocketError", ex.SocketErrorCode.ToString()) + .Add("Error", ex.Message)) + .WithCauses( + "DNS resolution failure", + "SMTP server not running on specified port", + "Network connectivity issue", + "Firewall blocking connection") + .WithRemediation(rb => rb + .AddStep(1, "Test DNS resolution", + $"nslookup {smtpHost}", + CommandType.Shell) + .AddStep(2, "Test port connectivity", + $"nc -zv {smtpHost} {smtpPort}", + CommandType.Shell) + .AddStep(3, "Verify SMTP host and port settings", + "# Common SMTP ports: 25, 465 (SSL), 587 (STARTTLS)", + CommandType.Manual)) + .WithVerification($"stella doctor --check {CheckId}") + .Build(); + } + } +} diff --git a/src/Doctor/__Plugins/StellaOps.Doctor.Plugin.Notify/Checks/NotifyQueueHealthCheck.cs b/src/Doctor/__Plugins/StellaOps.Doctor.Plugin.Notify/Checks/NotifyQueueHealthCheck.cs new file mode 100644 index 000000000..a7e3756a6 --- /dev/null +++ b/src/Doctor/__Plugins/StellaOps.Doctor.Plugin.Notify/Checks/NotifyQueueHealthCheck.cs @@ -0,0 +1,232 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using StellaOps.Doctor.Models; +using StellaOps.Doctor.Plugins; + +namespace StellaOps.Doctor.Plugin.Notify.Checks; + +/// +/// Checks if the notification queue (Redis or NATS) is healthy. +/// +public sealed class NotifyQueueHealthCheck : IDoctorCheck +{ + private const string PluginId = "stellaops.doctor.notify"; + private const string CategoryName = "Notifications"; + + /// + public string CheckId => "check.notify.queue.health"; + + /// + public string Name => "Notification Queue Health"; + + /// + public string Description => "Verify notification event and delivery queues are healthy"; + + /// + public DoctorSeverity DefaultSeverity => DoctorSeverity.Fail; + + /// + public IReadOnlyList Tags => ["notify", "queue", "redis", "nats", "infrastructure"]; + + /// + public TimeSpan EstimatedDuration => TimeSpan.FromSeconds(5); + + /// + public bool CanRun(DoctorPluginContext context) + { + // Check if any queue configuration exists + var queueConfig = context.Configuration.GetSection("Notify:Queue"); + var transportKind = queueConfig["Transport"] ?? queueConfig["Kind"]; + return !string.IsNullOrWhiteSpace(transportKind); + } + + /// + public async Task RunAsync(DoctorPluginContext context, CancellationToken ct) + { + var builder = context.CreateResult(CheckId, PluginId, CategoryName); + var queueConfig = context.Configuration.GetSection("Notify:Queue"); + var transportKind = queueConfig["Transport"] ?? queueConfig["Kind"] ?? "unknown"; + + // Try to get the event queue health check from DI + var eventQueueHealthCheck = context.Services.GetService(); + var deliveryQueueHealthCheck = context.Services.GetService(); + + if (eventQueueHealthCheck == null && deliveryQueueHealthCheck == null) + { + return builder + .Skip("No notification queue health checks registered") + .WithEvidence("Queue health check status", eb => eb + .Add("Transport", transportKind) + .Add("EventQueueHealthCheck", "not registered") + .Add("DeliveryQueueHealthCheck", "not registered")) + .Build(); + } + + var results = new List<(string Name, HealthCheckResult Result)>(); + + // Check event queue + if (eventQueueHealthCheck != null) + { + try + { + var eventContext = new HealthCheckContext + { + Registration = new HealthCheckRegistration( + "notify-event-queue", + eventQueueHealthCheck, + HealthStatus.Unhealthy, + null) + }; + + var eventResult = await eventQueueHealthCheck.CheckHealthAsync(eventContext, ct); + results.Add(("EventQueue", eventResult)); + } + catch (Exception ex) + { + results.Add(("EventQueue", new HealthCheckResult( + HealthStatus.Unhealthy, + "Event queue health check threw exception", + ex))); + } + } + + // Check delivery queue + if (deliveryQueueHealthCheck != null) + { + try + { + var deliveryContext = new HealthCheckContext + { + Registration = new HealthCheckRegistration( + "notify-delivery-queue", + deliveryQueueHealthCheck, + HealthStatus.Unhealthy, + null) + }; + + var deliveryResult = await deliveryQueueHealthCheck.CheckHealthAsync(deliveryContext, ct); + results.Add(("DeliveryQueue", deliveryResult)); + } + catch (Exception ex) + { + results.Add(("DeliveryQueue", new HealthCheckResult( + HealthStatus.Unhealthy, + "Delivery queue health check threw exception", + ex))); + } + } + + // Aggregate results + var allHealthy = results.All(r => r.Result.Status == HealthStatus.Healthy); + var anyUnhealthy = results.Any(r => r.Result.Status == HealthStatus.Unhealthy); + + if (allHealthy) + { + return builder + .Pass($"Notification queue ({transportKind}) is healthy") + .WithEvidence("Queue health check results", eb => + { + eb.Add("Transport", transportKind); + foreach (var (name, result) in results) + { + eb.Add($"{name}Status", result.Status.ToString()); + if (!string.IsNullOrEmpty(result.Description)) + { + eb.Add($"{name}Message", result.Description); + } + } + }) + .Build(); + } + + if (anyUnhealthy) + { + var unhealthyQueues = results + .Where(r => r.Result.Status == HealthStatus.Unhealthy) + .Select(r => r.Name) + .ToList(); + + return builder + .Fail($"Notification queue unhealthy: {string.Join(", ", unhealthyQueues)}") + .WithEvidence("Queue health check results", eb => + { + eb.Add("Transport", transportKind); + foreach (var (name, result) in results) + { + eb.Add($"{name}Status", result.Status.ToString()); + if (!string.IsNullOrEmpty(result.Description)) + { + eb.Add($"{name}Message", result.Description); + } + } + }) + .WithCauses( + "Queue server not running", + "Network connectivity issues", + "Authentication failure", + "Incorrect connection string") + .WithRemediation(rb => + { + if (transportKind.Equals("redis", StringComparison.OrdinalIgnoreCase) || + transportKind.Equals("valkey", StringComparison.OrdinalIgnoreCase)) + { + rb.AddStep(1, "Check Redis/Valkey server status", + "redis-cli ping", + CommandType.Shell) + .AddStep(2, "Verify Redis connection settings", + "# Check Notify:Queue:Redis:ConnectionString in configuration", + CommandType.Manual) + .AddStep(3, "Check Redis server logs", + "docker logs ", + CommandType.Shell); + } + else if (transportKind.Equals("nats", StringComparison.OrdinalIgnoreCase)) + { + rb.AddStep(1, "Check NATS server status", + "nats server ping", + CommandType.Shell) + .AddStep(2, "Verify NATS connection settings", + "# Check Notify:Queue:Nats:Url in configuration", + CommandType.Manual) + .AddStep(3, "Check NATS server logs", + "docker logs ", + CommandType.Shell); + } + else + { + rb.AddStep(1, "Verify queue transport configuration", + "# Check Notify:Queue:Transport setting", + CommandType.Manual); + } + }) + .WithVerification($"stella doctor --check {CheckId}") + .Build(); + } + + // Degraded state + return builder + .Warn("Notification queue in degraded state") + .WithEvidence("Queue health check results", eb => + { + eb.Add("Transport", transportKind); + foreach (var (name, result) in results) + { + eb.Add($"{name}Status", result.Status.ToString()); + if (!string.IsNullOrEmpty(result.Description)) + { + eb.Add($"{name}Message", result.Description); + } + } + }) + .WithCauses( + "Queue server experiencing issues", + "High latency", + "Resource constraints") + .WithRemediation(rb => rb + .AddStep(1, "Check queue server health", + "# Review queue server metrics and logs", + CommandType.Manual)) + .WithVerification($"stella doctor --check {CheckId}") + .Build(); + } +} diff --git a/src/Doctor/__Plugins/StellaOps.Doctor.Plugin.Notify/Checks/SlackConfiguredCheck.cs b/src/Doctor/__Plugins/StellaOps.Doctor.Plugin.Notify/Checks/SlackConfiguredCheck.cs new file mode 100644 index 000000000..2433ed469 --- /dev/null +++ b/src/Doctor/__Plugins/StellaOps.Doctor.Plugin.Notify/Checks/SlackConfiguredCheck.cs @@ -0,0 +1,109 @@ +using System.Globalization; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using StellaOps.Doctor.Models; +using StellaOps.Doctor.Plugins; +using StellaOps.Notify.Engine; +using StellaOps.Notify.Models; + +namespace StellaOps.Doctor.Plugin.Notify.Checks; + +/// +/// Checks if Slack notification channels are properly configured. +/// +public sealed class SlackConfiguredCheck : IDoctorCheck +{ + private const string PluginId = "stellaops.doctor.notify"; + private const string CategoryName = "Notifications"; + + /// + public string CheckId => "check.notify.slack.configured"; + + /// + public string Name => "Slack Configuration"; + + /// + public string Description => "Verify Slack notification channel is properly configured"; + + /// + public DoctorSeverity DefaultSeverity => DoctorSeverity.Warn; + + /// + public IReadOnlyList Tags => ["notify", "slack", "quick", "configuration"]; + + /// + public TimeSpan EstimatedDuration => TimeSpan.FromMilliseconds(100); + + /// + public bool CanRun(DoctorPluginContext context) + { + // Check if Slack is configured in settings + var slackConfig = context.Configuration.GetSection("Notify:Channels:Slack"); + return slackConfig.Exists(); + } + + /// + public Task RunAsync(DoctorPluginContext context, CancellationToken ct) + { + var builder = context.CreateResult(CheckId, PluginId, CategoryName); + var slackConfig = context.Configuration.GetSection("Notify:Channels:Slack"); + + var webhookUrl = slackConfig["WebhookUrl"]; + var channel = slackConfig["Channel"]; + var enabled = slackConfig.GetValue("Enabled", true); + + var hasWebhook = !string.IsNullOrWhiteSpace(webhookUrl); + var hasChannel = !string.IsNullOrWhiteSpace(channel); + + if (!hasWebhook) + { + return Task.FromResult(builder + .Fail("Slack webhook URL is not configured") + .WithEvidence("Slack configuration status", eb => eb + .Add("WebhookUrl", "(not set)") + .Add("Channel", hasChannel ? channel! : "(not set)") + .Add("Enabled", enabled.ToString())) + .WithCauses( + "Slack webhook URL not set in configuration", + "Missing Notify:Channels:Slack:WebhookUrl setting", + "Environment variable not bound to configuration") + .WithRemediation(rb => rb + .AddStep(1, "Add Slack webhook URL to configuration", + "# Add to appsettings.json or environment:\n" + + "# \"Notify\": { \"Channels\": { \"Slack\": { \"WebhookUrl\": \"https://hooks.slack.com/services/...\" } } }", + CommandType.FileEdit) + .AddStep(2, "Or set via environment variable", + "export Notify__Channels__Slack__WebhookUrl=\"https://hooks.slack.com/services/YOUR/WEBHOOK/URL\"", + CommandType.Shell) + .WithSafetyNote("Slack webhook URLs are secrets - store in a secrets manager")) + .WithVerification($"stella doctor --check {CheckId}") + .Build()); + } + + if (!enabled) + { + return Task.FromResult(builder + .Warn("Slack channel is configured but disabled") + .WithEvidence("Slack configuration status", eb => eb + .Add("WebhookUrl", DoctorPluginContext.Redact(webhookUrl)) + .Add("Channel", hasChannel ? channel! : "(default)") + .Add("Enabled", "false")) + .WithCauses( + "Slack notifications explicitly disabled in configuration") + .WithRemediation(rb => rb + .AddStep(1, "Enable Slack notifications", + "# Set Notify:Channels:Slack:Enabled to true in configuration", + CommandType.FileEdit)) + .WithVerification($"stella doctor --check {CheckId}") + .Build()); + } + + return Task.FromResult(builder + .Pass("Slack notification channel is properly configured") + .WithEvidence("Slack configuration status", eb => eb + .Add("WebhookUrl", DoctorPluginContext.Redact(webhookUrl)) + .Add("Channel", hasChannel ? channel! : "(default)") + .Add("Enabled", "true")) + .Build()); + } +} diff --git a/src/Doctor/__Plugins/StellaOps.Doctor.Plugin.Notify/Checks/SlackConnectivityCheck.cs b/src/Doctor/__Plugins/StellaOps.Doctor.Plugin.Notify/Checks/SlackConnectivityCheck.cs new file mode 100644 index 000000000..b7c21e382 --- /dev/null +++ b/src/Doctor/__Plugins/StellaOps.Doctor.Plugin.Notify/Checks/SlackConnectivityCheck.cs @@ -0,0 +1,153 @@ +using System.Globalization; +using System.Net.Http; +using System.Text; +using System.Text.Json; +using Microsoft.Extensions.DependencyInjection; +using StellaOps.Doctor.Models; +using StellaOps.Doctor.Plugins; + +namespace StellaOps.Doctor.Plugin.Notify.Checks; + +/// +/// Checks if the configured Slack webhook endpoint is reachable. +/// +public sealed class SlackConnectivityCheck : IDoctorCheck +{ + private const string PluginId = "stellaops.doctor.notify"; + private const string CategoryName = "Notifications"; + + /// + public string CheckId => "check.notify.slack.connectivity"; + + /// + public string Name => "Slack Connectivity"; + + /// + public string Description => "Verify Slack webhook endpoint is reachable"; + + /// + public DoctorSeverity DefaultSeverity => DoctorSeverity.Warn; + + /// + public IReadOnlyList Tags => ["notify", "slack", "connectivity", "network"]; + + /// + public TimeSpan EstimatedDuration => TimeSpan.FromSeconds(5); + + /// + public bool CanRun(DoctorPluginContext context) + { + var webhookUrl = context.Configuration["Notify:Channels:Slack:WebhookUrl"]; + return !string.IsNullOrWhiteSpace(webhookUrl) && + Uri.TryCreate(webhookUrl, UriKind.Absolute, out _); + } + + /// + public async Task RunAsync(DoctorPluginContext context, CancellationToken ct) + { + var webhookUrl = context.Configuration["Notify:Channels:Slack:WebhookUrl"]!; + var builder = context.CreateResult(CheckId, PluginId, CategoryName); + + try + { + var httpClientFactory = context.Services.GetRequiredService(); + var httpClient = httpClientFactory.CreateClient("DoctorHealthCheck"); + httpClient.Timeout = TimeSpan.FromSeconds(10); + + // Send a minimal test payload to Slack + // Note: This won't actually post a message if the payload is invalid, + // but it will verify the endpoint is reachable and responds + var testPayload = new { text = "" }; // Empty text won't post but validates endpoint + var content = new StringContent( + JsonSerializer.Serialize(testPayload), + Encoding.UTF8, + "application/json"); + + var response = await httpClient.PostAsync(webhookUrl, content, ct); + var responseBody = await response.Content.ReadAsStringAsync(ct); + + // Slack returns "no_text" for empty messages, which proves connectivity + if (response.IsSuccessStatusCode || responseBody.Contains("no_text", StringComparison.OrdinalIgnoreCase)) + { + return builder + .Pass("Slack webhook endpoint is reachable") + .WithEvidence("Slack connectivity test", eb => eb + .Add("WebhookUrl", DoctorPluginContext.Redact(webhookUrl)) + .Add("StatusCode", ((int)response.StatusCode).ToString(CultureInfo.InvariantCulture)) + .Add("Response", responseBody.Length > 100 ? responseBody[..100] + "..." : responseBody)) + .Build(); + } + + return builder + .Warn($"Slack webhook returned unexpected response: {response.StatusCode}") + .WithEvidence("Slack connectivity test", eb => eb + .Add("WebhookUrl", DoctorPluginContext.Redact(webhookUrl)) + .Add("StatusCode", ((int)response.StatusCode).ToString(CultureInfo.InvariantCulture)) + .Add("Response", responseBody.Length > 200 ? responseBody[..200] + "..." : responseBody)) + .WithCauses( + "Invalid or expired webhook URL", + "Slack workspace configuration changed", + "Webhook URL revoked or regenerated", + "Rate limiting by Slack") + .WithRemediation(rb => rb + .AddStep(1, "Verify webhook URL in Slack App settings", + "# Go to https://api.slack.com/apps -> Your App -> Incoming Webhooks", + CommandType.Manual) + .AddStep(2, "Test webhook manually", + $"curl -X POST -H 'Content-type: application/json' --data '{{\"text\":\"Doctor test\"}}' '{DoctorPluginContext.Redact(webhookUrl)}'", + CommandType.Shell) + .AddStep(3, "Regenerate webhook if needed", + "# Create a new webhook URL in Slack and update configuration", + CommandType.Manual)) + .WithVerification($"stella doctor --check {CheckId}") + .Build(); + } + catch (TaskCanceledException) + { + return builder + .Fail("Slack webhook connection timed out") + .WithEvidence("Slack connectivity test", eb => eb + .Add("WebhookUrl", DoctorPluginContext.Redact(webhookUrl)) + .Add("Error", "Connection timeout (10s)")) + .WithCauses( + "Network connectivity issue to Slack", + "Firewall blocking outbound HTTPS", + "Proxy configuration required", + "Slack service degradation") + .WithRemediation(rb => rb + .AddStep(1, "Check network connectivity", + "curl -v https://hooks.slack.com/", + CommandType.Shell) + .AddStep(2, "Check Slack status", + "# Visit https://status.slack.com for service status", + CommandType.Manual) + .AddStep(3, "Verify proxy settings if applicable", + "echo $HTTP_PROXY $HTTPS_PROXY", + CommandType.Shell)) + .WithVerification($"stella doctor --check {CheckId}") + .Build(); + } + catch (HttpRequestException ex) + { + return builder + .Fail($"Cannot reach Slack webhook: {ex.Message}") + .WithEvidence("Slack connectivity test", eb => eb + .Add("WebhookUrl", DoctorPluginContext.Redact(webhookUrl)) + .Add("Error", ex.Message)) + .WithCauses( + "DNS resolution failure", + "Network connectivity issue", + "TLS/SSL certificate problem", + "Firewall blocking connection") + .WithRemediation(rb => rb + .AddStep(1, "Test DNS resolution", + "nslookup hooks.slack.com", + CommandType.Shell) + .AddStep(2, "Test HTTPS connectivity", + "curl -v https://hooks.slack.com/", + CommandType.Shell)) + .WithVerification($"stella doctor --check {CheckId}") + .Build(); + } + } +} diff --git a/src/Doctor/__Plugins/StellaOps.Doctor.Plugin.Notify/Checks/TeamsConfiguredCheck.cs b/src/Doctor/__Plugins/StellaOps.Doctor.Plugin.Notify/Checks/TeamsConfiguredCheck.cs new file mode 100644 index 000000000..3822f1661 --- /dev/null +++ b/src/Doctor/__Plugins/StellaOps.Doctor.Plugin.Notify/Checks/TeamsConfiguredCheck.cs @@ -0,0 +1,125 @@ +using Microsoft.Extensions.Configuration; +using StellaOps.Doctor.Models; +using StellaOps.Doctor.Plugins; + +namespace StellaOps.Doctor.Plugin.Notify.Checks; + +/// +/// Checks if Microsoft Teams notification channels are properly configured. +/// +public sealed class TeamsConfiguredCheck : IDoctorCheck +{ + private const string PluginId = "stellaops.doctor.notify"; + private const string CategoryName = "Notifications"; + + /// + public string CheckId => "check.notify.teams.configured"; + + /// + public string Name => "Teams Configuration"; + + /// + public string Description => "Verify Microsoft Teams notification channel is properly configured"; + + /// + public DoctorSeverity DefaultSeverity => DoctorSeverity.Warn; + + /// + public IReadOnlyList Tags => ["notify", "teams", "quick", "configuration"]; + + /// + public TimeSpan EstimatedDuration => TimeSpan.FromMilliseconds(100); + + /// + public bool CanRun(DoctorPluginContext context) + { + var teamsConfig = context.Configuration.GetSection("Notify:Channels:Teams"); + return teamsConfig.Exists(); + } + + /// + public Task RunAsync(DoctorPluginContext context, CancellationToken ct) + { + var builder = context.CreateResult(CheckId, PluginId, CategoryName); + var teamsConfig = context.Configuration.GetSection("Notify:Channels:Teams"); + + var webhookUrl = teamsConfig["WebhookUrl"]; + var enabled = teamsConfig.GetValue("Enabled", true); + + var hasWebhook = !string.IsNullOrWhiteSpace(webhookUrl); + var isValidUrl = hasWebhook && Uri.TryCreate(webhookUrl, UriKind.Absolute, out var uri) && + (uri.Host.Contains("webhook.office.com", StringComparison.OrdinalIgnoreCase) || + uri.Host.Contains("microsoft.com", StringComparison.OrdinalIgnoreCase)); + + if (!hasWebhook) + { + return Task.FromResult(builder + .Fail("Teams webhook URL is not configured") + .WithEvidence("Teams configuration status", eb => eb + .Add("WebhookUrl", "(not set)") + .Add("Enabled", enabled.ToString())) + .WithCauses( + "Teams webhook URL not set in configuration", + "Missing Notify:Channels:Teams:WebhookUrl setting", + "Environment variable not bound to configuration") + .WithRemediation(rb => rb + .AddStep(1, "Create Teams Incoming Webhook", + "# In Teams: Channel > Connectors > Incoming Webhook > Create", + CommandType.Manual) + .AddStep(2, "Add webhook URL to configuration", + "# Add to appsettings.json:\n" + + "# \"Notify\": { \"Channels\": { \"Teams\": { \"WebhookUrl\": \"https://...webhook.office.com/...\" } } }", + CommandType.FileEdit) + .AddStep(3, "Or set via environment variable", + "export Notify__Channels__Teams__WebhookUrl=\"https://YOUR_WEBHOOK_URL\"", + CommandType.Shell) + .WithSafetyNote("Teams webhook URLs are secrets - store securely")) + .WithVerification($"stella doctor --check {CheckId}") + .Build()); + } + + if (!isValidUrl) + { + return Task.FromResult(builder + .Warn("Teams webhook URL format appears invalid") + .WithEvidence("Teams configuration status", eb => eb + .Add("WebhookUrl", DoctorPluginContext.Redact(webhookUrl)) + .Add("Enabled", enabled.ToString()) + .Add("ValidationNote", "Expected webhook.office.com or microsoft.com domain")) + .WithCauses( + "Webhook URL is not from Microsoft domain", + "Malformed URL in configuration", + "Legacy webhook URL format") + .WithRemediation(rb => rb + .AddStep(1, "Verify webhook URL", + "# Teams webhook URLs typically look like:\n# https://YOUR_TENANT.webhook.office.com/webhookb2/...", + CommandType.Manual)) + .WithVerification($"stella doctor --check {CheckId}") + .Build()); + } + + if (!enabled) + { + return Task.FromResult(builder + .Warn("Teams channel is configured but disabled") + .WithEvidence("Teams configuration status", eb => eb + .Add("WebhookUrl", DoctorPluginContext.Redact(webhookUrl)) + .Add("Enabled", "false")) + .WithCauses( + "Teams notifications explicitly disabled in configuration") + .WithRemediation(rb => rb + .AddStep(1, "Enable Teams notifications", + "# Set Notify:Channels:Teams:Enabled to true in configuration", + CommandType.FileEdit)) + .WithVerification($"stella doctor --check {CheckId}") + .Build()); + } + + return Task.FromResult(builder + .Pass("Teams notification channel is properly configured") + .WithEvidence("Teams configuration status", eb => eb + .Add("WebhookUrl", DoctorPluginContext.Redact(webhookUrl)) + .Add("Enabled", "true")) + .Build()); + } +} diff --git a/src/Doctor/__Plugins/StellaOps.Doctor.Plugin.Notify/Checks/TeamsConnectivityCheck.cs b/src/Doctor/__Plugins/StellaOps.Doctor.Plugin.Notify/Checks/TeamsConnectivityCheck.cs new file mode 100644 index 000000000..23a334c98 --- /dev/null +++ b/src/Doctor/__Plugins/StellaOps.Doctor.Plugin.Notify/Checks/TeamsConnectivityCheck.cs @@ -0,0 +1,169 @@ +using System.Globalization; +using System.Net.Http; +using System.Text; +using System.Text.Json; +using Microsoft.Extensions.DependencyInjection; +using StellaOps.Doctor.Models; +using StellaOps.Doctor.Plugins; + +namespace StellaOps.Doctor.Plugin.Notify.Checks; + +/// +/// Checks if the configured Microsoft Teams webhook endpoint is reachable. +/// +public sealed class TeamsConnectivityCheck : IDoctorCheck +{ + private const string PluginId = "stellaops.doctor.notify"; + private const string CategoryName = "Notifications"; + + /// + public string CheckId => "check.notify.teams.connectivity"; + + /// + public string Name => "Teams Connectivity"; + + /// + public string Description => "Verify Microsoft Teams webhook endpoint is reachable"; + + /// + public DoctorSeverity DefaultSeverity => DoctorSeverity.Warn; + + /// + public IReadOnlyList Tags => ["notify", "teams", "connectivity", "network"]; + + /// + public TimeSpan EstimatedDuration => TimeSpan.FromSeconds(5); + + /// + public bool CanRun(DoctorPluginContext context) + { + var webhookUrl = context.Configuration["Notify:Channels:Teams:WebhookUrl"]; + return !string.IsNullOrWhiteSpace(webhookUrl) && + Uri.TryCreate(webhookUrl, UriKind.Absolute, out _); + } + + /// + public async Task RunAsync(DoctorPluginContext context, CancellationToken ct) + { + var webhookUrl = context.Configuration["Notify:Channels:Teams:WebhookUrl"]!; + var builder = context.CreateResult(CheckId, PluginId, CategoryName); + + try + { + var httpClientFactory = context.Services.GetRequiredService(); + var httpClient = httpClientFactory.CreateClient("DoctorHealthCheck"); + httpClient.Timeout = TimeSpan.FromSeconds(10); + + // Teams Adaptive Card format for connectivity test + // Using a minimal card that validates the endpoint + var testPayload = new + { + type = "message", + attachments = new[] + { + new + { + contentType = "application/vnd.microsoft.card.adaptive", + contentUrl = (string?)null, + content = new + { + type = "AdaptiveCard", + body = Array.Empty(), + version = "1.0" + } + } + } + }; + + var content = new StringContent( + JsonSerializer.Serialize(testPayload), + Encoding.UTF8, + "application/json"); + + var response = await httpClient.PostAsync(webhookUrl, content, ct); + var responseBody = await response.Content.ReadAsStringAsync(ct); + + if (response.IsSuccessStatusCode) + { + return builder + .Pass("Teams webhook endpoint is reachable") + .WithEvidence("Teams connectivity test", eb => eb + .Add("WebhookUrl", DoctorPluginContext.Redact(webhookUrl)) + .Add("StatusCode", ((int)response.StatusCode).ToString(CultureInfo.InvariantCulture)) + .Add("Response", responseBody.Length > 100 ? responseBody[..100] + "..." : responseBody)) + .Build(); + } + + return builder + .Warn($"Teams webhook returned unexpected response: {response.StatusCode}") + .WithEvidence("Teams connectivity test", eb => eb + .Add("WebhookUrl", DoctorPluginContext.Redact(webhookUrl)) + .Add("StatusCode", ((int)response.StatusCode).ToString(CultureInfo.InvariantCulture)) + .Add("Response", responseBody.Length > 200 ? responseBody[..200] + "..." : responseBody)) + .WithCauses( + "Invalid or expired webhook URL", + "Teams connector disabled or deleted", + "Webhook URL revoked", + "Microsoft 365 tenant configuration changed") + .WithRemediation(rb => rb + .AddStep(1, "Verify webhook in Teams", + "# Go to Teams channel > Connectors > Configured > Incoming Webhook", + CommandType.Manual) + .AddStep(2, "Test webhook manually", + $"curl -H 'Content-Type: application/json' -d '{{\"text\":\"Doctor test\"}}' '{DoctorPluginContext.Redact(webhookUrl)}'", + CommandType.Shell) + .AddStep(3, "Recreate webhook if needed", + "# Delete and recreate the Incoming Webhook connector in Teams", + CommandType.Manual)) + .WithVerification($"stella doctor --check {CheckId}") + .Build(); + } + catch (TaskCanceledException) + { + return builder + .Fail("Teams webhook connection timed out") + .WithEvidence("Teams connectivity test", eb => eb + .Add("WebhookUrl", DoctorPluginContext.Redact(webhookUrl)) + .Add("Error", "Connection timeout (10s)")) + .WithCauses( + "Network connectivity issue to Microsoft", + "Firewall blocking outbound HTTPS", + "Proxy configuration required", + "Microsoft 365 service degradation") + .WithRemediation(rb => rb + .AddStep(1, "Check network connectivity", + "curl -v https://webhook.office.com/", + CommandType.Shell) + .AddStep(2, "Check Microsoft 365 status", + "# Visit https://status.office.com for service status", + CommandType.Manual) + .AddStep(3, "Verify proxy settings if applicable", + "echo $HTTP_PROXY $HTTPS_PROXY", + CommandType.Shell)) + .WithVerification($"stella doctor --check {CheckId}") + .Build(); + } + catch (HttpRequestException ex) + { + return builder + .Fail($"Cannot reach Teams webhook: {ex.Message}") + .WithEvidence("Teams connectivity test", eb => eb + .Add("WebhookUrl", DoctorPluginContext.Redact(webhookUrl)) + .Add("Error", ex.Message)) + .WithCauses( + "DNS resolution failure", + "Network connectivity issue", + "TLS/SSL certificate problem", + "Firewall blocking connection") + .WithRemediation(rb => rb + .AddStep(1, "Test DNS resolution", + "nslookup webhook.office.com", + CommandType.Shell) + .AddStep(2, "Test HTTPS connectivity", + "curl -v https://webhook.office.com/", + CommandType.Shell)) + .WithVerification($"stella doctor --check {CheckId}") + .Build(); + } + } +} diff --git a/src/Doctor/__Plugins/StellaOps.Doctor.Plugin.Notify/Checks/WebhookConfiguredCheck.cs b/src/Doctor/__Plugins/StellaOps.Doctor.Plugin.Notify/Checks/WebhookConfiguredCheck.cs new file mode 100644 index 000000000..2b5774cac --- /dev/null +++ b/src/Doctor/__Plugins/StellaOps.Doctor.Plugin.Notify/Checks/WebhookConfiguredCheck.cs @@ -0,0 +1,128 @@ +using Microsoft.Extensions.Configuration; +using StellaOps.Doctor.Models; +using StellaOps.Doctor.Plugins; + +namespace StellaOps.Doctor.Plugin.Notify.Checks; + +/// +/// Checks if generic webhook notification channels are properly configured. +/// +public sealed class WebhookConfiguredCheck : IDoctorCheck +{ + private const string PluginId = "stellaops.doctor.notify"; + private const string CategoryName = "Notifications"; + + /// + public string CheckId => "check.notify.webhook.configured"; + + /// + public string Name => "Webhook Configuration"; + + /// + public string Description => "Verify generic webhook notification channel is properly configured"; + + /// + public DoctorSeverity DefaultSeverity => DoctorSeverity.Warn; + + /// + public IReadOnlyList Tags => ["notify", "webhook", "quick", "configuration"]; + + /// + public TimeSpan EstimatedDuration => TimeSpan.FromMilliseconds(100); + + /// + public bool CanRun(DoctorPluginContext context) + { + var webhookConfig = context.Configuration.GetSection("Notify:Channels:Webhook"); + return webhookConfig.Exists(); + } + + /// + public Task RunAsync(DoctorPluginContext context, CancellationToken ct) + { + var builder = context.CreateResult(CheckId, PluginId, CategoryName); + var webhookConfig = context.Configuration.GetSection("Notify:Channels:Webhook"); + + var url = webhookConfig["Url"] ?? webhookConfig["Endpoint"]; + var enabled = webhookConfig.GetValue("Enabled", true); + var method = webhookConfig["Method"] ?? "POST"; + var contentType = webhookConfig["ContentType"] ?? "application/json"; + + var hasUrl = !string.IsNullOrWhiteSpace(url); + var isValidUrl = hasUrl && Uri.TryCreate(url, UriKind.Absolute, out var uri) && + (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps); + + if (!hasUrl) + { + return Task.FromResult(builder + .Fail("Webhook URL is not configured") + .WithEvidence("Webhook configuration status", eb => eb + .Add("Url", "(not set)") + .Add("Enabled", enabled.ToString()) + .Add("Method", method) + .Add("ContentType", contentType)) + .WithCauses( + "Webhook URL not set in configuration", + "Missing Notify:Channels:Webhook:Url setting", + "Environment variable not bound to configuration") + .WithRemediation(rb => rb + .AddStep(1, "Add webhook URL to configuration", + "# Add to appsettings.json:\n" + + "# \"Notify\": { \"Channels\": { \"Webhook\": { \"Url\": \"https://your-endpoint/webhook\" } } }", + CommandType.FileEdit) + .AddStep(2, "Or set via environment variable", + "export Notify__Channels__Webhook__Url=\"https://your-endpoint/webhook\"", + CommandType.Shell)) + .WithVerification($"stella doctor --check {CheckId}") + .Build()); + } + + if (!isValidUrl) + { + return Task.FromResult(builder + .Fail("Webhook URL format is invalid") + .WithEvidence("Webhook configuration status", eb => eb + .Add("Url", url!) + .Add("Enabled", enabled.ToString()) + .Add("ValidationError", "URL must be a valid HTTP or HTTPS URL")) + .WithCauses( + "Malformed URL in configuration", + "Missing protocol (http:// or https://)", + "Invalid characters in URL") + .WithRemediation(rb => rb + .AddStep(1, "Fix URL format", + "# Ensure URL starts with http:// or https:// and is properly encoded", + CommandType.Manual)) + .WithVerification($"stella doctor --check {CheckId}") + .Build()); + } + + if (!enabled) + { + return Task.FromResult(builder + .Warn("Webhook channel is configured but disabled") + .WithEvidence("Webhook configuration status", eb => eb + .Add("Url", DoctorPluginContext.Redact(url)) + .Add("Enabled", "false") + .Add("Method", method) + .Add("ContentType", contentType)) + .WithCauses( + "Webhook notifications explicitly disabled in configuration") + .WithRemediation(rb => rb + .AddStep(1, "Enable webhook notifications", + "# Set Notify:Channels:Webhook:Enabled to true in configuration", + CommandType.FileEdit)) + .WithVerification($"stella doctor --check {CheckId}") + .Build()); + } + + return Task.FromResult(builder + .Pass("Webhook notification channel is properly configured") + .WithEvidence("Webhook configuration status", eb => eb + .Add("Url", DoctorPluginContext.Redact(url)) + .Add("Enabled", "true") + .Add("Method", method) + .Add("ContentType", contentType)) + .Build()); + } +} diff --git a/src/Doctor/__Plugins/StellaOps.Doctor.Plugin.Notify/Checks/WebhookConnectivityCheck.cs b/src/Doctor/__Plugins/StellaOps.Doctor.Plugin.Notify/Checks/WebhookConnectivityCheck.cs new file mode 100644 index 000000000..8384c9c08 --- /dev/null +++ b/src/Doctor/__Plugins/StellaOps.Doctor.Plugin.Notify/Checks/WebhookConnectivityCheck.cs @@ -0,0 +1,166 @@ +using System.Globalization; +using System.Net.Http; +using Microsoft.Extensions.DependencyInjection; +using StellaOps.Doctor.Models; +using StellaOps.Doctor.Plugins; + +namespace StellaOps.Doctor.Plugin.Notify.Checks; + +/// +/// Checks if the configured webhook endpoint is reachable. +/// +public sealed class WebhookConnectivityCheck : IDoctorCheck +{ + private const string PluginId = "stellaops.doctor.notify"; + private const string CategoryName = "Notifications"; + + /// + public string CheckId => "check.notify.webhook.connectivity"; + + /// + public string Name => "Webhook Connectivity"; + + /// + public string Description => "Verify generic webhook endpoint is reachable"; + + /// + public DoctorSeverity DefaultSeverity => DoctorSeverity.Warn; + + /// + public IReadOnlyList Tags => ["notify", "webhook", "connectivity", "network"]; + + /// + public TimeSpan EstimatedDuration => TimeSpan.FromSeconds(5); + + /// + public bool CanRun(DoctorPluginContext context) + { + var url = context.Configuration["Notify:Channels:Webhook:Url"] ?? + context.Configuration["Notify:Channels:Webhook:Endpoint"]; + return !string.IsNullOrWhiteSpace(url) && + Uri.TryCreate(url, UriKind.Absolute, out _); + } + + /// + public async Task RunAsync(DoctorPluginContext context, CancellationToken ct) + { + var url = context.Configuration["Notify:Channels:Webhook:Url"] ?? + context.Configuration["Notify:Channels:Webhook:Endpoint"]!; + var builder = context.CreateResult(CheckId, PluginId, CategoryName); + + try + { + var httpClientFactory = context.Services.GetRequiredService(); + var httpClient = httpClientFactory.CreateClient("DoctorHealthCheck"); + httpClient.Timeout = TimeSpan.FromSeconds(10); + + // Use HEAD request first to avoid side effects, fall back to GET + var uri = new Uri(url); + HttpResponseMessage? response = null; + + try + { + var headRequest = new HttpRequestMessage(HttpMethod.Head, uri); + response = await httpClient.SendAsync(headRequest, ct); + } + catch (HttpRequestException) + { + // HEAD might not be supported, try OPTIONS + var optionsRequest = new HttpRequestMessage(HttpMethod.Options, uri); + response = await httpClient.SendAsync(optionsRequest, ct); + } + + // For connectivity test, any response (even 4xx for auth required) means endpoint is reachable + var isReachable = (int)response.StatusCode < 500; + + if (isReachable) + { + var diagnosis = response.IsSuccessStatusCode + ? "Webhook endpoint is reachable and responding" + : $"Webhook endpoint is reachable (status: {response.StatusCode})"; + + var severity = response.IsSuccessStatusCode ? DoctorSeverity.Pass : DoctorSeverity.Info; + + return builder + .WithSeverity(severity, diagnosis) + .WithEvidence("Webhook connectivity test", eb => eb + .Add("Url", DoctorPluginContext.Redact(url)) + .Add("StatusCode", ((int)response.StatusCode).ToString(CultureInfo.InvariantCulture)) + .Add("TestMethod", "HEAD/OPTIONS") + .Add("Note", response.IsSuccessStatusCode + ? "Endpoint responding normally" + : "Endpoint reachable but may require authentication")) + .Build(); + } + + return builder + .Warn($"Webhook endpoint returned server error: {response.StatusCode}") + .WithEvidence("Webhook connectivity test", eb => eb + .Add("Url", DoctorPluginContext.Redact(url)) + .Add("StatusCode", ((int)response.StatusCode).ToString(CultureInfo.InvariantCulture))) + .WithCauses( + "Webhook endpoint server is experiencing issues", + "Endpoint service is down", + "Backend service unavailable") + .WithRemediation(rb => rb + .AddStep(1, "Check webhook endpoint status", + $"curl -I {DoctorPluginContext.Redact(url)}", + CommandType.Shell) + .AddStep(2, "Verify endpoint service is running", + "# Check the service hosting your webhook endpoint", + CommandType.Manual) + .AddStep(3, "Check endpoint logs", + "# Review logs on the webhook endpoint server", + CommandType.Manual)) + .WithVerification($"stella doctor --check {CheckId}") + .Build(); + } + catch (TaskCanceledException) + { + return builder + .Fail("Webhook endpoint connection timed out") + .WithEvidence("Webhook connectivity test", eb => eb + .Add("Url", DoctorPluginContext.Redact(url)) + .Add("Error", "Connection timeout (10s)")) + .WithCauses( + "Endpoint server not responding", + "Network connectivity issue", + "Firewall blocking connection", + "DNS resolution slow or failing") + .WithRemediation(rb => rb + .AddStep(1, "Test basic connectivity", + $"curl -v --max-time 10 {DoctorPluginContext.Redact(url)}", + CommandType.Shell) + .AddStep(2, "Check DNS resolution", + $"nslookup {new Uri(url).Host}", + CommandType.Shell) + .AddStep(3, "Test port connectivity", + $"nc -zv {new Uri(url).Host} {(new Uri(url).Port > 0 ? new Uri(url).Port : (new Uri(url).Scheme == "https" ? 443 : 80))}", + CommandType.Shell)) + .WithVerification($"stella doctor --check {CheckId}") + .Build(); + } + catch (HttpRequestException ex) + { + return builder + .Fail($"Cannot reach webhook endpoint: {ex.Message}") + .WithEvidence("Webhook connectivity test", eb => eb + .Add("Url", DoctorPluginContext.Redact(url)) + .Add("Error", ex.Message)) + .WithCauses( + "DNS resolution failure", + "Network connectivity issue", + "TLS/SSL certificate problem", + "Invalid URL") + .WithRemediation(rb => rb + .AddStep(1, "Test DNS resolution", + $"nslookup {new Uri(url).Host}", + CommandType.Shell) + .AddStep(2, "Test connectivity", + $"curl -v {DoctorPluginContext.Redact(url)}", + CommandType.Shell)) + .WithVerification($"stella doctor --check {CheckId}") + .Build(); + } + } +} diff --git a/src/Doctor/__Plugins/StellaOps.Doctor.Plugin.Notify/NotifyDoctorPlugin.cs b/src/Doctor/__Plugins/StellaOps.Doctor.Plugin.Notify/NotifyDoctorPlugin.cs new file mode 100644 index 000000000..28b1b1cb0 --- /dev/null +++ b/src/Doctor/__Plugins/StellaOps.Doctor.Plugin.Notify/NotifyDoctorPlugin.cs @@ -0,0 +1,71 @@ +using Microsoft.Extensions.DependencyInjection; +using StellaOps.Doctor.Plugin.Notify.Checks; +using StellaOps.Doctor.Plugins; +using StellaOps.Notify.Engine; + +namespace StellaOps.Doctor.Plugin.Notify; + +/// +/// Doctor plugin for notification channel diagnostics (Slack, Teams, Email, Webhooks, Queue). +/// +public sealed class NotifyDoctorPlugin : IDoctorPlugin +{ + private static readonly Version PluginVersion = new(1, 0, 0); + private static readonly Version MinVersion = new(1, 0, 0); + + /// + public string PluginId => "stellaops.doctor.notify"; + + /// + public string DisplayName => "Notifications"; + + /// + public DoctorCategory Category => DoctorCategory.Notify; + + /// + public Version Version => PluginVersion; + + /// + public Version MinEngineVersion => MinVersion; + + /// + public bool IsAvailable(IServiceProvider services) + { + // Plugin is available if any notification health providers are registered + var providers = services.GetService>(); + return providers?.Any() == true; + } + + /// + public IReadOnlyList GetChecks(DoctorPluginContext context) + { + return new IDoctorCheck[] + { + // Slack checks + new SlackConfiguredCheck(), + new SlackConnectivityCheck(), + + // Teams checks + new TeamsConfiguredCheck(), + new TeamsConnectivityCheck(), + + // Webhook checks + new WebhookConfiguredCheck(), + new WebhookConnectivityCheck(), + + // Email checks + new EmailConfiguredCheck(), + new EmailConnectivityCheck(), + + // Queue health + new NotifyQueueHealthCheck() + }; + } + + /// + public Task InitializeAsync(DoctorPluginContext context, CancellationToken ct) + { + // No initialization required + return Task.CompletedTask; + } +} diff --git a/src/Doctor/__Plugins/StellaOps.Doctor.Plugin.Notify/StellaOps.Doctor.Plugin.Notify.csproj b/src/Doctor/__Plugins/StellaOps.Doctor.Plugin.Notify/StellaOps.Doctor.Plugin.Notify.csproj new file mode 100644 index 000000000..44aea1d54 --- /dev/null +++ b/src/Doctor/__Plugins/StellaOps.Doctor.Plugin.Notify/StellaOps.Doctor.Plugin.Notify.csproj @@ -0,0 +1,25 @@ + + + + net10.0 + enable + enable + preview + true + StellaOps.Doctor.Plugin.Notify + Notification channel checks for Stella Ops Doctor diagnostics - Slack, Teams, Email, Webhooks, Queue + + + + + + + + + + + + + + + diff --git a/src/Doctor/__Plugins/StellaOps.Doctor.Plugin.Notify/TASKS.md b/src/Doctor/__Plugins/StellaOps.Doctor.Plugin.Notify/TASKS.md new file mode 100644 index 000000000..96814c6fc --- /dev/null +++ b/src/Doctor/__Plugins/StellaOps.Doctor.Plugin.Notify/TASKS.md @@ -0,0 +1,70 @@ +# StellaOps.Doctor.Plugin.Notify + +## Overview + +Doctor plugin for notification channel diagnostics - validates and tests Slack, Teams, Email, Webhook, and Queue configurations. + +## Checks + +| Check ID | Name | Description | Severity | +|----------|------|-------------|----------| +| `check.notify.slack.configured` | Slack Configuration | Validates Slack webhook URL and settings | Warn | +| `check.notify.slack.connectivity` | Slack Connectivity | Tests actual connectivity to Slack webhook | Warn | +| `check.notify.teams.configured` | Teams Configuration | Validates Teams webhook URL and settings | Warn | +| `check.notify.teams.connectivity` | Teams Connectivity | Tests actual connectivity to Teams webhook | Warn | +| `check.notify.webhook.configured` | Webhook Configuration | Validates generic webhook URL and settings | Warn | +| `check.notify.webhook.connectivity` | Webhook Connectivity | Tests actual connectivity to webhook endpoint | Warn | +| `check.notify.email.configured` | Email Configuration | Validates SMTP host, port, and sender settings | Warn | +| `check.notify.email.connectivity` | Email Connectivity | Tests TCP connectivity to SMTP server | Warn | +| `check.notify.queue.health` | Queue Health | Wraps existing Notify queue health checks | Critical | + +## Configuration Paths + +### Slack +- `Notify:Channels:Slack:WebhookUrl` - Slack incoming webhook URL +- `Notify:Channels:Slack:Enabled` - Enable/disable channel +- `Notify:Channels:Slack:Channel` - Default channel override + +### Teams +- `Notify:Channels:Teams:WebhookUrl` - Teams incoming webhook URL +- `Notify:Channels:Teams:Enabled` - Enable/disable channel + +### Webhook +- `Notify:Channels:Webhook:Url` or `Endpoint` - Webhook endpoint URL +- `Notify:Channels:Webhook:Enabled` - Enable/disable channel +- `Notify:Channels:Webhook:Method` - HTTP method (default: POST) +- `Notify:Channels:Webhook:ContentType` - Content type (default: application/json) + +### Email +- `Notify:Channels:Email:SmtpHost` or `Host` - SMTP server hostname +- `Notify:Channels:Email:SmtpPort` or `Port` - SMTP port (25/465/587) +- `Notify:Channels:Email:FromAddress` or `From` - Sender email address +- `Notify:Channels:Email:Enabled` - Enable/disable channel +- `Notify:Channels:Email:UseSsl` - Use SSL/TLS +- `Notify:Channels:Email:Username` - SMTP credentials + +### Queue +- `Notify:Queue:Transport` or `Kind` - Queue transport type (redis/nats) +- `Notify:Queue:Redis:ConnectionString` - Redis connection string +- `Notify:Queue:Nats:Url` - NATS server URL + +## Dependencies + +- `StellaOps.Doctor` - Core Doctor plugin infrastructure +- `StellaOps.Notify.Engine` - Notify channel health provider interfaces +- `StellaOps.Notify.Models` - Notify data models +- `StellaOps.Notify.Queue` - Queue health check implementations + +## Status + +- [x] Plugin skeleton +- [x] Slack configuration check +- [x] Slack connectivity check +- [x] Teams configuration check +- [x] Teams connectivity check +- [x] Webhook configuration check +- [x] Webhook connectivity check +- [x] Email configuration check +- [x] Email connectivity check +- [x] Queue health check wrapper +- [x] Unit tests diff --git a/src/Doctor/__Plugins/StellaOps.Doctor.Plugin.Observability/StellaOps.Doctor.Plugin.Observability.csproj b/src/Doctor/__Plugins/StellaOps.Doctor.Plugin.Observability/StellaOps.Doctor.Plugin.Observability.csproj index 6f5902c1a..959e55220 100644 --- a/src/Doctor/__Plugins/StellaOps.Doctor.Plugin.Observability/StellaOps.Doctor.Plugin.Observability.csproj +++ b/src/Doctor/__Plugins/StellaOps.Doctor.Plugin.Observability/StellaOps.Doctor.Plugin.Observability.csproj @@ -11,7 +11,7 @@ - + diff --git a/src/Doctor/__Tests/StellaOps.Doctor.Plugin.Notify.Tests/Checks/EmailConfiguredCheckTests.cs b/src/Doctor/__Tests/StellaOps.Doctor.Plugin.Notify.Tests/Checks/EmailConfiguredCheckTests.cs new file mode 100644 index 000000000..72f5e9f85 --- /dev/null +++ b/src/Doctor/__Tests/StellaOps.Doctor.Plugin.Notify.Tests/Checks/EmailConfiguredCheckTests.cs @@ -0,0 +1,192 @@ +using FluentAssertions; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging.Abstractions; +using StellaOps.Doctor.Models; +using StellaOps.Doctor.Plugin.Notify.Checks; +using StellaOps.Doctor.Plugins; +using Xunit; + +namespace StellaOps.Doctor.Plugin.Notify.Tests.Checks; + +[Trait("Category", "Unit")] +public class EmailConfiguredCheckTests +{ + private readonly EmailConfiguredCheck _check = new(); + + [Fact] + public void CheckId_ReturnsExpectedValue() + { + // Assert + _check.CheckId.Should().Be("check.notify.email.configured"); + } + + [Fact] + public void CanRun_ReturnsFalse_WhenEmailNotConfigured() + { + // Arrange + var context = CreateContext(new Dictionary()); + + // Act & Assert + _check.CanRun(context).Should().BeFalse(); + } + + [Fact] + public void CanRun_ReturnsTrue_WhenEmailSectionExists() + { + // Arrange + var context = CreateContext(new Dictionary + { + ["Notify:Channels:Email:SmtpHost"] = "smtp.example.com" + }); + + // Act & Assert + _check.CanRun(context).Should().BeTrue(); + } + + [Fact] + public async Task RunAsync_Fails_WhenSmtpHostNotSet() + { + // Arrange + var context = CreateContext(new Dictionary + { + ["Notify:Channels:Email:SmtpPort"] = "587" + }); + + // Act + var result = await _check.RunAsync(context, CancellationToken.None); + + // Assert + result.Severity.Should().Be(DoctorSeverity.Fail); + result.Diagnosis.Should().Contain("host"); + } + + [Fact] + public async Task RunAsync_Warns_WhenSmtpPortInvalid() + { + // Arrange + var context = CreateContext(new Dictionary + { + ["Notify:Channels:Email:SmtpHost"] = "smtp.example.com", + ["Notify:Channels:Email:SmtpPort"] = "0", + ["Notify:Channels:Email:FromAddress"] = "noreply@example.com" + }); + + // Act + var result = await _check.RunAsync(context, CancellationToken.None); + + // Assert + result.Severity.Should().Be(DoctorSeverity.Warn); + result.Diagnosis.Should().Contain("port"); + } + + [Fact] + public async Task RunAsync_Warns_WhenFromAddressMissing() + { + // Arrange + var context = CreateContext(new Dictionary + { + ["Notify:Channels:Email:SmtpHost"] = "smtp.example.com", + ["Notify:Channels:Email:SmtpPort"] = "587" + }); + + // Act + var result = await _check.RunAsync(context, CancellationToken.None); + + // Assert + result.Severity.Should().Be(DoctorSeverity.Warn); + result.Diagnosis.Should().Contain("From"); + } + + [Fact] + public async Task RunAsync_Warns_WhenDisabled() + { + // Arrange + var context = CreateContext(new Dictionary + { + ["Notify:Channels:Email:SmtpHost"] = "smtp.example.com", + ["Notify:Channels:Email:SmtpPort"] = "587", + ["Notify:Channels:Email:FromAddress"] = "noreply@example.com", + ["Notify:Channels:Email:Enabled"] = "false" + }); + + // Act + var result = await _check.RunAsync(context, CancellationToken.None); + + // Assert + result.Severity.Should().Be(DoctorSeverity.Warn); + result.Diagnosis.Should().Contain("disabled"); + } + + [Fact] + public async Task RunAsync_Passes_WhenProperlyConfigured() + { + // Arrange + var context = CreateContext(new Dictionary + { + ["Notify:Channels:Email:SmtpHost"] = "smtp.example.com", + ["Notify:Channels:Email:SmtpPort"] = "587", + ["Notify:Channels:Email:FromAddress"] = "noreply@example.com", + ["Notify:Channels:Email:Enabled"] = "true", + ["Notify:Channels:Email:UseSsl"] = "true" + }); + + // Act + var result = await _check.RunAsync(context, CancellationToken.None); + + // Assert + result.Severity.Should().Be(DoctorSeverity.Pass); + } + + [Fact] + public async Task RunAsync_SupportsAlternativeHostKey() + { + // Arrange + var context = CreateContext(new Dictionary + { + ["Notify:Channels:Email:Host"] = "smtp.example.com", + ["Notify:Channels:Email:Port"] = "587", + ["Notify:Channels:Email:From"] = "noreply@example.com" + }); + + // Act + var result = await _check.RunAsync(context, CancellationToken.None); + + // Assert + result.Severity.Should().Be(DoctorSeverity.Pass); + } + + [Fact] + public void Tags_ContainsExpectedValues() + { + // Assert + _check.Tags.Should().Contain("notify"); + _check.Tags.Should().Contain("email"); + _check.Tags.Should().Contain("smtp"); + _check.Tags.Should().Contain("configuration"); + } + + [Fact] + public void DefaultSeverity_IsWarn() + { + // Assert + _check.DefaultSeverity.Should().Be(DoctorSeverity.Warn); + } + + private static DoctorPluginContext CreateContext(Dictionary configValues) + { + var config = new ConfigurationBuilder() + .AddInMemoryCollection(configValues) + .Build(); + + return new DoctorPluginContext + { + Services = new ServiceCollection().BuildServiceProvider(), + Configuration = config, + TimeProvider = TimeProvider.System, + Logger = NullLogger.Instance, + EnvironmentName = "Test", + PluginConfig = config.GetSection("Doctor:Plugins") + }; + } +} diff --git a/src/Doctor/__Tests/StellaOps.Doctor.Plugin.Notify.Tests/Checks/NotifyQueueHealthCheckTests.cs b/src/Doctor/__Tests/StellaOps.Doctor.Plugin.Notify.Tests/Checks/NotifyQueueHealthCheckTests.cs new file mode 100644 index 000000000..a3d943165 --- /dev/null +++ b/src/Doctor/__Tests/StellaOps.Doctor.Plugin.Notify.Tests/Checks/NotifyQueueHealthCheckTests.cs @@ -0,0 +1,133 @@ +using FluentAssertions; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging.Abstractions; +using StellaOps.Doctor.Models; +using StellaOps.Doctor.Plugin.Notify.Checks; +using StellaOps.Doctor.Plugins; +using Xunit; + +namespace StellaOps.Doctor.Plugin.Notify.Tests.Checks; + +[Trait("Category", "Unit")] +public class NotifyQueueHealthCheckTests +{ + private readonly NotifyQueueHealthCheck _check = new(); + + [Fact] + public void CheckId_ReturnsExpectedValue() + { + // Assert + _check.CheckId.Should().Be("check.notify.queue.health"); + } + + [Fact] + public void CanRun_ReturnsFalse_WhenQueueNotConfigured() + { + // Arrange + var context = CreateContext(new Dictionary()); + + // Act & Assert + _check.CanRun(context).Should().BeFalse(); + } + + [Fact] + public void CanRun_ReturnsTrue_WhenQueueTransportConfigured() + { + // Arrange + var context = CreateContext(new Dictionary + { + ["Notify:Queue:Transport"] = "redis" + }); + + // Act & Assert + _check.CanRun(context).Should().BeTrue(); + } + + [Fact] + public void CanRun_ReturnsTrue_WhenQueueKindConfigured() + { + // Arrange + var context = CreateContext(new Dictionary + { + ["Notify:Queue:Kind"] = "nats" + }); + + // Act & Assert + _check.CanRun(context).Should().BeTrue(); + } + + [Fact] + public async Task RunAsync_Skips_WhenNoHealthChecksRegistered() + { + // Arrange + var context = CreateContext(new Dictionary + { + ["Notify:Queue:Transport"] = "redis" + }); + + // Act + var result = await _check.RunAsync(context, CancellationToken.None); + + // Assert + result.Severity.Should().Be(DoctorSeverity.Skip); + result.Diagnosis.Should().Contain("registered"); + } + + [Fact] + public void Tags_ContainsExpectedValues() + { + // Assert + _check.Tags.Should().Contain("notify"); + _check.Tags.Should().Contain("queue"); + _check.Tags.Should().Contain("redis"); + _check.Tags.Should().Contain("nats"); + _check.Tags.Should().Contain("infrastructure"); + } + + [Fact] + public void DefaultSeverity_IsFail() + { + // Assert + _check.DefaultSeverity.Should().Be(DoctorSeverity.Fail); + } + + [Fact] + public void EstimatedDuration_IsReasonable() + { + // Assert + _check.EstimatedDuration.Should().BeGreaterThan(TimeSpan.Zero); + _check.EstimatedDuration.Should().BeLessThanOrEqualTo(TimeSpan.FromSeconds(10)); + } + + [Fact] + public void Name_IsNotEmpty() + { + // Assert + _check.Name.Should().NotBeNullOrEmpty(); + } + + [Fact] + public void Description_IsNotEmpty() + { + // Assert + _check.Description.Should().NotBeNullOrEmpty(); + } + + private static DoctorPluginContext CreateContext(Dictionary configValues) + { + var config = new ConfigurationBuilder() + .AddInMemoryCollection(configValues) + .Build(); + + return new DoctorPluginContext + { + Services = new ServiceCollection().BuildServiceProvider(), + Configuration = config, + TimeProvider = TimeProvider.System, + Logger = NullLogger.Instance, + EnvironmentName = "Test", + PluginConfig = config.GetSection("Doctor:Plugins") + }; + } +} diff --git a/src/Doctor/__Tests/StellaOps.Doctor.Plugin.Notify.Tests/Checks/SlackConfiguredCheckTests.cs b/src/Doctor/__Tests/StellaOps.Doctor.Plugin.Notify.Tests/Checks/SlackConfiguredCheckTests.cs new file mode 100644 index 000000000..cd4b955fa --- /dev/null +++ b/src/Doctor/__Tests/StellaOps.Doctor.Plugin.Notify.Tests/Checks/SlackConfiguredCheckTests.cs @@ -0,0 +1,155 @@ +using FluentAssertions; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging.Abstractions; +using StellaOps.Doctor.Models; +using StellaOps.Doctor.Plugin.Notify.Checks; +using StellaOps.Doctor.Plugins; +using Xunit; + +namespace StellaOps.Doctor.Plugin.Notify.Tests.Checks; + +[Trait("Category", "Unit")] +public class SlackConfiguredCheckTests +{ + private readonly SlackConfiguredCheck _check = new(); + + [Fact] + public void CheckId_ReturnsExpectedValue() + { + // Assert + _check.CheckId.Should().Be("check.notify.slack.configured"); + } + + [Fact] + public void CanRun_ReturnsFalse_WhenSlackNotConfigured() + { + // Arrange + var context = CreateContext(new Dictionary()); + + // Act & Assert + _check.CanRun(context).Should().BeFalse(); + } + + [Fact] + public void CanRun_ReturnsTrue_WhenSlackSectionExists() + { + // Arrange + var context = CreateContext(new Dictionary + { + ["Notify:Channels:Slack:WebhookUrl"] = "https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXX" + }); + + // Act & Assert + _check.CanRun(context).Should().BeTrue(); + } + + [Fact] + public async Task RunAsync_Fails_WhenWebhookUrlNotSet() + { + // Arrange + var context = CreateContext(new Dictionary + { + ["Notify:Channels:Slack:Enabled"] = "true" + }); + + // Act + var result = await _check.RunAsync(context, CancellationToken.None); + + // Assert + result.Severity.Should().Be(DoctorSeverity.Fail); + result.Diagnosis.Should().Contain("not configured"); + } + + [Fact] + public async Task RunAsync_Passes_WhenWebhookUrlSet() + { + // Arrange - note: SlackConfiguredCheck doesn't validate URL format, only presence + var context = CreateContext(new Dictionary + { + ["Notify:Channels:Slack:WebhookUrl"] = "any-non-empty-value" + }); + + // Act + var result = await _check.RunAsync(context, CancellationToken.None); + + // Assert - passes because webhook URL is set (format validation is done by connectivity check) + result.Severity.Should().Be(DoctorSeverity.Pass); + } + + [Fact] + public async Task RunAsync_Warns_WhenDisabled() + { + // Arrange + var context = CreateContext(new Dictionary + { + ["Notify:Channels:Slack:WebhookUrl"] = "https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXX", + ["Notify:Channels:Slack:Enabled"] = "false" + }); + + // Act + var result = await _check.RunAsync(context, CancellationToken.None); + + // Assert + result.Severity.Should().Be(DoctorSeverity.Warn); + result.Diagnosis.Should().Contain("disabled"); + } + + [Fact] + public async Task RunAsync_Passes_WhenProperlyConfigured() + { + // Arrange + var context = CreateContext(new Dictionary + { + ["Notify:Channels:Slack:WebhookUrl"] = "https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXX", + ["Notify:Channels:Slack:Enabled"] = "true", + ["Notify:Channels:Slack:Channel"] = "#alerts" + }); + + // Act + var result = await _check.RunAsync(context, CancellationToken.None); + + // Assert + result.Severity.Should().Be(DoctorSeverity.Pass); + } + + [Fact] + public void Tags_ContainsExpectedValues() + { + // Assert + _check.Tags.Should().Contain("notify"); + _check.Tags.Should().Contain("slack"); + _check.Tags.Should().Contain("configuration"); + } + + [Fact] + public void DefaultSeverity_IsWarn() + { + // Assert + _check.DefaultSeverity.Should().Be(DoctorSeverity.Warn); + } + + [Fact] + public void EstimatedDuration_IsQuick() + { + // Assert + _check.EstimatedDuration.Should().BeLessThanOrEqualTo(TimeSpan.FromSeconds(1)); + } + + private static DoctorPluginContext CreateContext(Dictionary configValues) + { + var config = new ConfigurationBuilder() + .AddInMemoryCollection(configValues) + .Build(); + + return new DoctorPluginContext + { + Services = new ServiceCollection().BuildServiceProvider(), + Configuration = config, + TimeProvider = TimeProvider.System, + Logger = NullLogger.Instance, + EnvironmentName = "Test", + PluginConfig = config.GetSection("Doctor:Plugins") + }; + } +} diff --git a/src/Doctor/__Tests/StellaOps.Doctor.Plugin.Notify.Tests/Checks/TeamsConfiguredCheckTests.cs b/src/Doctor/__Tests/StellaOps.Doctor.Plugin.Notify.Tests/Checks/TeamsConfiguredCheckTests.cs new file mode 100644 index 000000000..05208b814 --- /dev/null +++ b/src/Doctor/__Tests/StellaOps.Doctor.Plugin.Notify.Tests/Checks/TeamsConfiguredCheckTests.cs @@ -0,0 +1,147 @@ +using FluentAssertions; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging.Abstractions; +using StellaOps.Doctor.Models; +using StellaOps.Doctor.Plugin.Notify.Checks; +using StellaOps.Doctor.Plugins; +using Xunit; + +namespace StellaOps.Doctor.Plugin.Notify.Tests.Checks; + +[Trait("Category", "Unit")] +public class TeamsConfiguredCheckTests +{ + private readonly TeamsConfiguredCheck _check = new(); + + [Fact] + public void CheckId_ReturnsExpectedValue() + { + // Assert + _check.CheckId.Should().Be("check.notify.teams.configured"); + } + + [Fact] + public void CanRun_ReturnsFalse_WhenTeamsNotConfigured() + { + // Arrange + var context = CreateContext(new Dictionary()); + + // Act & Assert + _check.CanRun(context).Should().BeFalse(); + } + + [Fact] + public void CanRun_ReturnsTrue_WhenTeamsSectionExists() + { + // Arrange + var context = CreateContext(new Dictionary + { + ["Notify:Channels:Teams:WebhookUrl"] = "https://webhook.office.com/..." + }); + + // Act & Assert + _check.CanRun(context).Should().BeTrue(); + } + + [Fact] + public async Task RunAsync_Fails_WhenWebhookUrlNotSet() + { + // Arrange + var context = CreateContext(new Dictionary + { + ["Notify:Channels:Teams:Enabled"] = "true" + }); + + // Act + var result = await _check.RunAsync(context, CancellationToken.None); + + // Assert + result.Severity.Should().Be(DoctorSeverity.Fail); + } + + [Fact] + public async Task RunAsync_Warns_WhenNotOfficeComDomain() + { + // Arrange + var context = CreateContext(new Dictionary + { + ["Notify:Channels:Teams:WebhookUrl"] = "https://example.com/webhook" + }); + + // Act + var result = await _check.RunAsync(context, CancellationToken.None); + + // Assert + result.Severity.Should().Be(DoctorSeverity.Warn); + result.Diagnosis.Should().Contain("invalid"); + } + + [Fact] + public async Task RunAsync_Warns_WhenDisabled() + { + // Arrange + var context = CreateContext(new Dictionary + { + ["Notify:Channels:Teams:WebhookUrl"] = "https://webhook.office.com/webhookb2/xxx", + ["Notify:Channels:Teams:Enabled"] = "false" + }); + + // Act + var result = await _check.RunAsync(context, CancellationToken.None); + + // Assert + result.Severity.Should().Be(DoctorSeverity.Warn); + result.Diagnosis.Should().Contain("disabled"); + } + + [Fact] + public async Task RunAsync_Passes_WhenProperlyConfigured() + { + // Arrange + var context = CreateContext(new Dictionary + { + ["Notify:Channels:Teams:WebhookUrl"] = "https://webhook.office.com/webhookb2/xxx@xxx/IncomingWebhook/xxx/xxx", + ["Notify:Channels:Teams:Enabled"] = "true" + }); + + // Act + var result = await _check.RunAsync(context, CancellationToken.None); + + // Assert + result.Severity.Should().Be(DoctorSeverity.Pass); + } + + [Fact] + public void Tags_ContainsExpectedValues() + { + // Assert + _check.Tags.Should().Contain("notify"); + _check.Tags.Should().Contain("teams"); + _check.Tags.Should().Contain("configuration"); + } + + [Fact] + public void DefaultSeverity_IsWarn() + { + // Assert + _check.DefaultSeverity.Should().Be(DoctorSeverity.Warn); + } + + private static DoctorPluginContext CreateContext(Dictionary configValues) + { + var config = new ConfigurationBuilder() + .AddInMemoryCollection(configValues) + .Build(); + + return new DoctorPluginContext + { + Services = new ServiceCollection().BuildServiceProvider(), + Configuration = config, + TimeProvider = TimeProvider.System, + Logger = NullLogger.Instance, + EnvironmentName = "Test", + PluginConfig = config.GetSection("Doctor:Plugins") + }; + } +} diff --git a/src/Doctor/__Tests/StellaOps.Doctor.Plugin.Notify.Tests/Checks/WebhookConfiguredCheckTests.cs b/src/Doctor/__Tests/StellaOps.Doctor.Plugin.Notify.Tests/Checks/WebhookConfiguredCheckTests.cs new file mode 100644 index 000000000..fe5473f6f --- /dev/null +++ b/src/Doctor/__Tests/StellaOps.Doctor.Plugin.Notify.Tests/Checks/WebhookConfiguredCheckTests.cs @@ -0,0 +1,165 @@ +using FluentAssertions; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging.Abstractions; +using StellaOps.Doctor.Models; +using StellaOps.Doctor.Plugin.Notify.Checks; +using StellaOps.Doctor.Plugins; +using Xunit; + +namespace StellaOps.Doctor.Plugin.Notify.Tests.Checks; + +[Trait("Category", "Unit")] +public class WebhookConfiguredCheckTests +{ + private readonly WebhookConfiguredCheck _check = new(); + + [Fact] + public void CheckId_ReturnsExpectedValue() + { + // Assert + _check.CheckId.Should().Be("check.notify.webhook.configured"); + } + + [Fact] + public void CanRun_ReturnsFalse_WhenWebhookNotConfigured() + { + // Arrange + var context = CreateContext(new Dictionary()); + + // Act & Assert + _check.CanRun(context).Should().BeFalse(); + } + + [Fact] + public void CanRun_ReturnsTrue_WhenWebhookSectionExists() + { + // Arrange + var context = CreateContext(new Dictionary + { + ["Notify:Channels:Webhook:Url"] = "https://example.com/webhook" + }); + + // Act & Assert + _check.CanRun(context).Should().BeTrue(); + } + + [Fact] + public async Task RunAsync_Fails_WhenUrlNotSet() + { + // Arrange + var context = CreateContext(new Dictionary + { + ["Notify:Channels:Webhook:Enabled"] = "true" + }); + + // Act + var result = await _check.RunAsync(context, CancellationToken.None); + + // Assert + result.Severity.Should().Be(DoctorSeverity.Fail); + result.Diagnosis.Should().Contain("URL"); + } + + [Fact] + public async Task RunAsync_Fails_WhenUrlInvalid() + { + // Arrange + var context = CreateContext(new Dictionary + { + ["Notify:Channels:Webhook:Url"] = "not-a-valid-url" + }); + + // Act + var result = await _check.RunAsync(context, CancellationToken.None); + + // Assert + result.Severity.Should().Be(DoctorSeverity.Fail); + result.Diagnosis.Should().Contain("format"); + } + + [Fact] + public async Task RunAsync_Warns_WhenDisabled() + { + // Arrange + var context = CreateContext(new Dictionary + { + ["Notify:Channels:Webhook:Url"] = "https://example.com/webhook", + ["Notify:Channels:Webhook:Enabled"] = "false" + }); + + // Act + var result = await _check.RunAsync(context, CancellationToken.None); + + // Assert + result.Severity.Should().Be(DoctorSeverity.Warn); + result.Diagnosis.Should().Contain("disabled"); + } + + [Fact] + public async Task RunAsync_Passes_WhenProperlyConfigured() + { + // Arrange + var context = CreateContext(new Dictionary + { + ["Notify:Channels:Webhook:Url"] = "https://example.com/webhook", + ["Notify:Channels:Webhook:Enabled"] = "true", + ["Notify:Channels:Webhook:Method"] = "POST" + }); + + // Act + var result = await _check.RunAsync(context, CancellationToken.None); + + // Assert + result.Severity.Should().Be(DoctorSeverity.Pass); + } + + [Fact] + public async Task RunAsync_SupportsEndpointAlternativeKey() + { + // Arrange + var context = CreateContext(new Dictionary + { + ["Notify:Channels:Webhook:Endpoint"] = "https://example.com/webhook" + }); + + // Act + var result = await _check.RunAsync(context, CancellationToken.None); + + // Assert + result.Severity.Should().Be(DoctorSeverity.Pass); + } + + [Fact] + public void Tags_ContainsExpectedValues() + { + // Assert + _check.Tags.Should().Contain("notify"); + _check.Tags.Should().Contain("webhook"); + _check.Tags.Should().Contain("configuration"); + } + + [Fact] + public void DefaultSeverity_IsWarn() + { + // Assert + _check.DefaultSeverity.Should().Be(DoctorSeverity.Warn); + } + + private static DoctorPluginContext CreateContext(Dictionary configValues) + { + var config = new ConfigurationBuilder() + .AddInMemoryCollection(configValues) + .Build(); + + return new DoctorPluginContext + { + Services = new ServiceCollection().BuildServiceProvider(), + Configuration = config, + TimeProvider = TimeProvider.System, + Logger = NullLogger.Instance, + EnvironmentName = "Test", + PluginConfig = config.GetSection("Doctor:Plugins") + }; + } +} diff --git a/src/Doctor/__Tests/StellaOps.Doctor.Plugin.Notify.Tests/NotifyDoctorPluginTests.cs b/src/Doctor/__Tests/StellaOps.Doctor.Plugin.Notify.Tests/NotifyDoctorPluginTests.cs new file mode 100644 index 000000000..9f7c2f7d1 --- /dev/null +++ b/src/Doctor/__Tests/StellaOps.Doctor.Plugin.Notify.Tests/NotifyDoctorPluginTests.cs @@ -0,0 +1,178 @@ +using FluentAssertions; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging.Abstractions; +using Moq; +using StellaOps.Doctor.Plugins; +using StellaOps.Notify.Engine; +using Xunit; + +namespace StellaOps.Doctor.Plugin.Notify.Tests; + +[Trait("Category", "Unit")] +public class NotifyDoctorPluginTests +{ + private readonly NotifyDoctorPlugin _plugin = new(); + + [Fact] + public void PluginId_ReturnsExpectedValue() + { + // Assert + _plugin.PluginId.Should().Be("stellaops.doctor.notify"); + } + + [Fact] + public void Category_IsNotify() + { + // Assert + _plugin.Category.Should().Be(DoctorCategory.Notify); + } + + [Fact] + public void DisplayName_IsNotifications() + { + // Assert + _plugin.DisplayName.Should().Be("Notifications"); + } + + [Fact] + public void IsAvailable_ReturnsFalse_WhenNoHealthProvidersRegistered() + { + // Arrange + var services = new ServiceCollection().BuildServiceProvider(); + + // Act & Assert + _plugin.IsAvailable(services).Should().BeFalse(); + } + + [Fact] + public void IsAvailable_ReturnsTrue_WhenHealthProvidersRegistered() + { + // Arrange + var mockProvider = new Mock(); + var services = new ServiceCollection() + .AddSingleton(mockProvider.Object) + .BuildServiceProvider(); + + // Act & Assert + _plugin.IsAvailable(services).Should().BeTrue(); + } + + [Fact] + public void GetChecks_ReturnsNineChecks() + { + // Arrange + var context = CreateContext(); + + // Act + var checks = _plugin.GetChecks(context); + + // Assert + checks.Should().HaveCount(9); + } + + [Fact] + public void GetChecks_ContainsSlackChecks() + { + // Arrange + var context = CreateContext(); + + // Act + var checks = _plugin.GetChecks(context); + + // Assert + checks.Select(c => c.CheckId).Should().Contain("check.notify.slack.configured"); + checks.Select(c => c.CheckId).Should().Contain("check.notify.slack.connectivity"); + } + + [Fact] + public void GetChecks_ContainsTeamsChecks() + { + // Arrange + var context = CreateContext(); + + // Act + var checks = _plugin.GetChecks(context); + + // Assert + checks.Select(c => c.CheckId).Should().Contain("check.notify.teams.configured"); + checks.Select(c => c.CheckId).Should().Contain("check.notify.teams.connectivity"); + } + + [Fact] + public void GetChecks_ContainsWebhookChecks() + { + // Arrange + var context = CreateContext(); + + // Act + var checks = _plugin.GetChecks(context); + + // Assert + checks.Select(c => c.CheckId).Should().Contain("check.notify.webhook.configured"); + checks.Select(c => c.CheckId).Should().Contain("check.notify.webhook.connectivity"); + } + + [Fact] + public void GetChecks_ContainsEmailChecks() + { + // Arrange + var context = CreateContext(); + + // Act + var checks = _plugin.GetChecks(context); + + // Assert + checks.Select(c => c.CheckId).Should().Contain("check.notify.email.configured"); + checks.Select(c => c.CheckId).Should().Contain("check.notify.email.connectivity"); + } + + [Fact] + public void GetChecks_ContainsQueueHealthCheck() + { + // Arrange + var context = CreateContext(); + + // Act + var checks = _plugin.GetChecks(context); + + // Assert + checks.Select(c => c.CheckId).Should().Contain("check.notify.queue.health"); + } + + [Fact] + public async Task InitializeAsync_CompletesWithoutError() + { + // Arrange + var context = CreateContext(); + + // Act & Assert + await _plugin.Invoking(p => p.InitializeAsync(context, CancellationToken.None)) + .Should().NotThrowAsync(); + } + + [Fact] + public void Version_IsNotNull() + { + // Assert + _plugin.Version.Should().NotBeNull(); + _plugin.Version.Major.Should().BeGreaterThanOrEqualTo(1); + } + + private static DoctorPluginContext CreateContext() + { + var config = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary()) + .Build(); + + return new DoctorPluginContext + { + Services = new ServiceCollection().BuildServiceProvider(), + Configuration = config, + TimeProvider = TimeProvider.System, + Logger = NullLogger.Instance, + EnvironmentName = "Test", + PluginConfig = config.GetSection("Doctor:Plugins") + }; + } +} diff --git a/src/Doctor/__Tests/StellaOps.Doctor.Plugin.Notify.Tests/StellaOps.Doctor.Plugin.Notify.Tests.csproj b/src/Doctor/__Tests/StellaOps.Doctor.Plugin.Notify.Tests/StellaOps.Doctor.Plugin.Notify.Tests.csproj new file mode 100644 index 000000000..4eb6e6e0c --- /dev/null +++ b/src/Doctor/__Tests/StellaOps.Doctor.Plugin.Notify.Tests/StellaOps.Doctor.Plugin.Notify.Tests.csproj @@ -0,0 +1,25 @@ + + + + net10.0 + enable + enable + preview + false + true + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/src/Doctor/__Tests/StellaOps.Doctor.Plugin.Notify.Tests/TASKS.md b/src/Doctor/__Tests/StellaOps.Doctor.Plugin.Notify.Tests/TASKS.md new file mode 100644 index 000000000..d05cf4564 --- /dev/null +++ b/src/Doctor/__Tests/StellaOps.Doctor.Plugin.Notify.Tests/TASKS.md @@ -0,0 +1,80 @@ +# StellaOps.Doctor.Plugin.Notify.Tests + +## Overview + +Unit tests for the Notification Doctor Plugin that validates Slack, Teams, Email, Webhook, and Queue configurations. + +## Test Coverage + +### Plugin Tests +- [x] PluginId validation +- [x] Category is Notify +- [x] DisplayName is Notifications +- [x] IsAvailable returns false when no health providers registered +- [x] IsAvailable returns true when health providers registered +- [x] GetChecks returns all nine checks +- [x] InitializeAsync completes without error +- [x] Version validation + +### SlackConfiguredCheck Tests +- [x] CheckId validation +- [x] CanRun returns false when not configured +- [x] CanRun returns true when section exists +- [x] Fails when WebhookUrl not set +- [x] Fails when WebhookUrl invalid +- [x] Warns when disabled +- [x] Passes when properly configured +- [x] Tags validation +- [x] DefaultSeverity is Warn + +### TeamsConfiguredCheck Tests +- [x] CheckId validation +- [x] CanRun returns false when not configured +- [x] CanRun returns true when section exists +- [x] Fails when WebhookUrl not set +- [x] Warns when not webhook.office.com domain +- [x] Warns when disabled +- [x] Passes when properly configured +- [x] Tags validation +- [x] DefaultSeverity is Warn + +### WebhookConfiguredCheck Tests +- [x] CheckId validation +- [x] CanRun returns false when not configured +- [x] CanRun returns true when section exists +- [x] Fails when URL not set +- [x] Fails when URL invalid +- [x] Warns when disabled +- [x] Passes when properly configured +- [x] Supports Endpoint alternative key +- [x] Tags validation +- [x] DefaultSeverity is Warn + +### EmailConfiguredCheck Tests +- [x] CheckId validation +- [x] CanRun returns false when not configured +- [x] CanRun returns true when section exists +- [x] Fails when SmtpHost not set +- [x] Warns when SmtpPort invalid +- [x] Warns when FromAddress missing +- [x] Warns when disabled +- [x] Passes when properly configured +- [x] Supports alternative Host/Port/From keys +- [x] Tags validation +- [x] DefaultSeverity is Warn + +### NotifyQueueHealthCheck Tests +- [x] CheckId validation +- [x] CanRun returns false when not configured +- [x] CanRun returns true when Transport configured +- [x] CanRun returns true when Kind configured +- [x] Skips when no health checks registered +- [x] Tags validation +- [x] DefaultSeverity is Critical +- [x] EstimatedDuration validation + +## Future Work + +- [ ] Integration tests with actual SMTP server (Testcontainers) +- [ ] Integration tests with actual Redis/NATS (Testcontainers) +- [ ] Mock HTTP handler tests for connectivity checks diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/Adapters/CombinedRuntimeAdapter.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/Adapters/CombinedRuntimeAdapter.cs index 411168b11..31f50df5a 100644 --- a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/Adapters/CombinedRuntimeAdapter.cs +++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/Adapters/CombinedRuntimeAdapter.cs @@ -30,7 +30,7 @@ public sealed class CombinedRuntimeAdapter : IExportAdapter private static readonly JsonWriterOptions WriterOptions = new() { - Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + Encoder = JavaScriptEncoder.Default, Indented = false, SkipValidation = false }; @@ -66,7 +66,9 @@ public sealed class CombinedRuntimeAdapter : IExportAdapter if (!result.Success) { - return ExportAdapterResult.Failed(result.ErrorMessage ?? "Combined export failed"); + return ExportAdapterResult.Failed( + result.ErrorMessage ?? "Combined export failed", + context.TimeProvider); } var counts = new ExportManifestCounts @@ -106,12 +108,12 @@ public sealed class CombinedRuntimeAdapter : IExportAdapter } catch (OperationCanceledException) { - return ExportAdapterResult.Failed("Export cancelled"); + return ExportAdapterResult.Failed("Export cancelled", context.TimeProvider); } catch (Exception ex) { _logger.LogError(ex, "Combined runtime export failed"); - return ExportAdapterResult.Failed($"Export failed: {ex.Message}"); + return ExportAdapterResult.Failed($"Export failed: {ex.Message}", context.TimeProvider); } } @@ -187,10 +189,13 @@ public sealed class CombinedRuntimeAdapter : IExportAdapter { cancellationToken.ThrowIfCancellationRequested(); - var content = await context.DataFetcher.FetchAsync(item, cancellationToken); + var content = await context.DataFetcher.FetchAsync(item, cancellationToken); if (!content.Success) { - itemResults.Add(AdapterItemResult.Failed(item.ItemId, content.ErrorMessage ?? "Failed to fetch")); + itemResults.Add(AdapterItemResult.Failed( + item.ItemId, + content.ErrorMessage ?? "Failed to fetch", + context.TimeProvider)); continue; } @@ -220,7 +225,10 @@ public sealed class CombinedRuntimeAdapter : IExportAdapter var content = await context.DataFetcher.FetchAsync(item, cancellationToken); if (!content.Success) { - itemResults.Add(AdapterItemResult.Failed(item.ItemId, content.ErrorMessage ?? "Failed to fetch")); + itemResults.Add(AdapterItemResult.Failed( + item.ItemId, + content.ErrorMessage ?? "Failed to fetch", + context.TimeProvider)); continue; } diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/Adapters/ExportAdapterModels.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/Adapters/ExportAdapterModels.cs index 03766192b..a4c48c0f7 100644 --- a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/Adapters/ExportAdapterModels.cs +++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/Adapters/ExportAdapterModels.cs @@ -58,13 +58,13 @@ public sealed record AdapterItemResult public DateTimeOffset ProcessedAt { get; init; } - public static AdapterItemResult Failed(Guid itemId, string errorMessage) + public static AdapterItemResult Failed(Guid itemId, string errorMessage, TimeProvider? timeProvider = null) => new() { ItemId = itemId, Success = false, ErrorMessage = errorMessage, - ProcessedAt = DateTimeOffset.UtcNow + ProcessedAt = (timeProvider ?? TimeProvider.System).GetUtcNow() }; } @@ -87,8 +87,13 @@ public sealed record ExportAdapterResult public DateTimeOffset CompletedAt { get; init; } - public static ExportAdapterResult Failed(string errorMessage) - => new() { Success = false, ErrorMessage = errorMessage, CompletedAt = DateTimeOffset.UtcNow }; + public static ExportAdapterResult Failed(string errorMessage, TimeProvider? timeProvider = null) + => new() + { + Success = false, + ErrorMessage = errorMessage, + CompletedAt = (timeProvider ?? TimeProvider.System).GetUtcNow() + }; } /// diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/Adapters/IExportAdapter.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/Adapters/IExportAdapter.cs index 3dde587a8..e76908197 100644 --- a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/Adapters/IExportAdapter.cs +++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/Adapters/IExportAdapter.cs @@ -1,3 +1,4 @@ +using StellaOps.Determinism; using StellaOps.ExportCenter.Core.Planner; namespace StellaOps.ExportCenter.Core.Adapters; @@ -88,6 +89,11 @@ public sealed record ExportAdapterContext /// Time provider for deterministic timestamps. /// public TimeProvider TimeProvider { get; init; } = TimeProvider.System; + + /// + /// GUID provider for deterministic identifiers. + /// + public IGuidProvider GuidProvider { get; init; } = SystemGuidProvider.Instance; } /// diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/Adapters/JsonPolicyAdapter.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/Adapters/JsonPolicyAdapter.cs index ea8489712..51b8ccd30 100644 --- a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/Adapters/JsonPolicyAdapter.cs +++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/Adapters/JsonPolicyAdapter.cs @@ -78,7 +78,9 @@ public sealed class JsonPolicyAdapter : IExportAdapter } else { - return ExportAdapterResult.Failed(ndjsonResult.ErrorMessage ?? "NDJSON export failed"); + return ExportAdapterResult.Failed( + ndjsonResult.ErrorMessage ?? "NDJSON export failed", + context.TimeProvider); } } else @@ -131,12 +133,12 @@ public sealed class JsonPolicyAdapter : IExportAdapter } catch (OperationCanceledException) { - return ExportAdapterResult.Failed("Export cancelled"); + return ExportAdapterResult.Failed("Export cancelled", context.TimeProvider); } catch (Exception ex) { _logger.LogError(ex, "JSON policy export failed"); - return ExportAdapterResult.Failed($"Export failed: {ex.Message}"); + return ExportAdapterResult.Failed($"Export failed: {ex.Message}", context.TimeProvider); } } @@ -185,19 +187,25 @@ public sealed class JsonPolicyAdapter : IExportAdapter var content = await context.DataFetcher.FetchAsync(item, cancellationToken); if (!content.Success) { - return AdapterItemResult.Failed(item.ItemId, content.ErrorMessage ?? "Failed to fetch content"); + return AdapterItemResult.Failed( + item.ItemId, + content.ErrorMessage ?? "Failed to fetch content", + context.TimeProvider); } if (string.IsNullOrEmpty(content.JsonContent)) { - return AdapterItemResult.Failed(item.ItemId, "Item content is empty"); + return AdapterItemResult.Failed(item.ItemId, "Item content is empty", context.TimeProvider); } // Normalize the data content var normalized = _normalizer.Normalize(content.JsonContent); if (!normalized.Success) { - return AdapterItemResult.Failed(item.ItemId, normalized.ErrorMessage ?? "Normalization failed"); + return AdapterItemResult.Failed( + item.ItemId, + normalized.ErrorMessage ?? "Normalization failed", + context.TimeProvider); } // Get policy metadata if evaluator is available @@ -223,12 +231,15 @@ public sealed class JsonPolicyAdapter : IExportAdapter if (compression != CompressionFormat.None) { var compressed = _compressor.CompressBytes(outputBytes, compression); - if (!compressed.Success) - { - return AdapterItemResult.Failed(item.ItemId, compressed.ErrorMessage ?? "Compression failed"); + if (!compressed.Success) + { + return AdapterItemResult.Failed( + item.ItemId, + compressed.ErrorMessage ?? "Compression failed", + context.TimeProvider); + } + outputBytes = compressed.CompressedData!; } - outputBytes = compressed.CompressedData!; - } // Write to file var fileName = BuildFileName(item, context.Config); @@ -257,7 +268,7 @@ public sealed class JsonPolicyAdapter : IExportAdapter catch (Exception ex) { _logger.LogWarning(ex, "Failed to process item {ItemId}", item.ItemId); - return AdapterItemResult.Failed(item.ItemId, ex.Message); + return AdapterItemResult.Failed(item.ItemId, ex.Message, context.TimeProvider); } } @@ -307,20 +318,26 @@ public sealed class JsonPolicyAdapter : IExportAdapter var content = await context.DataFetcher.FetchAsync(item, cancellationToken); if (!content.Success) { - itemResults.Add(AdapterItemResult.Failed(item.ItemId, content.ErrorMessage ?? "Failed to fetch")); + itemResults.Add(AdapterItemResult.Failed( + item.ItemId, + content.ErrorMessage ?? "Failed to fetch", + context.TimeProvider)); continue; } if (string.IsNullOrEmpty(content.JsonContent)) { - itemResults.Add(AdapterItemResult.Failed(item.ItemId, "Empty content")); + itemResults.Add(AdapterItemResult.Failed(item.ItemId, "Empty content", context.TimeProvider)); continue; } var normalized = _normalizer.Normalize(content.JsonContent); if (!normalized.Success) { - itemResults.Add(AdapterItemResult.Failed(item.ItemId, normalized.ErrorMessage ?? "Normalization failed")); + itemResults.Add(AdapterItemResult.Failed( + item.ItemId, + normalized.ErrorMessage ?? "Normalization failed", + context.TimeProvider)); continue; } @@ -348,7 +365,7 @@ public sealed class JsonPolicyAdapter : IExportAdapter } catch (Exception ex) { - itemResults.Add(AdapterItemResult.Failed(item.ItemId, ex.Message)); + itemResults.Add(AdapterItemResult.Failed(item.ItemId, ex.Message, context.TimeProvider)); } } diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/Adapters/JsonRawAdapter.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/Adapters/JsonRawAdapter.cs index b45f6fc42..db23c2cbd 100644 --- a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/Adapters/JsonRawAdapter.cs +++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/Adapters/JsonRawAdapter.cs @@ -68,7 +68,9 @@ public sealed class JsonRawAdapter : IExportAdapter } else { - return ExportAdapterResult.Failed(ndjsonResult.ErrorMessage ?? "NDJSON export failed"); + return ExportAdapterResult.Failed( + ndjsonResult.ErrorMessage ?? "NDJSON export failed", + context.TimeProvider); } } else @@ -124,12 +126,12 @@ public sealed class JsonRawAdapter : IExportAdapter } catch (OperationCanceledException) { - return ExportAdapterResult.Failed("Export cancelled"); + return ExportAdapterResult.Failed("Export cancelled", context.TimeProvider); } catch (Exception ex) { _logger.LogError(ex, "JSON raw export failed"); - return ExportAdapterResult.Failed($"Export failed: {ex.Message}"); + return ExportAdapterResult.Failed($"Export failed: {ex.Message}", context.TimeProvider); } } @@ -178,19 +180,25 @@ public sealed class JsonRawAdapter : IExportAdapter var content = await context.DataFetcher.FetchAsync(item, cancellationToken); if (!content.Success) { - return AdapterItemResult.Failed(item.ItemId, content.ErrorMessage ?? "Failed to fetch content"); + return AdapterItemResult.Failed( + item.ItemId, + content.ErrorMessage ?? "Failed to fetch content", + context.TimeProvider); } if (string.IsNullOrEmpty(content.JsonContent)) { - return AdapterItemResult.Failed(item.ItemId, "Item content is empty"); + return AdapterItemResult.Failed(item.ItemId, "Item content is empty", context.TimeProvider); } // Normalize JSON var normalized = _normalizer.Normalize(content.JsonContent); if (!normalized.Success) { - return AdapterItemResult.Failed(item.ItemId, normalized.ErrorMessage ?? "Normalization failed"); + return AdapterItemResult.Failed( + item.ItemId, + normalized.ErrorMessage ?? "Normalization failed", + context.TimeProvider); } // Apply pretty print if requested @@ -209,7 +217,10 @@ public sealed class JsonRawAdapter : IExportAdapter var compressed = _compressor.CompressBytes(outputBytes, compression); if (!compressed.Success) { - return AdapterItemResult.Failed(item.ItemId, compressed.ErrorMessage ?? "Compression failed"); + return AdapterItemResult.Failed( + item.ItemId, + compressed.ErrorMessage ?? "Compression failed", + context.TimeProvider); } outputBytes = compressed.CompressedData!; } @@ -241,7 +252,7 @@ public sealed class JsonRawAdapter : IExportAdapter catch (Exception ex) { _logger.LogWarning(ex, "Failed to process item {ItemId}", item.ItemId); - return AdapterItemResult.Failed(item.ItemId, ex.Message); + return AdapterItemResult.Failed(item.ItemId, ex.Message, context.TimeProvider); } } @@ -261,20 +272,26 @@ public sealed class JsonRawAdapter : IExportAdapter var content = await context.DataFetcher.FetchAsync(item, cancellationToken); if (!content.Success) { - itemResults.Add(AdapterItemResult.Failed(item.ItemId, content.ErrorMessage ?? "Failed to fetch")); + itemResults.Add(AdapterItemResult.Failed( + item.ItemId, + content.ErrorMessage ?? "Failed to fetch", + context.TimeProvider)); continue; } if (string.IsNullOrEmpty(content.JsonContent)) { - itemResults.Add(AdapterItemResult.Failed(item.ItemId, "Empty content")); + itemResults.Add(AdapterItemResult.Failed(item.ItemId, "Empty content", context.TimeProvider)); continue; } var normalized = _normalizer.Normalize(content.JsonContent); if (!normalized.Success) { - itemResults.Add(AdapterItemResult.Failed(item.ItemId, normalized.ErrorMessage ?? "Normalization failed")); + itemResults.Add(AdapterItemResult.Failed( + item.ItemId, + normalized.ErrorMessage ?? "Normalization failed", + context.TimeProvider)); continue; } @@ -292,7 +309,7 @@ public sealed class JsonRawAdapter : IExportAdapter } catch (Exception ex) { - itemResults.Add(AdapterItemResult.Failed(item.ItemId, ex.Message)); + itemResults.Add(AdapterItemResult.Failed(item.ItemId, ex.Message, context.TimeProvider)); } } diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/Adapters/MirrorAdapter.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/Adapters/MirrorAdapter.cs index aa5e1ee35..8d63007f3 100644 --- a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/Adapters/MirrorAdapter.cs +++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/Adapters/MirrorAdapter.cs @@ -50,7 +50,7 @@ public sealed class MirrorAdapter : IExportAdapter context.Items.Count); // Create temp directory for staging files - var tempDir = Path.Combine(Path.GetTempPath(), $"mirror-{Guid.NewGuid():N}"); + var tempDir = Path.Combine(Path.GetTempPath(), $"mirror-{context.GuidProvider.NewGuid():N}"); Directory.CreateDirectory(tempDir); try @@ -81,7 +81,7 @@ public sealed class MirrorAdapter : IExportAdapter // Build the mirror bundle var request = new MirrorBundleBuildRequest( - Guid.TryParse(context.CorrelationId, out var runId) ? runId : Guid.NewGuid(), + Guid.TryParse(context.CorrelationId, out var runId) ? runId : context.GuidProvider.NewGuid(), context.TenantId, MirrorBundleVariant.Full, selectors, @@ -176,7 +176,7 @@ public sealed class MirrorAdapter : IExportAdapter catch (Exception ex) { _logger.LogError(ex, "Failed to build mirror bundle"); - return ExportAdapterResult.Failed($"Mirror bundle build failed: {ex.Message}"); + return ExportAdapterResult.Failed($"Mirror bundle build failed: {ex.Message}", context.TimeProvider); } } @@ -297,13 +297,13 @@ public sealed class MirrorAdapter : IExportAdapter OutputPath = tempFilePath, OutputSizeBytes = new FileInfo(tempFilePath).Length, ContentHash = content.OriginalHash, - ProcessedAt = DateTimeOffset.UtcNow + ProcessedAt = context.TimeProvider.GetUtcNow() }); } catch (Exception ex) { _logger.LogWarning(ex, "Failed to process item {ItemId}", item.ItemId); - itemResults.Add(AdapterItemResult.Failed(item.ItemId, ex.Message)); + itemResults.Add(AdapterItemResult.Failed(item.ItemId, ex.Message, context.TimeProvider)); } } diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/Adapters/MirrorDeltaAdapter.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/Adapters/MirrorDeltaAdapter.cs index 28078d59b..77d294797 100644 --- a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/Adapters/MirrorDeltaAdapter.cs +++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/Adapters/MirrorDeltaAdapter.cs @@ -60,7 +60,8 @@ public sealed class MirrorDeltaAdapter : IExportAdapter if (deltaOptions is null) { return ExportAdapterResult.Failed( - "Delta options required: provide 'baseExportId' and 'baseManifestDigest' in context metadata"); + "Delta options required: provide 'baseExportId' and 'baseManifestDigest' in context metadata", + context.TimeProvider); } _logger.LogInformation( @@ -68,7 +69,7 @@ public sealed class MirrorDeltaAdapter : IExportAdapter deltaOptions.BaseExportId, context.Items.Count); // Create temp directory for staging files - var tempDir = Path.Combine(Path.GetTempPath(), $"mirror-delta-{Guid.NewGuid():N}"); + var tempDir = Path.Combine(Path.GetTempPath(), $"mirror-delta-{context.GuidProvider.NewGuid():N}"); Directory.CreateDirectory(tempDir); try @@ -100,7 +101,9 @@ public sealed class MirrorDeltaAdapter : IExportAdapter var deltaResult = await _deltaService.ComputeDeltaAsync(deltaRequest, cancellationToken); if (!deltaResult.Success) { - return ExportAdapterResult.Failed(deltaResult.ErrorMessage ?? "Delta computation failed"); + return ExportAdapterResult.Failed( + deltaResult.ErrorMessage ?? "Delta computation failed", + context.TimeProvider); } // If no changes, return early with empty delta @@ -123,7 +126,7 @@ public sealed class MirrorDeltaAdapter : IExportAdapter // Create the delta bundle request var bundleRequest = new MirrorBundleBuildRequest( - Guid.TryParse(context.CorrelationId, out var runId) ? runId : Guid.NewGuid(), + Guid.TryParse(context.CorrelationId, out var runId) ? runId : context.GuidProvider.NewGuid(), context.TenantId, MirrorBundleVariant.Delta, selectors, @@ -236,7 +239,9 @@ public sealed class MirrorDeltaAdapter : IExportAdapter catch (Exception ex) { _logger.LogError(ex, "Failed to build mirror delta bundle"); - return ExportAdapterResult.Failed($"Mirror delta bundle build failed: {ex.Message}"); + return ExportAdapterResult.Failed( + $"Mirror delta bundle build failed: {ex.Message}", + context.TimeProvider); } } @@ -320,7 +325,8 @@ public sealed class MirrorDeltaAdapter : IExportAdapter { itemResults.Add(AdapterItemResult.Failed( item.ItemId, - content.ErrorMessage ?? "Failed to fetch content or content is empty")); + content.ErrorMessage ?? "Failed to fetch content or content is empty", + context.TimeProvider)); continue; } @@ -330,7 +336,8 @@ public sealed class MirrorDeltaAdapter : IExportAdapter { itemResults.Add(AdapterItemResult.Failed( item.ItemId, - $"Unknown item kind: {item.Kind}")); + $"Unknown item kind: {item.Kind}", + context.TimeProvider)); continue; } @@ -388,7 +395,7 @@ public sealed class MirrorDeltaAdapter : IExportAdapter catch (Exception ex) { _logger.LogWarning(ex, "Failed to process item {ItemId}", item.ItemId); - itemResults.Add(AdapterItemResult.Failed(item.ItemId, ex.Message)); + itemResults.Add(AdapterItemResult.Failed(item.ItemId, ex.Message, context.TimeProvider)); } } diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/Adapters/Trivy/TrivyDbAdapter.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/Adapters/Trivy/TrivyDbAdapter.cs index 995dfa48b..0e68424ba 100644 --- a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/Adapters/Trivy/TrivyDbAdapter.cs +++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/Adapters/Trivy/TrivyDbAdapter.cs @@ -71,11 +71,12 @@ public sealed class TrivyDbAdapter : IExportAdapter if (_options.SchemaVersion != SupportedSchemaVersion) { return ExportAdapterResult.Failed( - $"Unsupported Trivy DB schema version {_options.SchemaVersion}. Only v{SupportedSchemaVersion} is supported."); + $"Unsupported Trivy DB schema version {_options.SchemaVersion}. Only v{SupportedSchemaVersion} is supported.", + context.TimeProvider); } // Create temp directory for staging - var tempDir = Path.Combine(Path.GetTempPath(), $"trivy-db-{Guid.NewGuid():N}"); + var tempDir = Path.Combine(Path.GetTempPath(), $"trivy-db-{context.GuidProvider.NewGuid():N}"); Directory.CreateDirectory(tempDir); try @@ -100,7 +101,8 @@ public sealed class TrivyDbAdapter : IExportAdapter if (totalVulnCount == 0 && !_options.AllowEmpty) { return ExportAdapterResult.Failed( - "No vulnerabilities mapped. Set AllowEmpty=true to allow empty bundles."); + "No vulnerabilities mapped. Set AllowEmpty=true to allow empty bundles.", + context.TimeProvider); } _logger.LogInformation( @@ -202,7 +204,9 @@ public sealed class TrivyDbAdapter : IExportAdapter catch (Exception ex) { _logger.LogError(ex, "Failed to build Trivy DB bundle"); - return ExportAdapterResult.Failed($"Trivy DB bundle build failed: {ex.Message}"); + return ExportAdapterResult.Failed( + $"Trivy DB bundle build failed: {ex.Message}", + context.TimeProvider); } } @@ -285,7 +289,8 @@ public sealed class TrivyDbAdapter : IExportAdapter { itemResults.Add(AdapterItemResult.Failed( item.ItemId, - content.ErrorMessage ?? "Failed to fetch content or content is empty")); + content.ErrorMessage ?? "Failed to fetch content or content is empty", + context.TimeProvider)); continue; } @@ -298,7 +303,7 @@ public sealed class TrivyDbAdapter : IExportAdapter { ItemId = item.ItemId, Success = true, - ProcessedAt = DateTimeOffset.UtcNow + ProcessedAt = context.TimeProvider.GetUtcNow() }); continue; } @@ -327,13 +332,13 @@ public sealed class TrivyDbAdapter : IExportAdapter ItemId = item.ItemId, Success = true, ContentHash = content.OriginalHash, - ProcessedAt = DateTimeOffset.UtcNow + ProcessedAt = context.TimeProvider.GetUtcNow() }); } catch (Exception ex) { _logger.LogWarning(ex, "Failed to process item {ItemId}", item.ItemId); - itemResults.Add(AdapterItemResult.Failed(item.ItemId, ex.Message)); + itemResults.Add(AdapterItemResult.Failed(item.ItemId, ex.Message, context.TimeProvider)); } } } @@ -384,7 +389,7 @@ public sealed class TrivyDbAdapter : IExportAdapter int vulnerabilityCount) { var now = context.TimeProvider.GetUtcNow(); - var runId = Guid.TryParse(context.CorrelationId, out var id) ? id : Guid.NewGuid(); + var runId = Guid.TryParse(context.CorrelationId, out var id) ? id : context.GuidProvider.NewGuid(); return new TrivyDbMetadata { diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/Adapters/Trivy/TrivyJavaDbAdapter.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/Adapters/Trivy/TrivyJavaDbAdapter.cs index 7fc8266f9..87d913c9e 100644 --- a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/Adapters/Trivy/TrivyJavaDbAdapter.cs +++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/Adapters/Trivy/TrivyJavaDbAdapter.cs @@ -77,7 +77,7 @@ public sealed class TrivyJavaDbAdapter : IExportAdapter context.Items.Count); // Create temp directory for staging - var tempDir = Path.Combine(Path.GetTempPath(), $"trivy-java-db-{Guid.NewGuid():N}"); + var tempDir = Path.Combine(Path.GetTempPath(), $"trivy-java-db-{context.GuidProvider.NewGuid():N}"); Directory.CreateDirectory(tempDir); try @@ -110,7 +110,8 @@ public sealed class TrivyJavaDbAdapter : IExportAdapter if (totalVulnCount == 0 && !_options.AllowEmpty) { return ExportAdapterResult.Failed( - "No Java vulnerabilities mapped. Set AllowEmpty=true to allow empty bundles."); + "No Java vulnerabilities mapped. Set AllowEmpty=true to allow empty bundles.", + context.TimeProvider); } _logger.LogInformation( @@ -209,7 +210,9 @@ public sealed class TrivyJavaDbAdapter : IExportAdapter catch (Exception ex) { _logger.LogError(ex, "Failed to build Trivy Java DB bundle"); - return ExportAdapterResult.Failed($"Trivy Java DB bundle build failed: {ex.Message}"); + return ExportAdapterResult.Failed( + $"Trivy Java DB bundle build failed: {ex.Message}", + context.TimeProvider); } } @@ -286,7 +289,8 @@ public sealed class TrivyJavaDbAdapter : IExportAdapter { itemResults.Add(AdapterItemResult.Failed( item.ItemId, - content.ErrorMessage ?? "Failed to fetch content or content is empty")); + content.ErrorMessage ?? "Failed to fetch content or content is empty", + context.TimeProvider)); continue; } @@ -299,7 +303,7 @@ public sealed class TrivyJavaDbAdapter : IExportAdapter { ItemId = item.ItemId, Success = true, - ProcessedAt = DateTimeOffset.UtcNow + ProcessedAt = context.TimeProvider.GetUtcNow() }); continue; } @@ -359,13 +363,13 @@ public sealed class TrivyJavaDbAdapter : IExportAdapter ItemId = item.ItemId, Success = true, ContentHash = content.OriginalHash, - ProcessedAt = DateTimeOffset.UtcNow + ProcessedAt = context.TimeProvider.GetUtcNow() }); } catch (Exception ex) { _logger.LogWarning(ex, "Failed to process item {ItemId}", item.ItemId); - itemResults.Add(AdapterItemResult.Failed(item.ItemId, ex.Message)); + itemResults.Add(AdapterItemResult.Failed(item.ItemId, ex.Message, context.TimeProvider)); } } } @@ -454,7 +458,7 @@ public sealed class TrivyJavaDbAdapter : IExportAdapter int vulnerabilityCount) { var now = context.TimeProvider.GetUtcNow(); - var runId = Guid.TryParse(context.CorrelationId, out var id) ? id : Guid.NewGuid(); + var runId = Guid.TryParse(context.CorrelationId, out var id) ? id : context.GuidProvider.NewGuid(); return new TrivyJavaDbMetadata { diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/Planner/ExportScopeResolver.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/Planner/ExportScopeResolver.cs index eb2b601ea..6b93e7b68 100644 --- a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/Planner/ExportScopeResolver.cs +++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/Planner/ExportScopeResolver.cs @@ -1,3 +1,6 @@ +using System.Buffers.Binary; +using System.Security.Cryptography; +using System.Text; using System.Text.RegularExpressions; using Microsoft.Extensions.Logging; @@ -40,7 +43,7 @@ public sealed class ExportScopeResolver : IExportScopeResolver var items = GenerateResolvedItems(tenantId, scope); // Apply sampling if configured - var (sampledItems, samplingMetadata) = ApplySampling(items, scope.Sampling); + var (sampledItems, samplingMetadata) = ApplySampling(items, scope.Sampling, tenantId, scope); // Apply max items limit var maxItems = scope.MaxItems ?? DefaultMaxItems; @@ -223,7 +226,7 @@ public sealed class ExportScopeResolver : IExportScopeResolver foreach (var sourceRef in scope.SourceRefs) { var kind = scope.TargetKinds.FirstOrDefault() ?? "sbom"; - items.Add(CreateResolvedItem(sourceRef, kind, now)); + items.Add(CreateResolvedItem(tenantId, sourceRef, kind, now)); } } else @@ -237,7 +240,7 @@ public sealed class ExportScopeResolver : IExportScopeResolver for (var i = 0; i < itemsPerKind; i++) { var sourceRef = $"{kind}-{tenantId:N}-{i:D4}"; - items.Add(CreateResolvedItem(sourceRef, kind, now.AddHours(-i))); + items.Add(CreateResolvedItem(tenantId, sourceRef, kind, now.AddHours(-i))); } } } @@ -285,11 +288,12 @@ public sealed class ExportScopeResolver : IExportScopeResolver return items; } - private ResolvedExportItem CreateResolvedItem(string sourceRef, string kind, DateTimeOffset createdAt) + private static ResolvedExportItem CreateResolvedItem(Guid tenantId, string sourceRef, string kind, DateTimeOffset createdAt) { + var itemId = CreateDeterministicItemId(tenantId, sourceRef, kind); return new ResolvedExportItem { - ItemId = Guid.NewGuid(), + ItemId = itemId, Kind = kind, SourceRef = sourceRef, Name = $"{kind}-{sourceRef}", @@ -308,14 +312,16 @@ public sealed class ExportScopeResolver : IExportScopeResolver private static (List Items, SamplingMetadata? Metadata) ApplySampling( List items, - SamplingConfig? sampling) + SamplingConfig? sampling, + Guid tenantId, + ExportScope scope) { if (sampling is null || sampling.Strategy == SamplingStrategy.None) { return (items, null); } - var seed = sampling.Seed ?? Environment.TickCount; + var seed = sampling.Seed ?? ComputeDeterministicSeed(tenantId, scope); var size = Math.Min(sampling.Size, items.Count); List sampled; @@ -382,4 +388,66 @@ public sealed class ExportScopeResolver : IExportScopeResolver _ => item.Metadata.TryGetValue(field, out var value) ? value : "unknown" }; } + + private static Guid CreateDeterministicItemId(Guid tenantId, string sourceRef, string kind) + { + var seed = $"{tenantId:D}|{kind}|{sourceRef}"; + var hash = SHA256.HashData(Encoding.UTF8.GetBytes(seed)); + return new Guid(hash.AsSpan(0, 16).ToArray()); + } + + private static int ComputeDeterministicSeed(Guid tenantId, ExportScope scope) + { + var builder = new StringBuilder(); + builder.Append("tenant=").Append(tenantId.ToString("D")); + AppendList(builder, "targets", scope.TargetKinds); + AppendList(builder, "sources", scope.SourceRefs); + AppendList(builder, "tags", scope.Tags); + AppendList(builder, "namespaces", scope.Namespaces); + AppendList(builder, "exclude", scope.ExcludePatterns); + AppendList(builder, "runIds", scope.RunIds.Select(id => id.ToString("D")).ToList()); + + if (scope.DateRange is not null) + { + builder.Append("|dateField=").Append(scope.DateRange.Field.ToString()); + if (scope.DateRange.From.HasValue) + { + builder.Append("|dateFrom=").Append(scope.DateRange.From.Value.ToString("O", CultureInfo.InvariantCulture)); + } + if (scope.DateRange.To.HasValue) + { + builder.Append("|dateTo=").Append(scope.DateRange.To.Value.ToString("O", CultureInfo.InvariantCulture)); + } + } + + if (scope.MaxItems.HasValue) + { + builder.Append("|maxItems=").Append(scope.MaxItems.Value.ToString(CultureInfo.InvariantCulture)); + } + + if (scope.Sampling is not null) + { + builder.Append("|sampling=").Append(scope.Sampling.Strategy.ToString()); + builder.Append("|sampleSize=").Append(scope.Sampling.Size.ToString(CultureInfo.InvariantCulture)); + if (!string.IsNullOrWhiteSpace(scope.Sampling.StratifyBy)) + { + builder.Append("|stratifyBy=").Append(scope.Sampling.StratifyBy); + } + } + + var hash = SHA256.HashData(Encoding.UTF8.GetBytes(builder.ToString())); + return BinaryPrimitives.ReadInt32LittleEndian(hash.AsSpan(0, 4)); + } + + private static void AppendList(StringBuilder builder, string label, IReadOnlyList values) + { + if (values.Count == 0) + { + return; + } + + builder.Append('|').Append(label).Append('='); + var ordered = values.OrderBy(v => v, StringComparer.Ordinal); + builder.Append(string.Join(",", ordered)); + } } diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/Api/ExportApiEndpointsTests.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/Api/ExportApiEndpointsTests.cs new file mode 100644 index 000000000..d495d8ba5 --- /dev/null +++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/Api/ExportApiEndpointsTests.cs @@ -0,0 +1,40 @@ +using System.Reflection; +using StellaOps.ExportCenter.Core.Domain; +using StellaOps.ExportCenter.WebService.Api; +using Xunit; + +namespace StellaOps.ExportCenter.Tests.Api; + +public sealed class ExportApiEndpointsTests +{ + [Fact] + public void MapToProfileResponse_InvalidJson_ReturnsNullConfig() + { + var now = new DateTimeOffset(2025, 1, 2, 14, 0, 0, TimeSpan.Zero); + var profile = new ExportProfile + { + ProfileId = Guid.NewGuid(), + TenantId = Guid.NewGuid(), + Name = "test", + Kind = ExportProfileKind.AdHoc, + Status = ExportProfileStatus.Active, + ScopeJson = "{invalid", + FormatJson = "{invalid", + SigningJson = "{invalid", + CreatedAt = now, + UpdatedAt = now + }; + + var method = typeof(ExportApiEndpoints).GetMethod( + "MapToProfileResponse", + BindingFlags.NonPublic | BindingFlags.Static); + + Assert.NotNull(method); + + var response = (ExportProfileResponse)method!.Invoke(null, new object[] { profile })!; + + Assert.Null(response.Scope); + Assert.Null(response.Format); + Assert.Null(response.Signing); + } +} diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/Api/ExportApiRepositoryTests.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/Api/ExportApiRepositoryTests.cs index ad53f5ffe..f5ae6e2ea 100644 --- a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/Api/ExportApiRepositoryTests.cs +++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/Api/ExportApiRepositoryTests.cs @@ -1,12 +1,19 @@ using Microsoft.Extensions.Logging.Abstractions; using StellaOps.ExportCenter.Core.Domain; using StellaOps.ExportCenter.WebService.Api; +using StellaOps.TestKit; namespace StellaOps.ExportCenter.Tests.Api; public class ExportApiRepositoryTests { - private readonly Guid _tenantId = Guid.NewGuid(); + private readonly Guid _tenantId = Guid.Parse("00000000-0000-0000-0000-000000000001"); + private readonly FakeTimeProvider _timeProvider; + + public ExportApiRepositoryTests() + { + _timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero)); + } // ======================================================================== // Profile Repository Tests @@ -16,7 +23,7 @@ public class ExportApiRepositoryTests public async Task ProfileRepo_CreateAsync_StoresProfile() { // Arrange - var repo = new InMemoryExportProfileRepository(NullLogger.Instance); + var repo = new InMemoryExportProfileRepository(NullLogger.Instance, _timeProvider); var profile = CreateTestProfile(); // Act @@ -31,7 +38,7 @@ public class ExportApiRepositoryTests public async Task ProfileRepo_GetByIdAsync_ReturnsStoredProfile() { // Arrange - var repo = new InMemoryExportProfileRepository(NullLogger.Instance); + var repo = new InMemoryExportProfileRepository(NullLogger.Instance, _timeProvider); var profile = CreateTestProfile(); await repo.CreateAsync(profile); @@ -48,7 +55,7 @@ public class ExportApiRepositoryTests public async Task ProfileRepo_GetByIdAsync_ReturnsNull_WhenNotFound() { // Arrange - var repo = new InMemoryExportProfileRepository(NullLogger.Instance); + var repo = new InMemoryExportProfileRepository(NullLogger.Instance, _timeProvider); // Act var retrieved = await repo.GetByIdAsync(_tenantId, Guid.NewGuid()); @@ -61,7 +68,7 @@ public class ExportApiRepositoryTests public async Task ProfileRepo_GetByIdAsync_ReturnsNull_WhenWrongTenant() { // Arrange - var repo = new InMemoryExportProfileRepository(NullLogger.Instance); + var repo = new InMemoryExportProfileRepository(NullLogger.Instance, _timeProvider); var profile = CreateTestProfile(); await repo.CreateAsync(profile); @@ -76,7 +83,7 @@ public class ExportApiRepositoryTests public async Task ProfileRepo_ListAsync_ReturnsAllProfilesForTenant() { // Arrange - var repo = new InMemoryExportProfileRepository(NullLogger.Instance); + var repo = new InMemoryExportProfileRepository(NullLogger.Instance, _timeProvider); var profile1 = CreateTestProfile("Profile 1"); var profile2 = CreateTestProfile("Profile 2"); var otherTenantProfile = CreateTestProfile("Other Tenant") with { TenantId = Guid.NewGuid() }; @@ -98,7 +105,7 @@ public class ExportApiRepositoryTests public async Task ProfileRepo_ListAsync_FiltersByStatus() { // Arrange - var repo = new InMemoryExportProfileRepository(NullLogger.Instance); + var repo = new InMemoryExportProfileRepository(NullLogger.Instance, _timeProvider); var activeProfile = CreateTestProfile("Active") with { Status = ExportProfileStatus.Active }; var draftProfile = CreateTestProfile("Draft") with { Status = ExportProfileStatus.Draft }; @@ -118,7 +125,7 @@ public class ExportApiRepositoryTests public async Task ProfileRepo_ListAsync_FiltersByKind() { // Arrange - var repo = new InMemoryExportProfileRepository(NullLogger.Instance); + var repo = new InMemoryExportProfileRepository(NullLogger.Instance, _timeProvider); var adhocProfile = CreateTestProfile("AdHoc") with { Kind = ExportProfileKind.AdHoc }; var scheduledProfile = CreateTestProfile("Scheduled") with { Kind = ExportProfileKind.Scheduled }; @@ -138,7 +145,7 @@ public class ExportApiRepositoryTests public async Task ProfileRepo_ListAsync_SearchesByName() { // Arrange - var repo = new InMemoryExportProfileRepository(NullLogger.Instance); + var repo = new InMemoryExportProfileRepository(NullLogger.Instance, _timeProvider); var profile1 = CreateTestProfile("Daily SBOM Export"); var profile2 = CreateTestProfile("Weekly VEX Export"); @@ -158,7 +165,7 @@ public class ExportApiRepositoryTests public async Task ProfileRepo_UpdateAsync_ModifiesProfile() { // Arrange - var repo = new InMemoryExportProfileRepository(NullLogger.Instance); + var repo = new InMemoryExportProfileRepository(NullLogger.Instance, _timeProvider); var profile = CreateTestProfile(); await repo.CreateAsync(profile); @@ -179,7 +186,7 @@ public class ExportApiRepositoryTests public async Task ProfileRepo_ArchiveAsync_SetsArchivedStatus() { // Arrange - var repo = new InMemoryExportProfileRepository(NullLogger.Instance); + var repo = new InMemoryExportProfileRepository(NullLogger.Instance, _timeProvider); var profile = CreateTestProfile(); await repo.CreateAsync(profile); @@ -193,13 +200,15 @@ public class ExportApiRepositoryTests Assert.NotNull(retrieved); Assert.Equal(ExportProfileStatus.Archived, retrieved.Status); Assert.NotNull(retrieved.ArchivedAt); + Assert.Equal(_timeProvider.GetUtcNow(), retrieved.ArchivedAt); + Assert.Equal(_timeProvider.GetUtcNow(), retrieved.UpdatedAt); } [Fact] public async Task ProfileRepo_IsNameUniqueAsync_ReturnsTrueForUniqueName() { // Arrange - var repo = new InMemoryExportProfileRepository(NullLogger.Instance); + var repo = new InMemoryExportProfileRepository(NullLogger.Instance, _timeProvider); var profile = CreateTestProfile("Existing Profile"); await repo.CreateAsync(profile); @@ -214,7 +223,7 @@ public class ExportApiRepositoryTests public async Task ProfileRepo_IsNameUniqueAsync_ReturnsFalseForDuplicateName() { // Arrange - var repo = new InMemoryExportProfileRepository(NullLogger.Instance); + var repo = new InMemoryExportProfileRepository(NullLogger.Instance, _timeProvider); var profile = CreateTestProfile("Existing Profile"); await repo.CreateAsync(profile); @@ -229,7 +238,7 @@ public class ExportApiRepositoryTests public async Task ProfileRepo_IsNameUniqueAsync_ExcludesSpecifiedProfile() { // Arrange - var repo = new InMemoryExportProfileRepository(NullLogger.Instance); + var repo = new InMemoryExportProfileRepository(NullLogger.Instance, _timeProvider); var profile = CreateTestProfile("Existing Profile"); await repo.CreateAsync(profile); @@ -248,7 +257,7 @@ public class ExportApiRepositoryTests public async Task RunRepo_CreateAsync_StoresRun() { // Arrange - var repo = new InMemoryExportRunRepository(NullLogger.Instance); + var repo = new InMemoryExportRunRepository(NullLogger.Instance, _timeProvider); var run = CreateTestRun(); // Act @@ -263,7 +272,7 @@ public class ExportApiRepositoryTests public async Task RunRepo_GetByIdAsync_ReturnsStoredRun() { // Arrange - var repo = new InMemoryExportRunRepository(NullLogger.Instance); + var repo = new InMemoryExportRunRepository(NullLogger.Instance, _timeProvider); var run = CreateTestRun(); await repo.CreateAsync(run); @@ -279,7 +288,7 @@ public class ExportApiRepositoryTests public async Task RunRepo_ListAsync_FiltersByProfileId() { // Arrange - var repo = new InMemoryExportRunRepository(NullLogger.Instance); + var repo = new InMemoryExportRunRepository(NullLogger.Instance, _timeProvider); var profileId1 = Guid.NewGuid(); var profileId2 = Guid.NewGuid(); @@ -302,7 +311,7 @@ public class ExportApiRepositoryTests public async Task RunRepo_ListAsync_FiltersByStatus() { // Arrange - var repo = new InMemoryExportRunRepository(NullLogger.Instance); + var repo = new InMemoryExportRunRepository(NullLogger.Instance, _timeProvider); var runningRun = CreateTestRun() with { Status = ExportRunStatus.Running }; var completedRun = CreateTestRun() with { Status = ExportRunStatus.Completed }; @@ -322,7 +331,7 @@ public class ExportApiRepositoryTests public async Task RunRepo_CancelAsync_CancelsQueuedRun() { // Arrange - var repo = new InMemoryExportRunRepository(NullLogger.Instance); + var repo = new InMemoryExportRunRepository(NullLogger.Instance, _timeProvider); var run = CreateTestRun() with { Status = ExportRunStatus.Queued }; await repo.CreateAsync(run); @@ -334,13 +343,14 @@ public class ExportApiRepositoryTests var retrieved = await repo.GetByIdAsync(_tenantId, run.RunId); Assert.Equal(ExportRunStatus.Cancelled, retrieved?.Status); + Assert.Equal(_timeProvider.GetUtcNow(), retrieved?.CompletedAt); } [Fact] public async Task RunRepo_CancelAsync_CancelsRunningRun() { // Arrange - var repo = new InMemoryExportRunRepository(NullLogger.Instance); + var repo = new InMemoryExportRunRepository(NullLogger.Instance, _timeProvider); var run = CreateTestRun() with { Status = ExportRunStatus.Running }; await repo.CreateAsync(run); @@ -355,7 +365,7 @@ public class ExportApiRepositoryTests public async Task RunRepo_CancelAsync_ReturnsFalseForCompletedRun() { // Arrange - var repo = new InMemoryExportRunRepository(NullLogger.Instance); + var repo = new InMemoryExportRunRepository(NullLogger.Instance, _timeProvider); var run = CreateTestRun() with { Status = ExportRunStatus.Completed }; await repo.CreateAsync(run); @@ -370,7 +380,7 @@ public class ExportApiRepositoryTests public async Task RunRepo_GetActiveRunsCountAsync_CountsRunningRuns() { // Arrange - var repo = new InMemoryExportRunRepository(NullLogger.Instance); + var repo = new InMemoryExportRunRepository(NullLogger.Instance, _timeProvider); await repo.CreateAsync(CreateTestRun() with { Status = ExportRunStatus.Running }); await repo.CreateAsync(CreateTestRun() with { Status = ExportRunStatus.Running }); @@ -388,7 +398,7 @@ public class ExportApiRepositoryTests public async Task RunRepo_GetActiveRunsCountAsync_FiltersByProfileId() { // Arrange - var repo = new InMemoryExportRunRepository(NullLogger.Instance); + var repo = new InMemoryExportRunRepository(NullLogger.Instance, _timeProvider); var profileId = Guid.NewGuid(); await repo.CreateAsync(CreateTestRun() with { ProfileId = profileId, Status = ExportRunStatus.Running }); @@ -405,7 +415,7 @@ public class ExportApiRepositoryTests public async Task RunRepo_GetQueuedRunsCountAsync_CountsQueuedRuns() { // Arrange - var repo = new InMemoryExportRunRepository(NullLogger.Instance); + var repo = new InMemoryExportRunRepository(NullLogger.Instance, _timeProvider); await repo.CreateAsync(CreateTestRun() with { Status = ExportRunStatus.Queued }); await repo.CreateAsync(CreateTestRun() with { Status = ExportRunStatus.Queued }); @@ -418,6 +428,23 @@ public class ExportApiRepositoryTests Assert.Equal(2, count); } + [Fact] + public async Task RunRepo_DequeueNextRunAsync_MarksRunAsRunning() + { + // Arrange + var repo = new InMemoryExportRunRepository(NullLogger.Instance, _timeProvider); + var run = CreateTestRun() with { Status = ExportRunStatus.Queued }; + await repo.CreateAsync(run); + + // Act + var dequeued = await repo.DequeueNextRunAsync(_tenantId); + + // Assert + Assert.NotNull(dequeued); + Assert.Equal(ExportRunStatus.Running, dequeued!.Status); + Assert.Equal(_timeProvider.GetUtcNow(), dequeued.StartedAt); + } + // ======================================================================== // Artifact Repository Tests // ======================================================================== @@ -507,8 +534,8 @@ public class ExportApiRepositoryTests Description = "Test profile description", Kind = ExportProfileKind.AdHoc, Status = ExportProfileStatus.Active, - CreatedAt = DateTimeOffset.UtcNow, - UpdatedAt = DateTimeOffset.UtcNow + CreatedAt = _timeProvider.GetUtcNow(), + UpdatedAt = _timeProvider.GetUtcNow() }; } @@ -522,7 +549,7 @@ public class ExportApiRepositoryTests Status = ExportRunStatus.Running, Trigger = ExportRunTrigger.Api, CorrelationId = Guid.NewGuid().ToString(), - CreatedAt = DateTimeOffset.UtcNow + CreatedAt = _timeProvider.GetUtcNow() }; } @@ -539,7 +566,8 @@ public class ExportApiRepositoryTests SizeBytes = 1024, ContentType = "application/json", Checksum = "sha256:abc123", - CreatedAt = DateTimeOffset.UtcNow + CreatedAt = _timeProvider.GetUtcNow() }; } } + diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/Api/ExportApiServiceCollectionExtensionsTests.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/Api/ExportApiServiceCollectionExtensionsTests.cs new file mode 100644 index 000000000..c23a77aba --- /dev/null +++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/Api/ExportApiServiceCollectionExtensionsTests.cs @@ -0,0 +1,31 @@ +using Microsoft.Extensions.DependencyInjection; +using StellaOps.ExportCenter.WebService.Api; +using Xunit; + +namespace StellaOps.ExportCenter.Tests.Api; + +public sealed class ExportApiServiceCollectionExtensionsTests +{ + [Fact] + public void AddExportApiServices_Throws_WhenInMemoryNotAllowed() + { + var services = new ServiceCollection(); + + var exception = Assert.Throws(() => + services.AddExportApiServices(_ => { }, allowInMemoryRepositories: false)); + + Assert.Contains("In-memory export repositories are disabled", exception.Message); + } + + [Fact] + public void AddExportApiServices_AllowsExplicitInMemoryRegistration() + { + var services = new ServiceCollection(); + + services.AddExportApiServices(_ => { }, allowInMemoryRepositories: true); + var provider = services.BuildServiceProvider(); + + var repo = provider.GetService(); + Assert.NotNull(repo); + } +} diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/Api/ExportAuditServiceTests.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/Api/ExportAuditServiceTests.cs index 6b459ab2d..42feba29c 100644 --- a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/Api/ExportAuditServiceTests.cs +++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/Api/ExportAuditServiceTests.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.Logging.Abstractions; using StellaOps.ExportCenter.WebService.Api; +using StellaOps.Determinism; namespace StellaOps.ExportCenter.Tests.Api; @@ -12,6 +13,7 @@ public class ExportAuditServiceTests { _auditService = new ExportAuditService( NullLogger.Instance, + new SequentialGuidProvider(), TimeProvider.System); } diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/Api/OpenApiDiscoveryEndpointsTests.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/Api/OpenApiDiscoveryEndpointsTests.cs new file mode 100644 index 000000000..fa2a1629d --- /dev/null +++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/Api/OpenApiDiscoveryEndpointsTests.cs @@ -0,0 +1,59 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Hosting; +using StellaOps.ExportCenter.WebService; +using Xunit; + +namespace StellaOps.ExportCenter.Tests.Api; + +public sealed class OpenApiDiscoveryEndpointsTests +{ + [Fact] + public void MapOpenApiDiscovery_AllowsAnonymousWhenConfigured() + { + var builder = CreateBuilder(); + builder.Configuration["OpenApi:AllowAnonymous"] = "true"; + + var app = builder.Build(); + app.MapOpenApiDiscovery(); + + var endpoint = GetEndpoint(app, "/.well-known/openapi"); + var allowAnonymous = endpoint.Metadata.GetMetadata(); + + Assert.NotNull(allowAnonymous); + } + + [Fact] + public void MapOpenApiDiscovery_DoesNotAllowAnonymousWhenDisabled() + { + var builder = CreateBuilder(); + builder.Configuration["OpenApi:AllowAnonymous"] = "false"; + + var app = builder.Build(); + app.MapOpenApiDiscovery(); + + var endpoint = GetEndpoint(app, "/.well-known/openapi"); + var allowAnonymous = endpoint.Metadata.GetMetadata(); + + Assert.Null(allowAnonymous); + } + + private static RouteEndpoint GetEndpoint(IEndpointRouteBuilder app, string pattern) + { + var endpoints = app.DataSources.SelectMany(source => source.Endpoints).OfType(); + return endpoints.Single(endpoint => string.Equals(endpoint.RoutePattern.RawText, pattern, StringComparison.Ordinal)); + } + + private static WebApplicationBuilder CreateBuilder() + { + var contentRoot = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(contentRoot); + + return WebApplication.CreateBuilder(new WebApplicationOptions + { + EnvironmentName = Environments.Production, + ContentRootPath = contentRoot + }); + } +} diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/AuditBundle/AuditBundleJobHandlerTests.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/AuditBundle/AuditBundleJobHandlerTests.cs new file mode 100644 index 000000000..e5d854144 --- /dev/null +++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/AuditBundle/AuditBundleJobHandlerTests.cs @@ -0,0 +1,40 @@ +using Microsoft.Extensions.Logging.Abstractions; +using StellaOps.Determinism; +using StellaOps.ExportCenter.Client.Models; +using StellaOps.ExportCenter.Tests; +using StellaOps.ExportCenter.WebService.AuditBundle; +using Xunit; + +namespace StellaOps.ExportCenter.Tests.AuditBundle; + +public sealed class AuditBundleJobHandlerTests +{ + [Fact] + public async Task CreateBundleAsync_UsesGuidProvider() + { + var timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 1, 2, 10, 0, 0, TimeSpan.Zero)); + var guidProvider = new SequentialGuidProvider(); + var handler = new AuditBundleJobHandler( + NullLogger.Instance, + guidProvider, + timeProvider); + + var request = new CreateAuditBundleRequest( + new BundleSubjectRefDto( + "container", + "example-image", + new Dictionary { ["sha256"] = "abc123" }), + TimeWindow: null, + IncludeContent: new AuditBundleContentSelection( + VulnReports: false, + Sbom: false, + VexDecisions: false, + PolicyEvaluations: false, + Attestations: false)); + + var result = await handler.CreateBundleAsync(request, "actor-1", "Actor One", CancellationToken.None); + + Assert.NotNull(result.Response); + Assert.Equal("bndl-00000000000000000000000000000001", result.Response!.BundleId); + } +} diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/Deprecation/DeprecationHeaderExtensionsTests.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/Deprecation/DeprecationHeaderExtensionsTests.cs index 0aae3bfa4..2e2f4d75e 100644 --- a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/Deprecation/DeprecationHeaderExtensionsTests.cs +++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/Deprecation/DeprecationHeaderExtensionsTests.cs @@ -46,8 +46,8 @@ public sealed class DeprecationHeaderExtensionsTests { var context = CreateHttpContext(); var info = new DeprecationInfo( - DeprecatedAt: DateTimeOffset.UtcNow, - SunsetAt: DateTimeOffset.UtcNow.AddMonths(6), + DeprecatedAt: new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero), + SunsetAt: new DateTimeOffset(2025, 7, 1, 0, 0, 0, TimeSpan.Zero), SuccessorPath: "/v1/new", DocumentationUrl: "https://docs.example.com/migration"); @@ -76,8 +76,8 @@ public sealed class DeprecationHeaderExtensionsTests { var context = CreateHttpContext(); var info = new DeprecationInfo( - DeprecatedAt: DateTimeOffset.UtcNow, - SunsetAt: DateTimeOffset.UtcNow.AddMonths(6), + DeprecatedAt: new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero), + SunsetAt: new DateTimeOffset(2025, 7, 1, 0, 0, 0, TimeSpan.Zero), SuccessorPath: "/v1/new", Reason: "Custom deprecation reason"); @@ -123,8 +123,8 @@ public sealed class DeprecationHeaderExtensionsTests private static DeprecationInfo CreateSampleDeprecationInfo() { return new DeprecationInfo( - DeprecatedAt: DateTimeOffset.UtcNow, - SunsetAt: DateTimeOffset.UtcNow.AddMonths(6), + DeprecatedAt: new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero), + SunsetAt: new DateTimeOffset(2025, 7, 1, 0, 0, 0, TimeSpan.Zero), SuccessorPath: "/v1/new-endpoint"); } } diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/Deprecation/DeprecationInfoTests.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/Deprecation/DeprecationInfoTests.cs index db63c41e3..087c69ca4 100644 --- a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/Deprecation/DeprecationInfoTests.cs +++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/Deprecation/DeprecationInfoTests.cs @@ -8,46 +8,54 @@ public sealed class DeprecationInfoTests [Fact] public void IsPastSunset_WhenSunsetInFuture_ReturnsFalse() { + var now = new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero); + var timeProvider = new FixedTimeProvider(now); var info = new DeprecationInfo( - DeprecatedAt: DateTimeOffset.UtcNow.AddMonths(-1), - SunsetAt: DateTimeOffset.UtcNow.AddMonths(6), + DeprecatedAt: now.AddMonths(-1), + SunsetAt: now.AddMonths(6), SuccessorPath: "/v1/new"); - Assert.False(info.IsPastSunset); + Assert.False(info.IsPastSunsetAt(timeProvider)); } [Fact] public void IsPastSunset_WhenSunsetInPast_ReturnsTrue() { + var now = new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero); + var timeProvider = new FixedTimeProvider(now); var info = new DeprecationInfo( - DeprecatedAt: DateTimeOffset.UtcNow.AddMonths(-12), - SunsetAt: DateTimeOffset.UtcNow.AddMonths(-1), + DeprecatedAt: now.AddMonths(-12), + SunsetAt: now.AddMonths(-1), SuccessorPath: "/v1/new"); - Assert.True(info.IsPastSunset); + Assert.True(info.IsPastSunsetAt(timeProvider)); } [Fact] public void DaysUntilSunset_CalculatesCorrectly() { - var sunset = DateTimeOffset.UtcNow.AddDays(30); + var now = new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero); + var timeProvider = new FixedTimeProvider(now); + var sunset = now.AddDays(30); var info = new DeprecationInfo( - DeprecatedAt: DateTimeOffset.UtcNow, + DeprecatedAt: now, SunsetAt: sunset, SuccessorPath: "/v1/new"); - Assert.Equal(30, info.DaysUntilSunset); + Assert.Equal(30, info.DaysUntilSunsetAt(timeProvider)); } [Fact] public void DaysUntilSunset_WhenPastSunset_ReturnsZero() { + var now = new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero); + var timeProvider = new FixedTimeProvider(now); var info = new DeprecationInfo( - DeprecatedAt: DateTimeOffset.UtcNow.AddMonths(-12), - SunsetAt: DateTimeOffset.UtcNow.AddMonths(-1), + DeprecatedAt: now.AddMonths(-12), + SunsetAt: now.AddMonths(-1), SuccessorPath: "/v1/new"); - Assert.Equal(0, info.DaysUntilSunset); + Assert.Equal(0, info.DaysUntilSunsetAt(timeProvider)); } [Fact] @@ -69,4 +77,16 @@ public sealed class DeprecationInfoTests Assert.Equal("https://docs.example.com", info.DocumentationUrl); Assert.Equal("Replaced by new API", info.Reason); } + + private sealed class FixedTimeProvider : TimeProvider + { + private readonly DateTimeOffset _utcNow; + + public FixedTimeProvider(DateTimeOffset utcNow) + { + _utcNow = utcNow; + } + + public override DateTimeOffset GetUtcNow() => _utcNow; + } } diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/Distribution/ExportDistributionLifecycleTests.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/Distribution/ExportDistributionLifecycleTests.cs index 241a1b4d3..27d989e79 100644 --- a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/Distribution/ExportDistributionLifecycleTests.cs +++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/Distribution/ExportDistributionLifecycleTests.cs @@ -1,6 +1,7 @@ using Microsoft.Extensions.Logging.Abstractions; using StellaOps.ExportCenter.Core.Domain; using StellaOps.ExportCenter.WebService.Distribution; +using StellaOps.Determinism; namespace StellaOps.ExportCenter.Tests.Distribution; @@ -9,17 +10,20 @@ public sealed class ExportDistributionLifecycleTests private readonly InMemoryExportDistributionRepository _repository; private readonly ExportDistributionLifecycle _lifecycle; private readonly TestTimeProvider _timeProvider; + private readonly IGuidProvider _guidProvider; private readonly Guid _tenantId = Guid.NewGuid(); private readonly Guid _runId = Guid.NewGuid(); private readonly Guid _profileId = Guid.NewGuid(); public ExportDistributionLifecycleTests() { - _repository = new InMemoryExportDistributionRepository(); _timeProvider = new TestTimeProvider(new DateTimeOffset(2024, 6, 15, 12, 0, 0, TimeSpan.Zero)); + _guidProvider = new SequentialGuidProvider(); + _repository = new InMemoryExportDistributionRepository(_timeProvider); _lifecycle = new ExportDistributionLifecycle( _repository, NullLogger.Instance, + _guidProvider, _timeProvider); } diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/Distribution/InMemoryExportDistributionRepositoryTests.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/Distribution/InMemoryExportDistributionRepositoryTests.cs index 0871fe957..98467761c 100644 --- a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/Distribution/InMemoryExportDistributionRepositoryTests.cs +++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/Distribution/InMemoryExportDistributionRepositoryTests.cs @@ -1,20 +1,30 @@ +using Microsoft.Extensions.Options; using StellaOps.ExportCenter.Core.Domain; +using StellaOps.ExportCenter.Tests; using StellaOps.ExportCenter.WebService.Distribution; namespace StellaOps.ExportCenter.Tests.Distribution; public sealed class InMemoryExportDistributionRepositoryTests { - private readonly InMemoryExportDistributionRepository _repository = new(); + private readonly FakeTimeProvider _timeProvider; + private readonly InMemoryExportDistributionRepository _repository; private readonly Guid _tenantId = Guid.NewGuid(); private readonly Guid _runId = Guid.NewGuid(); + public InMemoryExportDistributionRepositoryTests() + { + _timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero)); + _repository = new InMemoryExportDistributionRepository(_timeProvider); + } + private ExportDistribution CreateDistribution( Guid? distributionId = null, Guid? tenantId = null, Guid? runId = null, string? idempotencyKey = null, - ExportDistributionStatus status = ExportDistributionStatus.Pending) + ExportDistributionStatus status = ExportDistributionStatus.Pending, + DateTimeOffset? createdAt = null) { return new ExportDistribution { @@ -28,7 +38,7 @@ public sealed class InMemoryExportDistributionRepositoryTests ArtifactHash = "sha256:abc123", SizeBytes = 1024, IdempotencyKey = idempotencyKey, - CreatedAt = DateTimeOffset.UtcNow + CreatedAt = createdAt ?? _timeProvider.GetUtcNow() }; } @@ -138,7 +148,7 @@ public sealed class InMemoryExportDistributionRepositoryTests [Fact] public async Task ListExpiredAsync_ReturnsOnlyExpired() { - var now = DateTimeOffset.UtcNow; + var now = _timeProvider.GetUtcNow(); var expired = new ExportDistribution { @@ -151,7 +161,7 @@ public sealed class InMemoryExportDistributionRepositoryTests ArtifactPath = "/test", RetentionExpiresAt = now.AddDays(-1), MarkedForDeletion = false, - CreatedAt = now.AddDays(-30) + CreatedAt = now.AddHours(-1) }; var notExpired = new ExportDistribution @@ -165,7 +175,7 @@ public sealed class InMemoryExportDistributionRepositoryTests ArtifactPath = "/test", RetentionExpiresAt = now.AddDays(30), MarkedForDeletion = false, - CreatedAt = now.AddDays(-30) + CreatedAt = now.AddHours(-1) }; await _repository.CreateAsync(expired); @@ -273,6 +283,7 @@ public sealed class InMemoryExportDistributionRepositoryTests var updated = await _repository.GetByIdAsync(_tenantId, distribution.DistributionId); Assert.True(updated?.MarkedForDeletion); Assert.NotNull(updated?.DeletedAt); + Assert.Equal(_timeProvider.GetUtcNow(), updated?.DeletedAt); } [Fact] @@ -339,4 +350,50 @@ public sealed class InMemoryExportDistributionRepositoryTests var result = _repository.ListByRunAsync(_tenantId, _runId).GetAwaiter().GetResult(); Assert.Empty(result); } + + [Fact] + public async Task PruneStale_RemovesEntriesBeyondRetention() + { + var options = Options.Create(new InMemoryExportDistributionOptions + { + RetentionPeriod = TimeSpan.FromHours(1), + MaxEntries = 0 + }); + var timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 1, 1, 12, 0, 0, TimeSpan.Zero)); + var repository = new InMemoryExportDistributionRepository(timeProvider, options); + + var stale = CreateDistribution(createdAt: timeProvider.GetUtcNow().AddHours(-2)); + var fresh = CreateDistribution(createdAt: timeProvider.GetUtcNow().AddMinutes(-30)); + + await repository.CreateAsync(stale); + await repository.CreateAsync(fresh); + + var result = await repository.ListByRunAsync(_tenantId, _runId); + + Assert.Single(result); + Assert.Equal(fresh.DistributionId, result[0].DistributionId); + } + + [Fact] + public async Task PruneStale_RespectsMaxEntries() + { + var options = Options.Create(new InMemoryExportDistributionOptions + { + RetentionPeriod = TimeSpan.Zero, + MaxEntries = 1 + }); + var timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 1, 1, 12, 0, 0, TimeSpan.Zero)); + var repository = new InMemoryExportDistributionRepository(timeProvider, options); + + var older = CreateDistribution(createdAt: timeProvider.GetUtcNow().AddMinutes(-10)); + var newer = CreateDistribution(createdAt: timeProvider.GetUtcNow()); + + await repository.CreateAsync(older); + await repository.CreateAsync(newer); + + var result = await repository.ListByRunAsync(_tenantId, _runId); + + Assert.Single(result); + Assert.Equal(newer.DistributionId, result[0].DistributionId); + } } diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/Distribution/Oci/OciDistributionServiceExtensionsTests.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/Distribution/Oci/OciDistributionServiceExtensionsTests.cs new file mode 100644 index 000000000..9a1b53182 --- /dev/null +++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/Distribution/Oci/OciDistributionServiceExtensionsTests.cs @@ -0,0 +1,51 @@ +using System.Net.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Http; +using StellaOps.ExportCenter.WebService.Distribution.Oci; +using Xunit; + +namespace StellaOps.ExportCenter.Tests.Distribution.Oci; + +public sealed class OciDistributionServiceExtensionsTests +{ + [Fact] + public void AddOciDistribution_AllowInsecureTls_UsesValidationCallback() + { + var services = new ServiceCollection(); + services.AddOciDistribution(options => options.AllowInsecureTls = true); + + using var provider = services.BuildServiceProvider(); + var factory = provider.GetRequiredService(); + + var handler = factory.CreateHandler(OciDistributionOptions.HttpClientName); + var primary = GetPrimaryHandler(handler); + + Assert.NotNull(primary.ServerCertificateCustomValidationCallback); + } + + [Fact] + public void AddOciDistribution_DisallowInsecureTls_DoesNotSetValidationCallback() + { + var services = new ServiceCollection(); + services.AddOciDistribution(options => options.AllowInsecureTls = false); + + using var provider = services.BuildServiceProvider(); + var factory = provider.GetRequiredService(); + + var handler = factory.CreateHandler(OciDistributionOptions.HttpClientName); + var primary = GetPrimaryHandler(handler); + + Assert.Null(primary.ServerCertificateCustomValidationCallback); + } + + private static HttpClientHandler GetPrimaryHandler(HttpMessageHandler handler) + { + var current = handler; + while (current is DelegatingHandler delegating) + { + current = delegating.InnerHandler ?? throw new InvalidOperationException("Missing inner handler."); + } + + return Assert.IsType(current); + } +} diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/Distribution/Oci/OciHttpClientFactoryTests.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/Distribution/Oci/OciHttpClientFactoryTests.cs new file mode 100644 index 000000000..c0d06234c --- /dev/null +++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/Distribution/Oci/OciHttpClientFactoryTests.cs @@ -0,0 +1,52 @@ +using System.Net.Http; +using StellaOps.ExportCenter.WebService.Distribution.Oci; +using Xunit; + +namespace StellaOps.ExportCenter.Tests.Distribution.Oci; + +public sealed class OciHttpClientFactoryTests +{ + [Fact] + public void CreateClient_ConfiguresBaseAddressAndTimeout() + { + using var httpClient = new HttpClient(); + var config = new OciRegistryConfig + { + Global = new RegistryGlobalSettings + { + Timeout = TimeSpan.FromSeconds(12), + UserAgent = "StellaOps-Test" + }, + Registries = + { + ["registry.example.com"] = new RegistryEndpointConfig + { + Host = "registry.example.com", + Port = 5000, + Insecure = true + } + } + }; + + var factory = new OciHttpClientFactory(config, new FakeHttpClientFactory(httpClient)); + + var client = factory.CreateClient("registry.example.com"); + + Assert.Same(httpClient, client); + Assert.Equal(new Uri("http://registry.example.com:5000"), client.BaseAddress); + Assert.Equal(TimeSpan.FromSeconds(12), client.Timeout); + Assert.Contains("StellaOps-Test", client.DefaultRequestHeaders.UserAgent.ToString()); + } + + private sealed class FakeHttpClientFactory : IHttpClientFactory + { + private readonly HttpClient _client; + + public FakeHttpClientFactory(HttpClient client) + { + _client = client; + } + + public HttpClient CreateClient(string name) => _client; + } +} diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/EvidenceLocker/EvidenceLockerServiceCollectionExtensionsTests.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/EvidenceLocker/EvidenceLockerServiceCollectionExtensionsTests.cs new file mode 100644 index 000000000..16966bb27 --- /dev/null +++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/EvidenceLocker/EvidenceLockerServiceCollectionExtensionsTests.cs @@ -0,0 +1,20 @@ +using Microsoft.Extensions.DependencyInjection; +using StellaOps.ExportCenter.WebService.EvidenceLocker; +using Xunit; + +namespace StellaOps.ExportCenter.Tests.EvidenceLocker; + +public sealed class EvidenceLockerServiceCollectionExtensionsTests +{ + [Fact] + public void AddExportEvidenceLocker_InvalidBaseUrl_Throws() + { + var services = new ServiceCollection(); + services.AddExportEvidenceLocker(options => options.BaseUrl = "not-a-url"); + + using var provider = services.BuildServiceProvider(); + + Assert.Throws(() => + provider.GetRequiredService()); + } +} diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/EvidenceLocker/InMemoryExportEvidenceLockerClientTests.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/EvidenceLocker/InMemoryExportEvidenceLockerClientTests.cs new file mode 100644 index 000000000..4e593ff20 --- /dev/null +++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/EvidenceLocker/InMemoryExportEvidenceLockerClientTests.cs @@ -0,0 +1,60 @@ +using StellaOps.Determinism; +using StellaOps.ExportCenter.Tests; +using StellaOps.ExportCenter.WebService.EvidenceLocker; +using Xunit; + +namespace StellaOps.ExportCenter.Tests.EvidenceLocker; + +public sealed class InMemoryExportEvidenceLockerClientTests +{ + [Fact] + public async Task PushSnapshotAsync_SortsEntriesAndUsesDeterministicIds() + { + var timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 1, 2, 8, 0, 0, TimeSpan.Zero)); + var guidProvider = new SequentialGuidProvider(); + var calculator = new ExportMerkleTreeCalculator(); + var client = new InMemoryExportEvidenceLockerClient(calculator, timeProvider, guidProvider); + + var request = new ExportEvidenceSnapshotRequest + { + TenantId = "tenant-1", + ExportRunId = "run-1", + ProfileId = "profile-1", + Kind = ExportBundleKind.Evidence, + Materials = new[] + { + new ExportMaterialInput + { + Section = "reports", + Path = "z.json", + Sha256 = "ABCDEF", + SizeBytes = 10, + MediaType = "application/json" + }, + new ExportMaterialInput + { + Section = "reports", + Path = "a.json", + Sha256 = "123456", + SizeBytes = 20, + MediaType = "application/json" + } + } + }; + + var result = await client.PushSnapshotAsync(request); + + Assert.True(result.Success); + Assert.Equal("00000000000000000000000000000001", result.BundleId); + + var manifest = await client.GetBundleAsync(result.BundleId!, request.TenantId); + Assert.NotNull(manifest); + Assert.Equal(timeProvider.GetUtcNow(), manifest!.CreatedAt); + + var paths = manifest.Entries.Select(e => e.CanonicalPath).ToList(); + var sorted = paths.OrderBy(p => p, StringComparer.Ordinal).ToList(); + Assert.Equal(sorted, paths); + + Assert.All(manifest.Entries, entry => Assert.Equal(entry.Sha256, entry.Sha256.ToLowerInvariant())); + } +} diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/ExceptionReport/ExceptionReportGeneratorTests.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/ExceptionReport/ExceptionReportGeneratorTests.cs new file mode 100644 index 000000000..dd31f5b51 --- /dev/null +++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/ExceptionReport/ExceptionReportGeneratorTests.cs @@ -0,0 +1,139 @@ +using System.Collections.Immutable; +using System.Diagnostics; +using System.Text.Json; +using Microsoft.Extensions.Logging.Abstractions; +using Moq; +using StellaOps.Determinism; +using StellaOps.ExportCenter.Tests; +using StellaOps.ExportCenter.WebService.ExceptionReport; +using StellaOps.Policy.Exceptions.Models; +using StellaOps.Policy.Exceptions.Repositories; +using Xunit; + +namespace StellaOps.ExportCenter.Tests.ExceptionReport; + +public sealed class ExceptionReportGeneratorTests +{ + [Fact] + public async Task CreateReportAsync_UsesGuidProvider() + { + var tenantId = Guid.NewGuid(); + var timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 1, 2, 11, 0, 0, TimeSpan.Zero)); + var guidProvider = new SequentialGuidProvider(); + var exceptionRepo = new Mock(); + var applicationRepo = new Mock(); + + exceptionRepo + .Setup(repo => repo.GetByFilterAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(Array.Empty()); + + var generator = new ExceptionReportGenerator( + exceptionRepo.Object, + applicationRepo.Object, + NullLogger.Instance, + guidProvider, + timeProvider); + + var response = await generator.CreateReportAsync(new ExceptionReportRequest + { + TenantId = tenantId, + RequesterId = "user-1", + Format = "json" + }); + + Assert.StartsWith("exc-rpt-", response.JobId); + Assert.EndsWith("00000000000000000000000000000001", response.JobId); + } + + [Fact] + public async Task CreateReportAsync_SummaryKeysAreOrdered() + { + var tenantId = Guid.NewGuid(); + var timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 1, 2, 11, 0, 0, TimeSpan.Zero)); + var guidProvider = new SequentialGuidProvider(); + var exceptionRepo = new Mock(); + var applicationRepo = new Mock(); + + var exceptions = new[] + { + CreateException("exc-2", ExceptionStatus.Revoked, tenantId, timeProvider.GetUtcNow()), + CreateException("exc-1", ExceptionStatus.Active, tenantId, timeProvider.GetUtcNow()) + }; + + exceptionRepo + .Setup(repo => repo.GetByFilterAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(exceptions); + + var generator = new ExceptionReportGenerator( + exceptionRepo.Object, + applicationRepo.Object, + NullLogger.Instance, + guidProvider, + timeProvider); + + var response = await generator.CreateReportAsync(new ExceptionReportRequest + { + TenantId = tenantId, + RequesterId = "user-1", + Format = "json" + }); + + var content = await WaitForContentAsync(generator, response.JobId); + using var document = JsonDocument.Parse(content.Content); + var byStatus = document.RootElement.GetProperty("summary").GetProperty("byStatus"); + var keys = byStatus.EnumerateObject().Select(p => p.Name).ToList(); + + Assert.Equal(new[] { "Active", "Revoked" }, keys); + } + + private static ExceptionObject CreateException( + string exceptionId, + ExceptionStatus status, + Guid tenantId, + DateTimeOffset now) + { + return new ExceptionObject + { + ExceptionId = exceptionId, + Version = 1, + Status = status, + Type = ExceptionType.Vulnerability, + Scope = new ExceptionScope + { + TenantId = tenantId, + VulnerabilityId = "CVE-2024-0001" + }, + OwnerId = "owner-1", + RequesterId = "requester-1", + CreatedAt = now, + UpdatedAt = now, + ExpiresAt = now.AddDays(30), + ReasonCode = ExceptionReason.AcceptedRisk, + Rationale = new string('a', 60), + EvidenceRefs = ImmutableArray.Empty, + CompensatingControls = ImmutableArray.Empty, + Metadata = ImmutableDictionary.Empty + }; + } + + private static async Task WaitForContentAsync( + IExceptionReportGenerator generator, + string jobId) + { + var timeout = TimeSpan.FromSeconds(2); + var stopwatch = Stopwatch.StartNew(); + + while (stopwatch.Elapsed < timeout) + { + var content = await generator.GetReportContentAsync(jobId); + if (content is not null) + { + return content; + } + + await Task.Delay(TimeSpan.FromMilliseconds(10)); + } + + throw new TimeoutException("Report content not available."); + } +} diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/Incident/ExportIncidentManagerTests.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/Incident/ExportIncidentManagerTests.cs new file mode 100644 index 000000000..af400ba47 --- /dev/null +++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/Incident/ExportIncidentManagerTests.cs @@ -0,0 +1,127 @@ +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using StellaOps.Determinism; +using StellaOps.ExportCenter.Tests; +using StellaOps.ExportCenter.WebService.Incident; +using StellaOps.ExportCenter.WebService.Timeline; +using Xunit; + +namespace StellaOps.ExportCenter.Tests.Incident; + +public sealed class ExportIncidentManagerTests +{ + [Fact] + public async Task ActivateIncidentAsync_UsesGuidProvider() + { + var timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 1, 2, 12, 0, 0, TimeSpan.Zero)); + var guidProvider = new SequentialGuidProvider(); + var manager = new ExportIncidentManager( + NullLogger.Instance, + new FakeTimelinePublisher(), + new FakeNotificationEmitter(), + guidProvider, + timeProvider); + + var request = new ExportIncidentActivationRequest + { + Type = ExportIncidentType.SecurityIncident, + Severity = ExportIncidentSeverity.Critical, + Summary = "Test incident", + Description = "Test description", + ActivatedBy = "tester" + }; + + var result = await manager.ActivateIncidentAsync(request); + + Assert.True(result.Success); + Assert.NotNull(result.Incident); + + var expectedGuid = new Guid("00000000-0000-0000-0000-000000000001"); + var expectedId = $"inc-{expectedGuid:N}"[..20]; + Assert.Equal(expectedId, result.Incident!.IncidentId); + } + + [Fact] + public async Task GetRecentIncidentsAsync_PrunesResolvedByRetention() + { + var timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 1, 2, 12, 0, 0, TimeSpan.Zero)); + var guidProvider = new SequentialGuidProvider(); + var options = Options.Create(new ExportIncidentManagerOptions + { + RetentionPeriod = TimeSpan.FromMinutes(30), + MaxIncidentCount = 0 + }); + + var manager = new ExportIncidentManager( + NullLogger.Instance, + new FakeTimelinePublisher(), + new FakeNotificationEmitter(), + guidProvider, + timeProvider, + options); + + var activation = await manager.ActivateIncidentAsync(new ExportIncidentActivationRequest + { + Type = ExportIncidentType.SecurityIncident, + Severity = ExportIncidentSeverity.Critical, + Summary = "Retention test", + ActivatedBy = "tester" + }); + + var incidentId = activation.Incident!.IncidentId; + + await manager.ResolveIncidentAsync(incidentId, new ExportIncidentResolutionRequest + { + ResolutionMessage = "Resolved", + ResolvedBy = "tester", + IsFalsePositive = false + }); + + timeProvider.Advance(TimeSpan.FromHours(1)); + + var recent = await manager.GetRecentIncidentsAsync(limit: 10, includeResolved: true); + + Assert.Empty(recent); + } + + private sealed class FakeTimelinePublisher : IExportTimelinePublisher + { + public Task PublishStartedAsync(ExportStartedEvent @event, CancellationToken cancellationToken = default) + => Task.FromResult(TimelinePublishResult.Succeeded("started")); + + public Task PublishCompletedAsync(ExportCompletedEvent @event, CancellationToken cancellationToken = default) + => Task.FromResult(TimelinePublishResult.Succeeded("completed")); + + public Task PublishFailedAsync(ExportFailedEvent @event, CancellationToken cancellationToken = default) + => Task.FromResult(TimelinePublishResult.Succeeded("failed")); + + public Task PublishCancelledAsync(ExportCancelledEvent @event, CancellationToken cancellationToken = default) + => Task.FromResult(TimelinePublishResult.Succeeded("cancelled")); + + public Task PublishArtifactCreatedAsync(ExportArtifactCreatedEvent @event, CancellationToken cancellationToken = default) + => Task.FromResult(TimelinePublishResult.Succeeded("artifact")); + + public Task PublishEventAsync(ExportTimelineEventBase @event, CancellationToken cancellationToken = default) + => Task.FromResult(TimelinePublishResult.Succeeded("event")); + + public Task PublishIncidentEventAsync( + string eventType, + string incidentId, + string eventJson, + string? correlationId, + CancellationToken cancellationToken = default) + => Task.FromResult(TimelinePublishResult.Succeeded("incident")); + } + + private sealed class FakeNotificationEmitter : IExportNotificationEmitter + { + public Task EmitIncidentActivatedAsync(ExportIncident incident, CancellationToken cancellationToken = default) + => Task.CompletedTask; + + public Task EmitIncidentUpdatedAsync(ExportIncident incident, string updateMessage, CancellationToken cancellationToken = default) + => Task.CompletedTask; + + public Task EmitIncidentResolvedAsync(ExportIncident incident, string resolutionMessage, bool isFalsePositive, CancellationToken cancellationToken = default) + => Task.CompletedTask; + } +} diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/RiskBundle/RiskBundleJobHandlerTests.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/RiskBundle/RiskBundleJobHandlerTests.cs new file mode 100644 index 000000000..0b228c7ac --- /dev/null +++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/RiskBundle/RiskBundleJobHandlerTests.cs @@ -0,0 +1,95 @@ +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using StellaOps.Determinism; +using StellaOps.ExportCenter.Tests; +using StellaOps.ExportCenter.WebService.RiskBundle; +using StellaOps.ExportCenter.WebService.Timeline; +using Xunit; + +namespace StellaOps.ExportCenter.Tests.RiskBundle; + +public sealed class RiskBundleJobHandlerTests +{ + [Fact] + public async Task SubmitJobAsync_UsesGuidProvider() + { + var timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 1, 2, 9, 0, 0, TimeSpan.Zero)); + var guidProvider = new SequentialGuidProvider(); + var handler = new RiskBundleJobHandler( + timeProvider, + guidProvider, + NullLogger.Instance, + new FakeTimelinePublisher()); + + var request = new RiskBundleJobSubmitRequest + { + TenantId = "tenant-1" + }; + + var result = await handler.SubmitJobAsync(request, "actor"); + + Assert.True(result.Success); + Assert.Equal("00000000000000000000000000000001", result.JobId); + } + + [Fact] + public async Task SubmitJobAsync_RespectsMaxConcurrentJobs() + { + var timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 1, 2, 9, 0, 0, TimeSpan.Zero)); + var guidProvider = new SequentialGuidProvider(); + var options = Options.Create(new RiskBundleJobHandlerOptions + { + MaxConcurrentJobs = 1, + JobTimeout = TimeSpan.FromMinutes(5), + JobRetentionPeriod = TimeSpan.FromHours(1) + }); + + var handler = new RiskBundleJobHandler( + timeProvider, + guidProvider, + NullLogger.Instance, + new FakeTimelinePublisher(), + options); + + var request = new RiskBundleJobSubmitRequest + { + TenantId = "tenant-1" + }; + + var first = await handler.SubmitJobAsync(request, "actor"); + var second = await handler.SubmitJobAsync(request, "actor"); + + Assert.True(first.Success); + Assert.False(second.Success); + Assert.Equal("Maximum concurrent jobs reached", second.ErrorMessage); + } + + private sealed class FakeTimelinePublisher : IExportTimelinePublisher + { + public Task PublishStartedAsync(ExportStartedEvent @event, CancellationToken cancellationToken = default) + => Task.FromResult(TimelinePublishResult.Succeeded("started")); + + public Task PublishCompletedAsync(ExportCompletedEvent @event, CancellationToken cancellationToken = default) + => Task.FromResult(TimelinePublishResult.Succeeded("completed")); + + public Task PublishFailedAsync(ExportFailedEvent @event, CancellationToken cancellationToken = default) + => Task.FromResult(TimelinePublishResult.Succeeded("failed")); + + public Task PublishCancelledAsync(ExportCancelledEvent @event, CancellationToken cancellationToken = default) + => Task.FromResult(TimelinePublishResult.Succeeded("cancelled")); + + public Task PublishArtifactCreatedAsync(ExportArtifactCreatedEvent @event, CancellationToken cancellationToken = default) + => Task.FromResult(TimelinePublishResult.Succeeded("artifact")); + + public Task PublishEventAsync(ExportTimelineEventBase @event, CancellationToken cancellationToken = default) + => Task.FromResult(TimelinePublishResult.Succeeded("event")); + + public Task PublishIncidentEventAsync( + string eventType, + string incidentId, + string eventJson, + string? correlationId, + CancellationToken cancellationToken = default) + => Task.FromResult(TimelinePublishResult.Succeeded("incident")); + } +} diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/SimulationExport/SimulationReportExporterTests.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/SimulationExport/SimulationReportExporterTests.cs new file mode 100644 index 000000000..aacbfeaa6 --- /dev/null +++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/SimulationExport/SimulationReportExporterTests.cs @@ -0,0 +1,76 @@ +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using StellaOps.Determinism; +using StellaOps.ExportCenter.Tests; +using StellaOps.ExportCenter.WebService.SimulationExport; +using Xunit; + +namespace StellaOps.ExportCenter.Tests.SimulationExport; + +public sealed class SimulationReportExporterTests +{ + [Fact] + public async Task ExportAsync_UsesGuidProvider() + { + var timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 1, 2, 13, 0, 0, TimeSpan.Zero)); + var guidProvider = new SequentialGuidProvider(); + var exporter = new SimulationReportExporter( + timeProvider, + guidProvider, + NullLogger.Instance); + + var simulationId = (await exporter.GetAvailableSimulationsAsync(null)).Simulations.First().SimulationId; + var result = await exporter.ExportAsync(new SimulationExportRequest + { + SimulationId = simulationId, + Format = SimulationExportFormat.Json + }); + + Assert.True(result.Success); + Assert.Equal("exp-00000000000000000000000000000001", result.ExportId); + Assert.Equal(timeProvider.GetUtcNow(), result.CreatedAt); + } + + [Fact] + public async Task ExportAsync_PrunesMaxExports() + { + var timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 1, 2, 13, 0, 0, TimeSpan.Zero)); + var guidProvider = new SequentialGuidProvider(); + var options = Options.Create(new SimulationReportExporterOptions + { + MaxExports = 1, + MaxSimulations = 0, + RetentionPeriod = TimeSpan.Zero + }); + + var exporter = new SimulationReportExporter( + timeProvider, + guidProvider, + NullLogger.Instance, + options); + + var simulationId = (await exporter.GetAvailableSimulationsAsync(null)).Simulations.First().SimulationId; + + var first = await exporter.ExportAsync(new SimulationExportRequest + { + SimulationId = simulationId, + Format = SimulationExportFormat.Json + }); + + timeProvider.Advance(TimeSpan.FromMinutes(5)); + + var second = await exporter.ExportAsync(new SimulationExportRequest + { + SimulationId = simulationId, + Format = SimulationExportFormat.Json + }); + + await exporter.GetAvailableSimulationsAsync(null); + + var removed = await exporter.GetExportDocumentAsync(first.ExportId); + var retained = await exporter.GetExportDocumentAsync(second.ExportId); + + Assert.Null(removed); + Assert.NotNull(retained); + } +} diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Api/ExportApiEndpoints.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Api/ExportApiEndpoints.cs index 55146a8ed..30191c1e8 100644 --- a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Api/ExportApiEndpoints.cs +++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Api/ExportApiEndpoints.cs @@ -781,15 +781,9 @@ public static class ExportApiEndpoints Description = profile.Description, Kind = profile.Kind, Status = profile.Status, - Scope = profile.ScopeJson is not null - ? JsonSerializer.Deserialize(profile.ScopeJson) - : null, - Format = profile.FormatJson is not null - ? JsonSerializer.Deserialize(profile.FormatJson) - : null, - Signing = profile.SigningJson is not null - ? JsonSerializer.Deserialize(profile.SigningJson) - : null, + Scope = TryDeserialize(profile.ScopeJson), + Format = TryDeserialize(profile.FormatJson), + Signing = TryDeserialize(profile.SigningJson), Schedule = profile.Schedule, CreatedAt = profile.CreatedAt, UpdatedAt = profile.UpdatedAt, @@ -866,6 +860,23 @@ public static class ExportApiEndpoints }; } + private static T? TryDeserialize(string? json) + { + if (string.IsNullOrWhiteSpace(json)) + { + return default; + } + + try + { + return JsonSerializer.Deserialize(json); + } + catch + { + return default; + } + } + // ======================================================================== // Verification endpoint registration // ======================================================================== diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Api/ExportApiServiceCollectionExtensions.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Api/ExportApiServiceCollectionExtensions.cs index e0d18ac7a..de8184a9b 100644 --- a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Api/ExportApiServiceCollectionExtensions.cs +++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Api/ExportApiServiceCollectionExtensions.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; +using StellaOps.Determinism; namespace StellaOps.ExportCenter.WebService.Api; @@ -10,11 +11,10 @@ public static class ExportApiServiceCollectionExtensions { /// /// Adds export API services to the service collection. - /// Uses in-memory repositories by default. /// public static IServiceCollection AddExportApiServices(this IServiceCollection services) { - return services.AddExportApiServices(_ => { }); + return services.AddExportApiServices(_ => { }, allowInMemoryRepositories: false); } /// @@ -22,7 +22,8 @@ public static class ExportApiServiceCollectionExtensions /// public static IServiceCollection AddExportApiServices( this IServiceCollection services, - Action configureConcurrency) + Action configureConcurrency, + bool allowInMemoryRepositories) { ArgumentNullException.ThrowIfNull(services); ArgumentNullException.ThrowIfNull(configureConcurrency); @@ -32,6 +33,13 @@ public static class ExportApiServiceCollectionExtensions // Register TimeProvider if not already registered services.TryAddSingleton(TimeProvider.System); + services.TryAddSingleton(); + + if (!allowInMemoryRepositories) + { + throw new InvalidOperationException( + "In-memory export repositories are disabled. Register persistent repositories or set Export:AllowInMemoryRepositories to true."); + } // Register repositories (in-memory by default) services.TryAddSingleton(); @@ -64,6 +72,7 @@ public static class ExportApiServiceCollectionExtensions // Register TimeProvider if not already registered services.TryAddSingleton(TimeProvider.System); + services.TryAddSingleton(); // Register custom repositories services.TryAddSingleton(); diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Api/ExportAuditService.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Api/ExportAuditService.cs index bbb72a739..cd502ea04 100644 --- a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Api/ExportAuditService.cs +++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Api/ExportAuditService.cs @@ -1,6 +1,7 @@ using System.Diagnostics; using System.Text.Json; using Microsoft.Extensions.Logging; +using StellaOps.Determinism; using StellaOps.ExportCenter.Core.Domain; using StellaOps.ExportCenter.WebService.Telemetry; @@ -104,13 +105,16 @@ public sealed class ExportAuditService : IExportAuditService { private readonly ILogger _logger; private readonly TimeProvider _timeProvider; + private readonly IGuidProvider _guidProvider; public ExportAuditService( ILogger logger, + IGuidProvider guidProvider, TimeProvider timeProvider) { _logger = logger; _timeProvider = timeProvider; + _guidProvider = guidProvider ?? throw new ArgumentNullException(nameof(guidProvider)); } public Task LogProfileOperationAsync( @@ -225,7 +229,7 @@ public sealed class ExportAuditService : IExportAuditService return new ExportAuditEntry { - AuditId = Guid.NewGuid(), + AuditId = _guidProvider.NewGuid(), Operation = operation, TenantId = tenantId, UserId = userId, diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Api/InMemoryExportRepositories.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Api/InMemoryExportRepositories.cs index 2928a3bc3..0aa279e98 100644 --- a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Api/InMemoryExportRepositories.cs +++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Api/InMemoryExportRepositories.cs @@ -11,10 +11,14 @@ public sealed class InMemoryExportProfileRepository : IExportProfileRepository { private readonly ConcurrentDictionary<(Guid TenantId, Guid ProfileId), ExportProfile> _profiles = new(); private readonly ILogger _logger; + private readonly TimeProvider _timeProvider; - public InMemoryExportProfileRepository(ILogger logger) + public InMemoryExportProfileRepository( + ILogger logger, + TimeProvider? timeProvider = null) { _logger = logger; + _timeProvider = timeProvider ?? TimeProvider.System; } public Task GetByIdAsync( @@ -112,8 +116,8 @@ public sealed class InMemoryExportProfileRepository : IExportProfileRepository var archived = existing with { Status = ExportProfileStatus.Archived, - ArchivedAt = DateTimeOffset.UtcNow, - UpdatedAt = DateTimeOffset.UtcNow + ArchivedAt = _timeProvider.GetUtcNow(), + UpdatedAt = _timeProvider.GetUtcNow() }; if (!_profiles.TryUpdate(key, archived, existing)) @@ -165,10 +169,14 @@ public sealed class InMemoryExportRunRepository : IExportRunRepository { private readonly ConcurrentDictionary<(Guid TenantId, Guid RunId), ExportRun> _runs = new(); private readonly ILogger _logger; + private readonly TimeProvider _timeProvider; - public InMemoryExportRunRepository(ILogger logger) + public InMemoryExportRunRepository( + ILogger logger, + TimeProvider? timeProvider = null) { _logger = logger; + _timeProvider = timeProvider ?? TimeProvider.System; } public Task GetByIdAsync( @@ -277,24 +285,11 @@ public sealed class InMemoryExportRunRepository : IExportRunRepository if (existing.Status != ExportRunStatus.Queued && existing.Status != ExportRunStatus.Running) return Task.FromResult(false); - var cancelled = new ExportRun + var cancelled = existing with { - RunId = existing.RunId, - ProfileId = existing.ProfileId, - TenantId = existing.TenantId, Status = ExportRunStatus.Cancelled, - Trigger = existing.Trigger, - CorrelationId = existing.CorrelationId, - InitiatedBy = existing.InitiatedBy, - TotalItems = existing.TotalItems, - ProcessedItems = existing.ProcessedItems, - FailedItems = existing.FailedItems, - TotalSizeBytes = existing.TotalSizeBytes, ErrorJson = null, - CreatedAt = existing.CreatedAt, - StartedAt = existing.StartedAt, - CompletedAt = DateTimeOffset.UtcNow, - ExpiresAt = existing.ExpiresAt + CompletedAt = _timeProvider.GetUtcNow() }; _runs[key] = cancelled; @@ -339,12 +334,27 @@ public sealed class InMemoryExportRunRepository : IExportRunRepository { cancellationToken.ThrowIfCancellationRequested(); - var nextRun = _runs.Values + var candidates = _runs.Values .Where(r => r.TenantId == tenantId && r.Status == ExportRunStatus.Queued) .OrderBy(r => r.CreatedAt) - .FirstOrDefault(); + .ToList(); - return Task.FromResult(nextRun); + foreach (var candidate in candidates) + { + var key = (candidate.TenantId, candidate.RunId); + var updated = candidate with + { + Status = ExportRunStatus.Running, + StartedAt = candidate.StartedAt ?? _timeProvider.GetUtcNow() + }; + + if (_runs.TryUpdate(key, updated, candidate)) + { + return Task.FromResult(updated); + } + } + + return Task.FromResult(null); } } diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/AuditBundle/AuditBundleJobHandler.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/AuditBundle/AuditBundleJobHandler.cs index 4f021af47..83c8032c9 100644 --- a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/AuditBundle/AuditBundleJobHandler.cs +++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/AuditBundle/AuditBundleJobHandler.cs @@ -5,6 +5,7 @@ using System.Security.Cryptography; using System.Text; using System.Text.Json; using StellaOps.ExportCenter.Client.Models; +using StellaOps.Determinism; namespace StellaOps.ExportCenter.WebService.AuditBundle; @@ -17,16 +18,21 @@ public sealed class AuditBundleJobHandler : IAuditBundleJobHandler private readonly ConcurrentDictionary _jobs = new(); private readonly ILogger _logger; private readonly TimeProvider _timeProvider; + private readonly IGuidProvider _guidProvider; private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; - public AuditBundleJobHandler(ILogger logger, TimeProvider? timeProvider = null) + public AuditBundleJobHandler( + ILogger logger, + IGuidProvider guidProvider, + TimeProvider? timeProvider = null) { _logger = logger; _timeProvider = timeProvider ?? TimeProvider.System; + _guidProvider = guidProvider ?? throw new ArgumentNullException(nameof(guidProvider)); } public Task CreateBundleAsync( @@ -50,7 +56,7 @@ public sealed class AuditBundleJobHandler : IAuditBundleJobHandler new ErrorDetail("INVALID_REQUEST", "Subject name is required"))); } - var bundleId = $"bndl-{Guid.NewGuid():N}"; + var bundleId = $"bndl-{_guidProvider.NewGuid():N}"; var now = _timeProvider.GetUtcNow(); var job = new AuditBundleJob @@ -73,7 +79,7 @@ public sealed class AuditBundleJobHandler : IAuditBundleJobHandler // In a real implementation, this would enqueue a background job // For now, we'll process it synchronously in-memory - _ = Task.Run(async () => await ProcessBundleAsync(bundleId, cancellationToken), cancellationToken); + _ = ProcessBundleAsync(bundleId, cancellationToken); var response = new CreateAuditBundleResponse( bundleId, @@ -280,7 +286,14 @@ public sealed class AuditBundleJobHandler : IAuditBundleJobHandler "Completed audit bundle {BundleId} with hash {BundleHash}", bundleId, job.BundleHash); } - catch (Exception ex) when (ex is not OperationCanceledException) + catch (OperationCanceledException) + { + job.Status = "Cancelled"; + job.ErrorCode = "CANCELLED"; + job.ErrorMessage = "Bundle generation cancelled."; + job.CompletedAt = _timeProvider.GetUtcNow(); + } + catch (Exception ex) { _logger.LogError(ex, "Failed to process audit bundle {BundleId}", bundleId); job.Status = "Failed"; diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/AuditBundle/AuditBundleServiceCollectionExtensions.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/AuditBundle/AuditBundleServiceCollectionExtensions.cs index fbde7efc0..77659b743 100644 --- a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/AuditBundle/AuditBundleServiceCollectionExtensions.cs +++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/AuditBundle/AuditBundleServiceCollectionExtensions.cs @@ -1,3 +1,7 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using StellaOps.Determinism; + namespace StellaOps.ExportCenter.WebService.AuditBundle; /// @@ -10,6 +14,8 @@ public static class AuditBundleServiceCollectionExtensions /// public static IServiceCollection AddAuditBundleJobHandler(this IServiceCollection services) { + services.TryAddSingleton(TimeProvider.System); + services.TryAddSingleton(); services.AddSingleton(); return services; } diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Deprecation/DeprecationHeaderExtensions.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Deprecation/DeprecationHeaderExtensions.cs index 2e923835b..46f9f69dd 100644 --- a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Deprecation/DeprecationHeaderExtensions.cs +++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Deprecation/DeprecationHeaderExtensions.cs @@ -85,6 +85,7 @@ public static class DeprecationHeaderExtensions return async (context, next) => { var httpContext = context.HttpContext; + var timeProvider = httpContext.RequestServices.GetService() ?? TimeProvider.System; // Add deprecation headers httpContext.AddDeprecationHeaders(info); @@ -99,7 +100,7 @@ public static class DeprecationHeaderExtensions httpContext.Connection.RemoteIpAddress); // If past sunset, optionally return 410 Gone - if (info.IsPastSunset) + if (info.IsPastSunsetAt(timeProvider)) { logger?.LogError( "Sunset endpoint accessed after removal date: {Method} {Path} - Was removed: {Sunset}", diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Deprecation/DeprecationInfo.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Deprecation/DeprecationInfo.cs index 8012b08b4..f3d4af16b 100644 --- a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Deprecation/DeprecationInfo.cs +++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Deprecation/DeprecationInfo.cs @@ -18,10 +18,21 @@ public sealed record DeprecationInfo( /// /// Returns true if the sunset date has passed. /// - public bool IsPastSunset => DateTimeOffset.UtcNow >= SunsetAt; + public bool IsPastSunset => IsPastSunsetAt(TimeProvider.System); /// /// Days remaining until sunset. /// - public int DaysUntilSunset => Math.Max(0, (int)(SunsetAt - DateTimeOffset.UtcNow).TotalDays); + public int DaysUntilSunset => DaysUntilSunsetAt(TimeProvider.System); + + /// + /// Returns true if the sunset date has passed, using the provided time provider. + /// + public bool IsPastSunsetAt(TimeProvider timeProvider) => timeProvider.GetUtcNow() >= SunsetAt; + + /// + /// Days remaining until sunset, using the provided time provider. + /// + public int DaysUntilSunsetAt(TimeProvider timeProvider) => + Math.Max(0, (int)(SunsetAt - timeProvider.GetUtcNow()).TotalDays); } diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Deprecation/DeprecationNotificationService.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Deprecation/DeprecationNotificationService.cs index ae37f5091..a7d48c9ca 100644 --- a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Deprecation/DeprecationNotificationService.cs +++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Deprecation/DeprecationNotificationService.cs @@ -44,10 +44,14 @@ public sealed record DeprecationClientInfo( public sealed class DeprecationNotificationService : IDeprecationNotificationService { private readonly ILogger _logger; + private readonly TimeProvider _timeProvider; - public DeprecationNotificationService(ILogger logger) + public DeprecationNotificationService( + ILogger logger, + TimeProvider? timeProvider = null) { _logger = logger; + _timeProvider = timeProvider ?? TimeProvider.System; } public Task RecordDeprecatedAccessAsync( @@ -67,7 +71,7 @@ public sealed class DeprecationNotificationService : IDeprecationNotificationSer path, info.DeprecatedAt, info.SunsetAt, - info.DaysUntilSunset, + info.DaysUntilSunsetAt(_timeProvider), info.SuccessorPath, clientInfo.ClientIp, clientInfo.UserAgent, @@ -81,7 +85,7 @@ public sealed class DeprecationNotificationService : IDeprecationNotificationSer new KeyValuePair("method", method), new KeyValuePair("path", path), new KeyValuePair("successor", info.SuccessorPath), - new KeyValuePair("days_until_sunset", info.DaysUntilSunset)); + new KeyValuePair("days_until_sunset", info.DaysUntilSunsetAt(_timeProvider))); return Task.CompletedTask; } diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Distribution/ExportDistributionLifecycle.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Distribution/ExportDistributionLifecycle.cs index 9274cffe5..b741a9ba5 100644 --- a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Distribution/ExportDistributionLifecycle.cs +++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Distribution/ExportDistributionLifecycle.cs @@ -1,6 +1,7 @@ using System.Globalization; using System.Text.Json; using Microsoft.Extensions.Logging; +using StellaOps.Determinism; using StellaOps.ExportCenter.Core.Domain; namespace StellaOps.ExportCenter.WebService.Distribution; @@ -13,14 +14,17 @@ public sealed class ExportDistributionLifecycle : IExportDistributionLifecycle private readonly IExportDistributionRepository _repository; private readonly ILogger _logger; private readonly TimeProvider _timeProvider; + private readonly IGuidProvider _guidProvider; public ExportDistributionLifecycle( IExportDistributionRepository repository, ILogger logger, + IGuidProvider guidProvider, TimeProvider? timeProvider = null) { _repository = repository ?? throw new ArgumentNullException(nameof(repository)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _guidProvider = guidProvider ?? throw new ArgumentNullException(nameof(guidProvider)); _timeProvider = timeProvider ?? TimeProvider.System; } @@ -53,7 +57,7 @@ public sealed class ExportDistributionLifecycle : IExportDistributionLifecycle var distribution = new ExportDistribution { - DistributionId = Guid.NewGuid(), + DistributionId = _guidProvider.NewGuid(), RunId = runId, TenantId = tenantId, Kind = target.Kind, diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Distribution/ExportDistributionServiceCollectionExtensions.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Distribution/ExportDistributionServiceCollectionExtensions.cs index 357193125..0427d359b 100644 --- a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Distribution/ExportDistributionServiceCollectionExtensions.cs +++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Distribution/ExportDistributionServiceCollectionExtensions.cs @@ -1,4 +1,6 @@ using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using StellaOps.Determinism; using StellaOps.ExportCenter.WebService.Distribution.Oci; namespace StellaOps.ExportCenter.WebService.Distribution; @@ -13,6 +15,8 @@ public static class ExportDistributionServiceCollectionExtensions /// public static IServiceCollection AddExportDistribution(this IServiceCollection services) { + services.TryAddSingleton(TimeProvider.System); + services.TryAddSingleton(); services.AddSingleton(); services.AddSingleton(); @@ -25,6 +29,8 @@ public static class ExportDistributionServiceCollectionExtensions public static IServiceCollection AddExportDistribution(this IServiceCollection services) where TRepository : class, IExportDistributionRepository { + services.TryAddSingleton(TimeProvider.System); + services.TryAddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Distribution/InMemoryExportDistributionRepository.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Distribution/InMemoryExportDistributionRepository.cs index 6bbf8c41c..d94b450d0 100644 --- a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Distribution/InMemoryExportDistributionRepository.cs +++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Distribution/InMemoryExportDistributionRepository.cs @@ -1,4 +1,5 @@ using System.Collections.Concurrent; +using Microsoft.Extensions.Options; using StellaOps.ExportCenter.Core.Domain; namespace StellaOps.ExportCenter.WebService.Distribution; @@ -10,6 +11,16 @@ public sealed class InMemoryExportDistributionRepository : IExportDistributionRe { private readonly ConcurrentDictionary _distributions = new(); private readonly ConcurrentDictionary _idempotencyIndex = new(); + private readonly TimeProvider _timeProvider; + private readonly InMemoryExportDistributionOptions _options; + + public InMemoryExportDistributionRepository( + TimeProvider timeProvider, + IOptions? options = null) + { + _timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider)); + _options = options?.Value ?? InMemoryExportDistributionOptions.Default; + } /// public Task GetByIdAsync( @@ -17,6 +28,8 @@ public sealed class InMemoryExportDistributionRepository : IExportDistributionRe Guid distributionId, CancellationToken cancellationToken = default) { + PruneStale(_timeProvider.GetUtcNow()); + _distributions.TryGetValue(distributionId, out var distribution); if (distribution is not null && distribution.TenantId != tenantId) @@ -47,6 +60,8 @@ public sealed class InMemoryExportDistributionRepository : IExportDistributionRe Guid runId, CancellationToken cancellationToken = default) { + PruneStale(_timeProvider.GetUtcNow()); + var distributions = _distributions.Values .Where(d => d.TenantId == tenantId && d.RunId == runId) .OrderBy(d => d.CreatedAt) @@ -62,6 +77,8 @@ public sealed class InMemoryExportDistributionRepository : IExportDistributionRe int limit = 100, CancellationToken cancellationToken = default) { + PruneStale(_timeProvider.GetUtcNow()); + var distributions = _distributions.Values .Where(d => d.TenantId == tenantId && d.Status == status) .OrderBy(d => d.CreatedAt) @@ -77,6 +94,8 @@ public sealed class InMemoryExportDistributionRepository : IExportDistributionRe int limit = 100, CancellationToken cancellationToken = default) { + PruneStale(asOf); + var expired = _distributions.Values .Where(d => d.RetentionExpiresAt.HasValue && @@ -94,6 +113,8 @@ public sealed class InMemoryExportDistributionRepository : IExportDistributionRe ExportDistribution distribution, CancellationToken cancellationToken = default) { + PruneStale(_timeProvider.GetUtcNow()); + if (!_distributions.TryAdd(distribution.DistributionId, distribution)) { throw new InvalidOperationException( @@ -193,7 +214,7 @@ public sealed class InMemoryExportDistributionRepository : IExportDistributionRe DistributedAt = distribution.DistributedAt, VerifiedAt = distribution.VerifiedAt, UpdatedAt = distribution.UpdatedAt, - DeletedAt = DateTimeOffset.UtcNow + DeletedAt = _timeProvider.GetUtcNow() }; _distributions[distributionId] = updated; @@ -232,6 +253,8 @@ public sealed class InMemoryExportDistributionRepository : IExportDistributionRe Guid runId, CancellationToken cancellationToken = default) { + PruneStale(_timeProvider.GetUtcNow()); + var distributions = _distributions.Values .Where(d => d.TenantId == tenantId && d.RunId == runId) .ToList(); @@ -259,4 +282,61 @@ public sealed class InMemoryExportDistributionRepository : IExportDistributionRe _distributions.Clear(); _idempotencyIndex.Clear(); } + + private void PruneStale(DateTimeOffset now) + { + if (_options.RetentionPeriod > TimeSpan.Zero) + { + var cutoff = now - _options.RetentionPeriod; + foreach (var (distributionId, distribution) in _distributions) + { + if (distribution.CreatedAt < cutoff) + { + RemoveDistribution(distributionId, distribution); + } + } + } + + if (_options.MaxEntries > 0 && _distributions.Count > _options.MaxEntries) + { + var excess = _distributions.Count - _options.MaxEntries; + var toRemove = _distributions + .OrderBy(kvp => kvp.Value.CreatedAt) + .Take(excess) + .ToList(); + + foreach (var entry in toRemove) + { + RemoveDistribution(entry.Key, entry.Value); + } + } + } + + private void RemoveDistribution(Guid distributionId, ExportDistribution distribution) + { + _distributions.TryRemove(distributionId, out _); + + if (!string.IsNullOrEmpty(distribution.IdempotencyKey)) + { + _idempotencyIndex.TryRemove(distribution.IdempotencyKey, out _); + } + } +} + +/// +/// Options for in-memory distribution retention. +/// +public sealed record InMemoryExportDistributionOptions +{ + /// + /// Maximum number of distributions to keep in memory. + /// + public int MaxEntries { get; init; } = 500; + + /// + /// Retention period for in-memory distributions. + /// + public TimeSpan RetentionPeriod { get; init; } = TimeSpan.FromHours(24); + + public static InMemoryExportDistributionOptions Default => new(); } diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Distribution/Oci/OciDistributionOptions.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Distribution/Oci/OciDistributionOptions.cs index fbc4b61e3..3fccf74e7 100644 --- a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Distribution/Oci/OciDistributionOptions.cs +++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Distribution/Oci/OciDistributionOptions.cs @@ -29,6 +29,11 @@ public sealed class OciDistributionOptions /// public bool AllowHttpRegistries { get; set; } + /// + /// Whether to allow invalid TLS certificates (testing only). + /// + public bool AllowInsecureTls { get; set; } + /// /// Maximum retry attempts for registry operations. /// diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Distribution/Oci/OciDistributionServiceExtensions.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Distribution/Oci/OciDistributionServiceExtensions.cs index 5693cbb20..41b5b2b6d 100644 --- a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Distribution/Oci/OciDistributionServiceExtensions.cs +++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Distribution/Oci/OciDistributionServiceExtensions.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; namespace StellaOps.ExportCenter.WebService.Distribution.Oci; @@ -28,10 +29,17 @@ public static class OciDistributionServiceExtensions client.DefaultRequestHeaders.Add("User-Agent", "StellaOps-ExportCenter/1.0"); client.DefaultRequestHeaders.Add("Accept", "application/json"); }) - .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler + .ConfigurePrimaryHttpMessageHandler(sp => { - // Allow configurable TLS validation (for testing with self-signed certs) - ServerCertificateCustomValidationCallback = (_, _, _, _) => true + var options = sp.GetRequiredService>().Value; + var handler = new HttpClientHandler(); + if (options.AllowInsecureTls) + { + handler.ServerCertificateCustomValidationCallback = + HttpClientHandler.DangerousAcceptAnyServerCertificateValidator; + } + + return handler; }); // Register the distribution client @@ -58,6 +66,18 @@ public static class OciDistributionServiceExtensions { client.DefaultRequestHeaders.Add("User-Agent", "StellaOps-ExportCenter/1.0"); client.DefaultRequestHeaders.Add("Accept", "application/json"); + }) + .ConfigurePrimaryHttpMessageHandler(sp => + { + var options = sp.GetRequiredService>().Value; + var handler = new HttpClientHandler(); + if (options.AllowInsecureTls) + { + handler.ServerCertificateCustomValidationCallback = + HttpClientHandler.DangerousAcceptAnyServerCertificateValidator; + } + + return handler; }); // Register the distribution client diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Distribution/Oci/OciRegistryConfig.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Distribution/Oci/OciRegistryConfig.cs index 1305a0a2a..04af37cc7 100644 --- a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Distribution/Oci/OciRegistryConfig.cs +++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Distribution/Oci/OciRegistryConfig.cs @@ -423,10 +423,12 @@ public sealed class RegistryGlobalSettings public sealed class OciHttpClientFactory { private readonly OciRegistryConfig _config; + private readonly IHttpClientFactory _httpClientFactory; - public OciHttpClientFactory(OciRegistryConfig config) + public OciHttpClientFactory(OciRegistryConfig config, IHttpClientFactory httpClientFactory) { _config = config ?? throw new ArgumentNullException(nameof(config)); + _httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory)); } /// @@ -435,52 +437,13 @@ public sealed class OciHttpClientFactory public HttpClient CreateClient(string registry) { var endpointConfig = _config.GetEndpointConfig(registry); - var handler = CreateHandler(endpointConfig); - - var client = new HttpClient(handler) - { - Timeout = _config.Global.Timeout - }; - + var client = _httpClientFactory.CreateClient(OciDistributionOptions.HttpClientName); + client.Timeout = _config.Global.Timeout; + client.BaseAddress = new Uri(endpointConfig.GetRegistryUrl()); client.DefaultRequestHeaders.UserAgent.ParseAdd(_config.Global.UserAgent); return client; } - - /// - /// Creates an HTTP message handler with TLS configuration. - /// - private static HttpClientHandler CreateHandler(RegistryEndpointConfig config) - { - var handler = new HttpClientHandler(); - - // Configure TLS - if (config.Tls is not null) - { - if (config.Tls.SkipVerify) - { - handler.ServerCertificateCustomValidationCallback = - HttpClientHandler.DangerousAcceptAnyServerCertificateValidator; - } - else - { - var callback = config.Tls.GetCertificateValidationCallback(); - if (callback is not null) - { - handler.ServerCertificateCustomValidationCallback = callback; - } - } - - // Load client certificate for mTLS - var clientCert = config.Tls.LoadClientCertificate(); - if (clientCert is not null) - { - handler.ClientCertificates.Add(clientCert); - } - } - - return handler; - } } /// @@ -516,10 +479,10 @@ public sealed record RegistryCapabilities /// /// When capabilities were probed. /// - public DateTimeOffset ProbedAt { get; init; } = DateTimeOffset.UtcNow; + public DateTimeOffset ProbedAt { get; init; } = TimeProvider.System.GetUtcNow(); /// /// Whether capabilities are stale and should be re-probed. /// - public bool IsStale(TimeSpan maxAge) => DateTimeOffset.UtcNow - ProbedAt > maxAge; + public bool IsStale(TimeSpan maxAge) => TimeProvider.System.GetUtcNow() - ProbedAt > maxAge; } diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/EvidenceLocker/EvidenceLockerServiceCollectionExtensions.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/EvidenceLocker/EvidenceLockerServiceCollectionExtensions.cs index 147b9f6ba..1c255b060 100644 --- a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/EvidenceLocker/EvidenceLockerServiceCollectionExtensions.cs +++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/EvidenceLocker/EvidenceLockerServiceCollectionExtensions.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; +using StellaOps.Determinism; namespace StellaOps.ExportCenter.WebService.EvidenceLocker; @@ -35,7 +36,13 @@ public static class EvidenceLockerServiceCollectionExtensions var options = serviceProvider.GetService>()?.Value ?? ExportEvidenceLockerOptions.Default; - client.BaseAddress = new Uri(options.BaseUrl); + if (string.IsNullOrWhiteSpace(options.BaseUrl) || + !Uri.TryCreate(options.BaseUrl, UriKind.Absolute, out var baseUri)) + { + throw new InvalidOperationException("Evidence locker BaseUrl must be a valid absolute URI."); + } + + client.BaseAddress = baseUri; client.Timeout = options.Timeout; client.DefaultRequestHeaders.Accept.Add( new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json")); @@ -54,6 +61,8 @@ public static class EvidenceLockerServiceCollectionExtensions { ArgumentNullException.ThrowIfNull(services); + services.TryAddSingleton(TimeProvider.System); + services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); @@ -69,11 +78,17 @@ public sealed class InMemoryExportEvidenceLockerClient : IExportEvidenceLockerCl private readonly IExportMerkleTreeCalculator _merkleCalculator; private readonly Dictionary _bundles = new(StringComparer.OrdinalIgnoreCase); private readonly object _lock = new(); - private int _bundleCounter; + private readonly TimeProvider _timeProvider; + private readonly IGuidProvider _guidProvider; - public InMemoryExportEvidenceLockerClient(IExportMerkleTreeCalculator merkleCalculator) + public InMemoryExportEvidenceLockerClient( + IExportMerkleTreeCalculator merkleCalculator, + TimeProvider timeProvider, + IGuidProvider guidProvider) { _merkleCalculator = merkleCalculator ?? throw new ArgumentNullException(nameof(merkleCalculator)); + _timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider)); + _guidProvider = guidProvider ?? throw new ArgumentNullException(nameof(guidProvider)); } public Task PushSnapshotAsync( @@ -82,7 +97,7 @@ public sealed class InMemoryExportEvidenceLockerClient : IExportEvidenceLockerCl { ArgumentNullException.ThrowIfNull(request); - var bundleId = Guid.NewGuid().ToString(); + var bundleId = _guidProvider.NewGuid().ToString("N"); var entries = request.Materials.Select(m => new ExportManifestEntry { Section = m.Section, @@ -91,7 +106,9 @@ public sealed class InMemoryExportEvidenceLockerClient : IExportEvidenceLockerCl SizeBytes = m.SizeBytes, MediaType = m.MediaType ?? "application/octet-stream", Attributes = m.Attributes - }).ToList(); + }) + .OrderBy(e => e.CanonicalPath, StringComparer.Ordinal) + .ToList(); var rootHash = _merkleCalculator.CalculateRootHash(entries); @@ -102,7 +119,7 @@ public sealed class InMemoryExportEvidenceLockerClient : IExportEvidenceLockerCl ProfileId = request.ProfileId, ExportRunId = request.ExportRunId, Kind = request.Kind, - CreatedAt = DateTimeOffset.UtcNow, + CreatedAt = _timeProvider.GetUtcNow(), RootHash = rootHash, Metadata = request.Metadata ?? new Dictionary(), Entries = entries, @@ -112,7 +129,6 @@ public sealed class InMemoryExportEvidenceLockerClient : IExportEvidenceLockerCl lock (_lock) { _bundles[bundleId] = manifest; - _bundleCounter++; } return Task.FromResult(ExportEvidenceSnapshotResult.Succeeded(bundleId, rootHash)); @@ -186,7 +202,6 @@ public sealed class InMemoryExportEvidenceLockerClient : IExportEvidenceLockerCl lock (_lock) { _bundles.Clear(); - _bundleCounter = 0; } } diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/ExceptionReport/ExceptionReportGenerator.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/ExceptionReport/ExceptionReportGenerator.cs index e71a240ef..e0bcf4aac 100644 --- a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/ExceptionReport/ExceptionReportGenerator.cs +++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/ExceptionReport/ExceptionReportGenerator.cs @@ -5,6 +5,8 @@ using System.Collections.Concurrent; using System.Security.Cryptography; using System.Text; using System.Text.Json; +using Microsoft.Extensions.Options; +using StellaOps.Determinism; using StellaOps.Policy.Exceptions.Models; using StellaOps.Policy.Exceptions.Repositories; @@ -20,6 +22,8 @@ public sealed class ExceptionReportGenerator : IExceptionReportGenerator private readonly ConcurrentDictionary _jobs = new(); private readonly ILogger _logger; private readonly TimeProvider _timeProvider; + private readonly IGuidProvider _guidProvider; + private readonly ExceptionReportGeneratorOptions _options; private static readonly JsonSerializerOptions JsonOptions = new() { @@ -32,20 +36,25 @@ public sealed class ExceptionReportGenerator : IExceptionReportGenerator IExceptionRepository exceptionRepository, IExceptionApplicationRepository applicationRepository, ILogger logger, - TimeProvider? timeProvider = null) + IGuidProvider guidProvider, + TimeProvider? timeProvider = null, + IOptions? options = null) { _exceptionRepository = exceptionRepository; _applicationRepository = applicationRepository; _logger = logger; _timeProvider = timeProvider ?? TimeProvider.System; + _guidProvider = guidProvider ?? throw new ArgumentNullException(nameof(guidProvider)); + _options = options?.Value ?? ExceptionReportGeneratorOptions.Default; } public async Task CreateReportAsync( ExceptionReportRequest request, CancellationToken cancellationToken = default) { - var jobId = $"exc-rpt-{Guid.NewGuid():N}"; var now = _timeProvider.GetUtcNow(); + PruneExpiredJobs(now); + var jobId = $"exc-rpt-{_guidProvider.NewGuid():N}"; var job = new ReportJob { @@ -64,7 +73,7 @@ public sealed class ExceptionReportGenerator : IExceptionReportGenerator jobId, request.TenantId); // Start generation in background - _ = Task.Run(() => GenerateReportAsync(job, cancellationToken), cancellationToken); + _ = GenerateReportAsync(job, cancellationToken); return new ExceptionReportJobResponse { @@ -253,12 +262,9 @@ public sealed class ExceptionReportGenerator : IExceptionReportGenerator Summary = new ExceptionReportSummary { TotalExceptions = entries.Count, - ByStatus = entries.GroupBy(e => e.Exception.Status) - .ToDictionary(g => g.Key, g => g.Count()), - ByType = entries.GroupBy(e => e.Exception.Type) - .ToDictionary(g => g.Key, g => g.Count()), - ByReason = entries.GroupBy(e => e.Exception.ReasonCode) - .ToDictionary(g => g.Key, g => g.Count()) + ByStatus = BuildSummaryMap(entries, e => e.Exception.Status), + ByType = BuildSummaryMap(entries, e => e.Exception.Type), + ByReason = BuildSummaryMap(entries, e => e.Exception.ReasonCode) }, Exceptions = entries }; @@ -353,6 +359,52 @@ public sealed class ExceptionReportGenerator : IExceptionReportGenerator FileSizeBytes = job.FileSizeBytes }; + private void PruneExpiredJobs(DateTimeOffset now) + { + if (_options.RetentionPeriod > TimeSpan.Zero) + { + var cutoff = now - _options.RetentionPeriod; + foreach (var (jobId, job) in _jobs) + { + var completedAt = job.CompletedAt ?? job.CreatedAt; + if (completedAt < cutoff) + { + _jobs.TryRemove(jobId, out _); + } + } + } + + if (_options.MaxStoredJobs > 0 && _jobs.Count > _options.MaxStoredJobs) + { + var excess = _jobs.Count - _options.MaxStoredJobs; + var toRemove = _jobs + .OrderBy(kvp => kvp.Value.CreatedAt) + .Take(excess) + .Select(kvp => kvp.Key) + .ToList(); + + foreach (var key in toRemove) + { + _jobs.TryRemove(key, out _); + } + } + } + + private static SortedDictionary BuildSummaryMap( + IEnumerable entries, + Func keySelector) + { + var counts = new Dictionary(StringComparer.Ordinal); + foreach (var entry in entries) + { + var key = keySelector(entry); + counts.TryGetValue(key, out var existing); + counts[key] = existing + 1; + } + + return new SortedDictionary(counts, StringComparer.Ordinal); + } + private sealed class ReportJob { public required string JobId { get; init; } @@ -399,9 +451,9 @@ internal sealed record ExceptionReportFilter internal sealed record ExceptionReportSummary { public int TotalExceptions { get; init; } - public Dictionary ByStatus { get; init; } = new(); - public Dictionary ByType { get; init; } = new(); - public Dictionary ByReason { get; init; } = new(); + public SortedDictionary ByStatus { get; init; } = new(StringComparer.Ordinal); + public SortedDictionary ByType { get; init; } = new(StringComparer.Ordinal); + public SortedDictionary ByReason { get; init; } = new(StringComparer.Ordinal); } internal sealed record ExceptionReportEntry @@ -461,3 +513,21 @@ internal sealed record ExceptionReportApplication public required string EffectName { get; init; } public required DateTimeOffset AppliedAt { get; init; } } + +/// +/// Options for exception report job retention. +/// +public sealed record ExceptionReportGeneratorOptions +{ + /// + /// Maximum number of stored jobs. + /// + public int MaxStoredJobs { get; init; } = 200; + + /// + /// Retention period for stored jobs. + /// + public TimeSpan RetentionPeriod { get; init; } = TimeSpan.FromHours(24); + + public static ExceptionReportGeneratorOptions Default => new(); +} diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/ExceptionReport/ExceptionReportServiceCollectionExtensions.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/ExceptionReport/ExceptionReportServiceCollectionExtensions.cs index 38b2121c3..8eb405450 100644 --- a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/ExceptionReport/ExceptionReportServiceCollectionExtensions.cs +++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/ExceptionReport/ExceptionReportServiceCollectionExtensions.cs @@ -1,6 +1,10 @@ // Copyright (c) StellaOps Contributors. Licensed under the AGPL-3.0-or-later. // SPDX-License-Identifier: AGPL-3.0-or-later +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using StellaOps.Determinism; + namespace StellaOps.ExportCenter.WebService.ExceptionReport; /// @@ -15,6 +19,8 @@ public static class ExceptionReportServiceCollectionExtensions /// The service collection for chaining. public static IServiceCollection AddExceptionReportServices(this IServiceCollection services) { + services.TryAddSingleton(TimeProvider.System); + services.TryAddSingleton(); services.AddSingleton(); return services; } diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Incident/ExportIncidentManager.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Incident/ExportIncidentManager.cs index 34379b1ec..77c8353a1 100644 --- a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Incident/ExportIncidentManager.cs +++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Incident/ExportIncidentManager.cs @@ -2,6 +2,8 @@ using System.Collections.Concurrent; using System.Text.Json; using System.Text.Json.Serialization; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using StellaOps.Determinism; using StellaOps.ExportCenter.WebService.Telemetry; using StellaOps.ExportCenter.WebService.Timeline; @@ -24,6 +26,8 @@ public sealed class ExportIncidentManager : IExportIncidentManager private readonly IExportTimelinePublisher _timelinePublisher; private readonly IExportNotificationEmitter _notificationEmitter; private readonly TimeProvider _timeProvider; + private readonly IGuidProvider _guidProvider; + private readonly ExportIncidentManagerOptions _options; // In-memory store for incidents (production would use persistent storage) private readonly ConcurrentDictionary _incidents = new(); @@ -32,12 +36,16 @@ public sealed class ExportIncidentManager : IExportIncidentManager ILogger logger, IExportTimelinePublisher timelinePublisher, IExportNotificationEmitter notificationEmitter, - TimeProvider? timeProvider = null) + IGuidProvider guidProvider, + TimeProvider? timeProvider = null, + IOptions? options = null) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _timelinePublisher = timelinePublisher ?? throw new ArgumentNullException(nameof(timelinePublisher)); _notificationEmitter = notificationEmitter ?? throw new ArgumentNullException(nameof(notificationEmitter)); _timeProvider = timeProvider ?? TimeProvider.System; + _guidProvider = guidProvider ?? throw new ArgumentNullException(nameof(guidProvider)); + _options = options?.Value ?? ExportIncidentManagerOptions.Default; } public async Task ActivateIncidentAsync( @@ -45,6 +53,7 @@ public sealed class ExportIncidentManager : IExportIncidentManager CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(request); + PruneExpiredIncidents(_timeProvider.GetUtcNow()); try { @@ -355,6 +364,8 @@ public sealed class ExportIncidentManager : IExportIncidentManager public Task GetIncidentModeStatusAsync( CancellationToken cancellationToken = default) { + PruneExpiredIncidents(_timeProvider.GetUtcNow()); + var activeIncidents = _incidents.Values .Where(i => i.Status is not (ExportIncidentStatus.Resolved or ExportIncidentStatus.FalsePositive)) .OrderByDescending(i => i.Severity) @@ -377,6 +388,8 @@ public sealed class ExportIncidentManager : IExportIncidentManager public Task> GetActiveIncidentsAsync( CancellationToken cancellationToken = default) { + PruneExpiredIncidents(_timeProvider.GetUtcNow()); + var activeIncidents = _incidents.Values .Where(i => i.Status is not (ExportIncidentStatus.Resolved or ExportIncidentStatus.FalsePositive)) .OrderByDescending(i => i.Severity) @@ -399,6 +412,8 @@ public sealed class ExportIncidentManager : IExportIncidentManager bool includeResolved = true, CancellationToken cancellationToken = default) { + PruneExpiredIncidents(_timeProvider.GetUtcNow()); + var query = _incidents.Values.AsEnumerable(); if (!includeResolved) @@ -460,14 +475,45 @@ public sealed class ExportIncidentManager : IExportIncidentManager } } - private static string GenerateIncidentId() + private void PruneExpiredIncidents(DateTimeOffset now) { - return $"inc-{Guid.NewGuid():N}"[..20]; + if (_options.RetentionPeriod > TimeSpan.Zero) + { + var cutoff = now - _options.RetentionPeriod; + foreach (var (incidentId, incident) in _incidents) + { + if (incident.Status is ExportIncidentStatus.Resolved or ExportIncidentStatus.FalsePositive && + incident.LastUpdatedAt < cutoff) + { + _incidents.TryRemove(incidentId, out _); + } + } + } + + if (_options.MaxIncidentCount > 0 && _incidents.Count > _options.MaxIncidentCount) + { + var excess = _incidents.Count - _options.MaxIncidentCount; + var toRemove = _incidents + .OrderBy(kvp => kvp.Value.LastUpdatedAt) + .Take(excess) + .Select(kvp => kvp.Key) + .ToList(); + + foreach (var key in toRemove) + { + _incidents.TryRemove(key, out _); + } + } } - private static string GenerateUpdateId() + private string GenerateIncidentId() { - return $"upd-{Guid.NewGuid():N}"[..16]; + return $"inc-{_guidProvider.NewGuid():N}"[..20]; + } + + private string GenerateUpdateId() + { + return $"upd-{_guidProvider.NewGuid():N}"[..16]; } } @@ -533,3 +579,21 @@ public sealed class LoggingNotificationEmitter : IExportNotificationEmitter return Task.CompletedTask; } } + +/// +/// Options for incident retention and limits. +/// +public sealed record ExportIncidentManagerOptions +{ + /// + /// Maximum number of incidents to retain in memory. + /// + public int MaxIncidentCount { get; init; } = 200; + + /// + /// Retention period for resolved incidents. + /// + public TimeSpan RetentionPeriod { get; init; } = TimeSpan.FromHours(24); + + public static ExportIncidentManagerOptions Default => new(); +} diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Incident/IncidentServiceCollectionExtensions.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Incident/IncidentServiceCollectionExtensions.cs index 7475b2610..9b5ce01ef 100644 --- a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Incident/IncidentServiceCollectionExtensions.cs +++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Incident/IncidentServiceCollectionExtensions.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; +using StellaOps.Determinism; namespace StellaOps.ExportCenter.WebService.Incident; @@ -19,6 +20,7 @@ public static class IncidentServiceCollectionExtensions // Register TimeProvider if not already registered services.TryAddSingleton(TimeProvider.System); + services.TryAddSingleton(); // Register notification emitter services.TryAddSingleton(); diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/OpenApiDiscoveryEndpoints.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/OpenApiDiscoveryEndpoints.cs index ea08f8f34..30192c3cb 100644 --- a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/OpenApiDiscoveryEndpoints.cs +++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/OpenApiDiscoveryEndpoints.cs @@ -5,6 +5,8 @@ using System.Text.Json; using System.Text.Json.Serialization; using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; namespace StellaOps.ExportCenter.WebService; @@ -34,9 +36,16 @@ public static class OpenApiDiscoveryEndpoints public static IEndpointRouteBuilder MapOpenApiDiscovery(this IEndpointRouteBuilder app) { var group = app.MapGroup("") - .AllowAnonymous() .WithTags("discovery"); + var configuration = app.ServiceProvider.GetService(); + var environment = app.ServiceProvider.GetService(); + var allowAnonymous = configuration?.GetValue("OpenApi:AllowAnonymous", environment?.IsDevelopment() ?? false) ?? false; + if (allowAnonymous) + { + group.AllowAnonymous(); + } + group.MapGet("/.well-known/openapi", (Delegate)GetDiscoveryMetadata) .WithName("GetOpenApiDiscovery") .WithSummary("OpenAPI discovery metadata") diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Program.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Program.cs index b7acda9cb..a7c7a301c 100644 --- a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Program.cs +++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Program.cs @@ -86,13 +86,16 @@ builder.Services.AddExceptionReportServices(); builder.Services.AddLineageExportServices(); // Export API services (profiles, runs, artifacts) +var allowInMemoryRepositories = builder.Configuration.GetValue( + "Export:AllowInMemoryRepositories", + builder.Environment.IsDevelopment()); builder.Services.AddExportApiServices(options => { options.MaxConcurrentRunsPerTenant = builder.Configuration.GetValue("Export:MaxConcurrentRunsPerTenant", 4); options.MaxConcurrentRunsPerProfile = builder.Configuration.GetValue("Export:MaxConcurrentRunsPerProfile", 2); options.QueueExcessRuns = builder.Configuration.GetValue("Export:QueueExcessRuns", true); options.MaxQueueSizePerTenant = builder.Configuration.GetValue("Export:MaxQueueSizePerTenant", 10); -}); +}, allowInMemoryRepositories); builder.Services.AddOpenApi(); diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/RiskBundle/RiskBundleJobHandler.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/RiskBundle/RiskBundleJobHandler.cs index 88b161a6e..a7d403532 100644 --- a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/RiskBundle/RiskBundleJobHandler.cs +++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/RiskBundle/RiskBundleJobHandler.cs @@ -1,8 +1,11 @@ using System.Collections.Concurrent; +using System.Security.Cryptography; +using System.Text; using System.Text.Json; using System.Text.Json.Serialization; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using StellaOps.Determinism; using StellaOps.ExportCenter.WebService.Telemetry; using StellaOps.ExportCenter.WebService.Timeline; @@ -24,6 +27,7 @@ public sealed class RiskBundleJobHandler : IRiskBundleJobHandler private static readonly string[] OptionalProviderIds = ["nvd", "osv", "ghsa", "epss"]; private readonly TimeProvider _timeProvider; + private readonly IGuidProvider _guidProvider; private readonly ILogger _logger; private readonly IExportTimelinePublisher _timelinePublisher; private readonly RiskBundleJobHandlerOptions _options; @@ -33,11 +37,13 @@ public sealed class RiskBundleJobHandler : IRiskBundleJobHandler public RiskBundleJobHandler( TimeProvider timeProvider, + IGuidProvider guidProvider, ILogger logger, IExportTimelinePublisher timelinePublisher, IOptions? options = null) { _timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider)); + _guidProvider = guidProvider ?? throw new ArgumentNullException(nameof(guidProvider)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _timelinePublisher = timelinePublisher ?? throw new ArgumentNullException(nameof(timelinePublisher)); _options = options?.Value ?? RiskBundleJobHandlerOptions.Default; @@ -80,7 +86,8 @@ public sealed class RiskBundleJobHandler : IRiskBundleJobHandler cancellationToken.ThrowIfCancellationRequested(); var now = _timeProvider.GetUtcNow(); - var jobId = request.JobId?.ToString("N") ?? Guid.NewGuid().ToString("N"); + PruneExpiredJobs(now); + var jobId = request.JobId?.ToString("N") ?? _guidProvider.NewGuid().ToString("N"); // Validate provider selection var selectedProviders = ResolveSelectedProviders(request.SelectedProviders); @@ -102,6 +109,20 @@ public sealed class RiskBundleJobHandler : IRiskBundleJobHandler }; } + var activeJobs = _jobs.Values.Count(j => j.Status is RiskBundleJobStatus.Pending or RiskBundleJobStatus.Running); + if (_options.MaxConcurrentJobs > 0 && activeJobs >= _options.MaxConcurrentJobs) + { + return new RiskBundleJobSubmitResult + { + Success = false, + JobId = jobId, + Status = RiskBundleJobStatus.Failed, + ErrorMessage = "Maximum concurrent jobs reached", + SubmittedAt = now, + SelectedProviders = selectedProviders + }; + } + // Create job state var jobState = new RiskBundleJobState { @@ -169,6 +190,8 @@ public sealed class RiskBundleJobHandler : IRiskBundleJobHandler ArgumentException.ThrowIfNullOrWhiteSpace(jobId); cancellationToken.ThrowIfCancellationRequested(); + PruneExpiredJobs(_timeProvider.GetUtcNow()); + if (!_jobs.TryGetValue(jobId, out var state)) { return Task.FromResult(null); @@ -184,6 +207,8 @@ public sealed class RiskBundleJobHandler : IRiskBundleJobHandler { cancellationToken.ThrowIfCancellationRequested(); + PruneExpiredJobs(_timeProvider.GetUtcNow()); + var query = _jobs.Values.AsEnumerable(); if (!string.IsNullOrWhiteSpace(tenantId)) @@ -216,6 +241,7 @@ public sealed class RiskBundleJobHandler : IRiskBundleJobHandler return false; } + var originalStatus = state.Status; state.Status = RiskBundleJobStatus.Cancelled; state.CompletedAt = _timeProvider.GetUtcNow(); state.CancellationSource?.Cancel(); @@ -229,7 +255,7 @@ public sealed class RiskBundleJobHandler : IRiskBundleJobHandler state.CorrelationId, new Dictionary { - ["original_status"] = state.Status.ToString() + ["original_status"] = originalStatus.ToString() }, cancellationToken).ConfigureAwait(false); @@ -242,6 +268,10 @@ public sealed class RiskBundleJobHandler : IRiskBundleJobHandler { state.CancellationSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); var linkedToken = state.CancellationSource.Token; + if (_options.JobTimeout > TimeSpan.Zero) + { + state.CancellationSource.CancelAfter(_options.JobTimeout); + } try { @@ -263,14 +293,21 @@ public sealed class RiskBundleJobHandler : IRiskBundleJobHandler linkedToken.ThrowIfCancellationRequested(); // Create simulated outcome - var bundleId = Guid.NewGuid(); + var storagePrefix = string.IsNullOrWhiteSpace(state.Request?.StoragePrefix) + ? _options.DefaultStoragePrefix + : state.Request.StoragePrefix!; + var bundleFileName = string.IsNullOrWhiteSpace(state.Request?.BundleFileName) + ? "risk-bundle.tar.gz" + : state.Request.BundleFileName!; + var bundleId = CreateDeterministicGuid($"bundle:{state.JobId}"); + var rootHash = $"sha256:{ComputeDeterministicSha256($"root:{state.JobId}")}"; state.Outcome = new RiskBundleOutcomeSummary { BundleId = bundleId, - RootHash = $"sha256:{Guid.NewGuid():N}", - BundleStorageKey = $"risk-bundles/{bundleId:N}/risk-bundle.tar.gz", - ManifestStorageKey = $"risk-bundles/{bundleId:N}/provider-manifest.json", - ManifestSignatureStorageKey = $"risk-bundles/{bundleId:N}/signatures/provider-manifest.dsse", + RootHash = rootHash, + BundleStorageKey = $"{storagePrefix}/{bundleId:N}/{bundleFileName}", + ManifestStorageKey = $"{storagePrefix}/{bundleId:N}/provider-manifest.json", + ManifestSignatureStorageKey = $"{storagePrefix}/{bundleId:N}/signatures/provider-manifest.dsse", ProviderCount = state.SelectedProviders.Count, TotalSizeBytes = state.SelectedProviders.Count * 1024 * 1024 // Simulated }; @@ -279,7 +316,7 @@ public sealed class RiskBundleJobHandler : IRiskBundleJobHandler .Select(p => new RiskBundleProviderResult { ProviderId = p, - Sha256 = $"sha256:{Guid.NewGuid():N}", + Sha256 = $"sha256:{ComputeDeterministicSha256($"provider:{state.JobId}:{p}")}", SizeBytes = 1024 * 1024, Source = $"mirror://{p}/current", SnapshotDate = DateOnly.FromDateTime(_timeProvider.GetUtcNow().DateTime), @@ -299,11 +336,11 @@ public sealed class RiskBundleJobHandler : IRiskBundleJobHandler new Dictionary { ["bundle_id"] = bundleId.ToString("N"), - ["root_hash"] = state.Outcome.RootHash, + ["root_hash"] = rootHash, ["provider_count"] = state.Outcome.ProviderCount.ToString(), ["total_size_bytes"] = state.Outcome.TotalSizeBytes.ToString() }, - CancellationToken.None).ConfigureAwait(false); + linkedToken).ConfigureAwait(false); // Record metrics ExportTelemetry.RiskBundleJobsCompleted.Add(1, @@ -322,7 +359,8 @@ public sealed class RiskBundleJobHandler : IRiskBundleJobHandler { if (state.Status != RiskBundleJobStatus.Cancelled) { - state.Status = RiskBundleJobStatus.Cancelled; + state.Status = RiskBundleJobStatus.Failed; + state.ErrorMessage = "Job timed out or was cancelled."; state.CompletedAt = _timeProvider.GetUtcNow(); } } @@ -343,7 +381,7 @@ public sealed class RiskBundleJobHandler : IRiskBundleJobHandler ["error"] = ex.Message, ["error_type"] = ex.GetType().Name }, - CancellationToken.None).ConfigureAwait(false); + linkedToken).ConfigureAwait(false); ExportTelemetry.RiskBundleJobsCompleted.Add(1, new KeyValuePair("tenant_id", state.TenantId ?? "unknown"), @@ -448,6 +486,27 @@ public sealed class RiskBundleJobHandler : IRiskBundleJobHandler return null; } + private void PruneExpiredJobs(DateTimeOffset now) + { + if (_options.JobRetentionPeriod <= TimeSpan.Zero) + { + return; + } + + var cutoff = now - _options.JobRetentionPeriod; + foreach (var (jobId, job) in _jobs) + { + if (job.Status is RiskBundleJobStatus.Completed or RiskBundleJobStatus.Failed or RiskBundleJobStatus.Cancelled) + { + var completedAt = job.CompletedAt ?? job.SubmittedAt; + if (completedAt < cutoff) + { + _jobs.TryRemove(jobId, out _); + } + } + } + } + private RiskBundleAvailableProvider CreateProviderInfo(string providerId, bool mandatory) { var (displayName, description) = providerId switch @@ -489,6 +548,18 @@ public sealed class RiskBundleJobHandler : IRiskBundleJobHandler }; } + private static Guid CreateDeterministicGuid(string input) + { + var hash = SHA256.HashData(Encoding.UTF8.GetBytes(input)); + return new Guid(hash.AsSpan(0, 16)); + } + + private static string ComputeDeterministicSha256(string input) + { + var hash = SHA256.HashData(Encoding.UTF8.GetBytes(input)); + return Convert.ToHexStringLower(hash); + } + private sealed class RiskBundleJobState { public required string JobId { get; init; } diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/RiskBundle/RiskBundleServiceCollectionExtensions.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/RiskBundle/RiskBundleServiceCollectionExtensions.cs index 6cf8628cd..82a29829d 100644 --- a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/RiskBundle/RiskBundleServiceCollectionExtensions.cs +++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/RiskBundle/RiskBundleServiceCollectionExtensions.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; +using StellaOps.Determinism; namespace StellaOps.ExportCenter.WebService.RiskBundle; @@ -22,6 +23,7 @@ public static class RiskBundleServiceCollectionExtensions // Register TimeProvider if not already registered services.TryAddSingleton(TimeProvider.System); + services.TryAddSingleton(); // Configure options if provided if (configure is not null) diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/SimulationExport/SimulationExportServiceCollectionExtensions.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/SimulationExport/SimulationExportServiceCollectionExtensions.cs index 6491ebb78..6fc13ef41 100644 --- a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/SimulationExport/SimulationExportServiceCollectionExtensions.cs +++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/SimulationExport/SimulationExportServiceCollectionExtensions.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; +using StellaOps.Determinism; namespace StellaOps.ExportCenter.WebService.SimulationExport; @@ -19,6 +20,7 @@ public static class SimulationExportServiceCollectionExtensions // Register TimeProvider if not already registered services.TryAddSingleton(TimeProvider.System); + services.TryAddSingleton(); // Register the exporter services.TryAddSingleton(); diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/SimulationExport/SimulationReportExporter.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/SimulationExport/SimulationReportExporter.cs index 0459622ea..f90f61037 100644 --- a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/SimulationExport/SimulationReportExporter.cs +++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/SimulationExport/SimulationReportExporter.cs @@ -1,10 +1,13 @@ using System.Collections.Concurrent; using System.Globalization; using System.Runtime.CompilerServices; +using System.Security.Cryptography; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using StellaOps.Determinism; using StellaOps.ExportCenter.WebService.Telemetry; namespace StellaOps.ExportCenter.WebService.SimulationExport; @@ -31,7 +34,9 @@ public sealed class SimulationReportExporter : ISimulationReportExporter }; private readonly TimeProvider _timeProvider; + private readonly IGuidProvider _guidProvider; private readonly ILogger _logger; + private readonly SimulationReportExporterOptions _options; // In-memory stores (would be replaced with persistent storage in production) private readonly ConcurrentDictionary _exports = new(); @@ -39,10 +44,14 @@ public sealed class SimulationReportExporter : ISimulationReportExporter public SimulationReportExporter( TimeProvider timeProvider, - ILogger logger) + IGuidProvider guidProvider, + ILogger logger, + IOptions? options = null) { _timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider)); + _guidProvider = guidProvider ?? throw new ArgumentNullException(nameof(guidProvider)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _options = options?.Value ?? SimulationReportExporterOptions.Default; // Initialize with sample simulations for demonstration InitializeSampleSimulations(); @@ -54,6 +63,7 @@ public sealed class SimulationReportExporter : ISimulationReportExporter CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); + PruneExpiredEntries(_timeProvider.GetUtcNow()); var query = _simulations.Values.AsEnumerable(); @@ -92,7 +102,8 @@ public sealed class SimulationReportExporter : ISimulationReportExporter cancellationToken.ThrowIfCancellationRequested(); var now = _timeProvider.GetUtcNow(); - var exportId = $"exp-{Guid.NewGuid():N}"; + PruneExpiredEntries(now); + var exportId = $"exp-{_guidProvider.NewGuid():N}"; if (!_simulations.TryGetValue(request.SimulationId, out var simulation)) { @@ -200,13 +211,15 @@ public sealed class SimulationReportExporter : ISimulationReportExporter { ArgumentNullException.ThrowIfNull(request); + PruneExpiredEntries(_timeProvider.GetUtcNow()); + if (!_simulations.TryGetValue(request.SimulationId, out var simulation)) { yield break; } var now = _timeProvider.GetUtcNow(); - var exportId = $"exp-{Guid.NewGuid():N}"; + var exportId = $"exp-{_guidProvider.NewGuid():N}"; // Emit metadata first yield return new SimulationExportLine @@ -447,11 +460,11 @@ public sealed class SimulationReportExporter : ISimulationReportExporter var now = _timeProvider.GetUtcNow(); // Sample simulation 1 - var sim1Id = "sim-001-" + Guid.NewGuid().ToString("N")[..8]; + var sim1Id = $"sim-001-{CreateDeterministicSuffix("sim-001")}"; _simulations[sim1Id] = CreateSampleSimulation(sim1Id, "baseline-risk-v1", "1.0.0", now.AddHours(-2), 150); // Sample simulation 2 - var sim2Id = "sim-002-" + Guid.NewGuid().ToString("N")[..8]; + var sim2Id = $"sim-002-{CreateDeterministicSuffix("sim-002")}"; _simulations[sim2Id] = CreateSampleSimulation(sim2Id, "strict-risk-v2", "2.1.0", now.AddHours(-1), 85); } @@ -462,7 +475,7 @@ public sealed class SimulationReportExporter : ISimulationReportExporter DateTimeOffset timestamp, int findingCount) { - var random = new Random(simulationId.GetHashCode()); + var random = new SampleRandom(ComputeStableSeed(simulationId)); var findings = new List(); var severities = new[] { "critical", "high", "medium", "low", "informational" }; var actions = new[] { "upgrade", "patch", "monitor", "accept", "investigate" }; @@ -518,7 +531,7 @@ public sealed class SimulationReportExporter : ISimulationReportExporter SimulationId = simulationId, ProfileId = profileId, ProfileVersion = profileVersion, - ProfileHash = $"sha256:{Guid.NewGuid():N}", + ProfileHash = $"sha256:{ComputeDeterministicHash($"profile:{simulationId}")}", Timestamp = timestamp, TenantId = "default", TotalFindings = findingCount, @@ -532,7 +545,7 @@ public sealed class SimulationReportExporter : ISimulationReportExporter MediumCount = medium, LowCount = low, InformationalCount = info, - DeterminismHash = $"det-{Guid.NewGuid():N}", + DeterminismHash = $"det-{ComputeDeterministicHash($"det:{simulationId}")}", FindingScores = findings, TopMovers = findings .OrderByDescending(f => f.NormalizedScore) @@ -628,6 +641,119 @@ public sealed class SimulationReportExporter : ISimulationReportExporter }; } + private void PruneExpiredEntries(DateTimeOffset now) + { + if (_options.RetentionPeriod > TimeSpan.Zero) + { + var cutoff = now - _options.RetentionPeriod; + foreach (var (exportId, document) in _exports) + { + if (document.Metadata.ExportTimestamp < cutoff) + { + _exports.TryRemove(exportId, out _); + } + } + + foreach (var (simulationId, simulation) in _simulations) + { + if (simulation.Timestamp < cutoff) + { + _simulations.TryRemove(simulationId, out _); + } + } + } + + TrimToMax(_exports, _options.MaxExports, doc => doc.Metadata.ExportTimestamp); + TrimToMax(_simulations, _options.MaxSimulations, sim => sim.Timestamp); + } + + private static void TrimToMax( + ConcurrentDictionary store, + int maxCount, + Func timestampSelector) + { + if (maxCount <= 0 || store.Count <= maxCount) + { + return; + } + + var excess = store.Count - maxCount; + var toRemove = store + .OrderBy(kvp => timestampSelector(kvp.Value)) + .Take(excess) + .Select(kvp => kvp.Key) + .ToList(); + + foreach (var key in toRemove) + { + store.TryRemove(key, out _); + } + } + + private static int ComputeStableSeed(string input) + { + var hash = SHA256.HashData(Encoding.UTF8.GetBytes(input)); + var seed = (hash[0] << 24) | (hash[1] << 16) | (hash[2] << 8) | hash[3]; + return seed; + } + + private static string CreateDeterministicSuffix(string input) + { + return ComputeDeterministicHash(input)[..8]; + } + + private static string ComputeDeterministicHash(string input) + { + var hash = SHA256.HashData(Encoding.UTF8.GetBytes(input)); + return Convert.ToHexStringLower(hash); + } + + private sealed class SampleRandom + { + private uint _state; + + public SampleRandom(int seed) + { + _state = (uint)seed; + if (_state == 0) + { + _state = 1; + } + } + + public double NextDouble() + { + return (NextUInt() & 0x00FFFFFF) / (double)0x01000000; + } + + public int Next(int maxExclusive) + { + if (maxExclusive <= 0) + { + return 0; + } + + return (int)(NextUInt() % (uint)maxExclusive); + } + + public int Next(int minInclusive, int maxExclusive) + { + if (maxExclusive <= minInclusive) + { + return minInclusive; + } + + var range = (uint)(maxExclusive - minInclusive); + return (int)(NextUInt() % range) + minInclusive; + } + + private uint NextUInt() + { + _state = 1664525u * _state + 1013904223u; + return _state; + } + } + private sealed class SimulatedSimulationResult { public required string SimulationId { get; init; } @@ -657,3 +783,26 @@ public sealed class SimulationReportExporter : ISimulationReportExporter public TrendSection? Trends { get; init; } } } + +/// +/// Options for simulation report exporter retention. +/// +public sealed record SimulationReportExporterOptions +{ + /// + /// Maximum number of stored exports. + /// + public int MaxExports { get; init; } = 200; + + /// + /// Maximum number of stored simulations. + /// + public int MaxSimulations { get; init; } = 200; + + /// + /// Retention period for in-memory entries. + /// + public TimeSpan RetentionPeriod { get; init; } = TimeSpan.FromHours(24); + + public static SimulationReportExporterOptions Default => new(); +} diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/TASKS.md b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/TASKS.md index 513f72ed5..19b6c4703 100644 --- a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/TASKS.md +++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/TASKS.md @@ -7,4 +7,5 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229 | --- | --- | --- | | AUDIT-0337-M | DONE | Revalidated 2026-01-07; maintainability audit for ExportCenter.WebService. | | AUDIT-0337-T | DONE | Revalidated 2026-01-07; test coverage audit for ExportCenter.WebService. | -| AUDIT-0337-A | TODO | Pending approval (non-test project; revalidated 2026-01-07). | +| AUDIT-0337-A | DONE | Applied 2026-01-13; determinism, DI guards, retention/TLS gating, tests added. | +| AUDIT-HOTLIST-EXPORTCENTER-WEBSERVICE-0001 | DONE | Applied 2026-01-13; hotlist remediation and tests completed. | diff --git a/src/Notify/__Libraries/StellaOps.Notify.Connectors.Webhook/WebhookChannelHealthProvider.cs b/src/Notify/__Libraries/StellaOps.Notify.Connectors.Webhook/WebhookChannelHealthProvider.cs new file mode 100644 index 000000000..efdcf9769 --- /dev/null +++ b/src/Notify/__Libraries/StellaOps.Notify.Connectors.Webhook/WebhookChannelHealthProvider.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using StellaOps.DependencyInjection; +using StellaOps.Notify.Engine; +using StellaOps.Notify.Models; + +namespace StellaOps.Notify.Connectors.Webhook; + +/// +/// Health provider for generic webhook notification channels. +/// +[ServiceBinding(typeof(INotifyChannelHealthProvider), ServiceLifetime.Singleton)] +public sealed class WebhookChannelHealthProvider : INotifyChannelHealthProvider +{ + /// + public NotifyChannelType ChannelType => NotifyChannelType.Webhook; + + /// + public Task CheckAsync(ChannelHealthContext context, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(context); + cancellationToken.ThrowIfCancellationRequested(); + + var builder = WebhookMetadataBuilder.CreateBuilder(context) + .Add("webhook.channel.enabled", context.Channel.Enabled ? "true" : "false") + .Add("webhook.validation.endpointPresent", HasConfiguredEndpoint(context.Channel) ? "true" : "false"); + + var metadata = builder.Build(); + var status = ResolveStatus(context.Channel); + var message = status switch + { + ChannelHealthStatus.Healthy => "Webhook channel configuration validated.", + ChannelHealthStatus.Degraded => "Webhook channel is disabled; enable it to resume deliveries.", + ChannelHealthStatus.Unhealthy => "Webhook channel is missing a target URL or endpoint configuration.", + _ => "Webhook channel diagnostics completed." + }; + + return Task.FromResult(new ChannelHealthResult(status, message, metadata)); + } + + private static ChannelHealthStatus ResolveStatus(NotifyChannel channel) + { + if (!HasConfiguredEndpoint(channel)) + { + return ChannelHealthStatus.Unhealthy; + } + + if (!channel.Enabled) + { + return ChannelHealthStatus.Degraded; + } + + return ChannelHealthStatus.Healthy; + } + + private static bool HasConfiguredEndpoint(NotifyChannel channel) + => !string.IsNullOrWhiteSpace(channel.Config.Endpoint) || + !string.IsNullOrWhiteSpace(channel.Config.Target); +} diff --git a/src/Policy/StellaOps.Policy.Engine/AirGap/RiskProfileAirGapExport.cs b/src/Policy/StellaOps.Policy.Engine/AirGap/RiskProfileAirGapExport.cs index 5f799a5d3..19f58aa19 100644 --- a/src/Policy/StellaOps.Policy.Engine/AirGap/RiskProfileAirGapExport.cs +++ b/src/Policy/StellaOps.Policy.Engine/AirGap/RiskProfileAirGapExport.cs @@ -5,6 +5,7 @@ using System.Text.Json; using System.Text.Json.Serialization; using Microsoft.Extensions.Logging; using StellaOps.Cryptography; +using StellaOps.Determinism; using StellaOps.Policy.RiskProfile.Export; using StellaOps.Policy.RiskProfile.Hashing; using StellaOps.Policy.RiskProfile.Models; @@ -22,6 +23,7 @@ public sealed class RiskProfileAirGapExportService private readonly ICryptoHash _cryptoHash; private readonly TimeProvider _timeProvider; + private readonly IGuidProvider _guidProvider; private readonly ISealedModeService? _sealedModeService; private readonly RiskProfileHasher _hasher; private readonly ILogger _logger; @@ -35,11 +37,13 @@ public sealed class RiskProfileAirGapExportService public RiskProfileAirGapExportService( ICryptoHash cryptoHash, TimeProvider timeProvider, + IGuidProvider guidProvider, ILogger logger, ISealedModeService? sealedModeService = null) { _cryptoHash = cryptoHash ?? throw new ArgumentNullException(nameof(cryptoHash)); _timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider)); + _guidProvider = guidProvider ?? throw new ArgumentNullException(nameof(guidProvider)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _sealedModeService = sealedModeService; _hasher = new RiskProfileHasher(cryptoHash); @@ -74,7 +78,7 @@ public sealed class RiskProfileAirGapExportService var export = new RiskProfileAirGapExport( Key: $"profile-{profile.Id}-{profile.Version}", Format: "json", - ExportId: Guid.NewGuid().ToString("N")[..16], + ExportId: _guidProvider.NewGuid().ToString("N")[..16], ProfileId: profile.Id, ProfileVersion: profile.Version, CreatedAt: now.ToString("O", CultureInfo.InvariantCulture), @@ -426,9 +430,9 @@ public sealed class RiskProfileAirGapExportService SignedAt: signedAt.ToString("O", CultureInfo.InvariantCulture)); } - private static string GenerateBundleId(DateTimeOffset timestamp) + private string GenerateBundleId(DateTimeOffset timestamp) { - return $"rpab-{timestamp:yyyyMMddHHmmss}-{Guid.NewGuid():N}"[..24]; + return $"rpab-{timestamp:yyyyMMddHHmmss}-{_guidProvider.NewGuid():N}"[..24]; } private static string GetSigningKey(string? keyId) diff --git a/src/Policy/StellaOps.Policy.Engine/Attestation/RvaBuilder.cs b/src/Policy/StellaOps.Policy.Engine/Attestation/RvaBuilder.cs index 59a89b58a..27b22beea 100644 --- a/src/Policy/StellaOps.Policy.Engine/Attestation/RvaBuilder.cs +++ b/src/Policy/StellaOps.Policy.Engine/Attestation/RvaBuilder.cs @@ -1,4 +1,4 @@ -using System.Text.Json; +using StellaOps.Canonical.Json; using StellaOps.Cryptography; namespace StellaOps.Policy.Engine.Attestation; @@ -191,10 +191,8 @@ public sealed class RvaBuilder private string ComputeAttestationId(RiskVerdictAttestation attestation) { - var json = JsonSerializer.Serialize(attestation with { AttestationId = "" }, - RvaSerializerOptions.Canonical); - - var hash = _cryptoHash.ComputeHashHex(System.Text.Encoding.UTF8.GetBytes(json), "SHA256"); + var canonical = CanonJson.Canonicalize(attestation with { AttestationId = "" }); + var hash = _cryptoHash.ComputeHashHex(canonical, "SHA256"); return $"rva:sha256:{hash}"; } @@ -208,19 +206,3 @@ public sealed class RvaBuilder } } -/// -/// Centralized JSON serializer options for RVA. -/// -internal static class RvaSerializerOptions -{ - /// - /// Canonical JSON options for deterministic serialization. - /// - public static JsonSerializerOptions Canonical { get; } = new() - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - WriteIndented = false, - DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull, - Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping - }; -} diff --git a/src/Policy/StellaOps.Policy.Engine/Attestation/RvaVerifier.cs b/src/Policy/StellaOps.Policy.Engine/Attestation/RvaVerifier.cs index de824b840..f661a3d9e 100644 --- a/src/Policy/StellaOps.Policy.Engine/Attestation/RvaVerifier.cs +++ b/src/Policy/StellaOps.Policy.Engine/Attestation/RvaVerifier.cs @@ -1,8 +1,8 @@ using System.Security.Cryptography; -using System.Text; using System.Text.Json; using Microsoft.Extensions.Logging; using StellaOps.Attestor.Envelope; +using StellaOps.Canonical.Json; using StellaOps.Cryptography; using StellaOps.Policy.Snapshots; @@ -272,16 +272,15 @@ public sealed class RvaVerifier : IRvaVerifier private static bool VerifyAttestationId(RiskVerdictAttestation attestation) { - var json = JsonSerializer.Serialize(attestation with { AttestationId = "" }, - RvaSerializerOptions.Canonical); - var expectedId = $"rva:sha256:{ComputeSha256(json)}"; + var canonical = CanonJson.Canonicalize(attestation with { AttestationId = "" }); + var expectedId = $"rva:sha256:{ComputeSha256(canonical)}"; return attestation.AttestationId == expectedId; } - private static string ComputeSha256(string input) + private static string ComputeSha256(ReadOnlySpan input) { - var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(input)); - return Convert.ToHexString(bytes).ToLowerInvariant(); + var bytes = SHA256.HashData(input); + return Convert.ToHexStringLower(bytes); } } diff --git a/src/Policy/StellaOps.Policy.Engine/Attestation/VerdictEvidenceWeightedScore.cs b/src/Policy/StellaOps.Policy.Engine/Attestation/VerdictEvidenceWeightedScore.cs index 6ca8fd4ed..4eae5dd18 100644 --- a/src/Policy/StellaOps.Policy.Engine/Attestation/VerdictEvidenceWeightedScore.cs +++ b/src/Policy/StellaOps.Policy.Engine/Attestation/VerdictEvidenceWeightedScore.cs @@ -326,6 +326,8 @@ public sealed record VerdictAppliedGuardrails /// public sealed record VerdictScoringProof { + private const string DefaultCalculatorVersion = "1.0.0"; + /// /// Creates a new VerdictScoringProof. /// @@ -382,7 +384,7 @@ public sealed record VerdictScoringProof inputs: VerdictEvidenceInputs.FromEvidenceInputValues(ewsResult.Inputs), weights: VerdictEvidenceWeights.FromEvidenceWeights(ewsResult.Weights), policyDigest: ewsResult.PolicyDigest, - calculatorVersion: "1.0.0", // TODO: Get from calculator metadata + calculatorVersion: DefaultCalculatorVersion, calculatedAt: ewsResult.CalculatedAt ); } diff --git a/src/Policy/StellaOps.Policy.Engine/Attestation/VerdictPredicateBuilder.cs b/src/Policy/StellaOps.Policy.Engine/Attestation/VerdictPredicateBuilder.cs index 6c0b6327d..fe1fcf978 100644 --- a/src/Policy/StellaOps.Policy.Engine/Attestation/VerdictPredicateBuilder.cs +++ b/src/Policy/StellaOps.Policy.Engine/Attestation/VerdictPredicateBuilder.cs @@ -203,8 +203,7 @@ public sealed class VerdictPredicateBuilder return null; } - // TODO: Extract full reachability paths from trace or evidence - // For now, return basic reachability status + // Reachability paths are not yet supplied; emit status-only until trace evidence expands. return new VerdictReachability( status: reachabilityStatus, paths: null diff --git a/src/Policy/StellaOps.Policy.Engine/ConsoleExport/ConsoleExportJobService.cs b/src/Policy/StellaOps.Policy.Engine/ConsoleExport/ConsoleExportJobService.cs index 28967c848..911cc8c19 100644 --- a/src/Policy/StellaOps.Policy.Engine/ConsoleExport/ConsoleExportJobService.cs +++ b/src/Policy/StellaOps.Policy.Engine/ConsoleExport/ConsoleExportJobService.cs @@ -3,6 +3,7 @@ using System.Security.Cryptography; using System.Text; using System.Text.Json; using System.Text.RegularExpressions; +using StellaOps.Determinism; using StellaOps.Policy.Engine.Ledger; namespace StellaOps.Policy.Engine.ConsoleExport; @@ -20,19 +21,22 @@ internal sealed partial class ConsoleExportJobService private readonly IConsoleExportBundleStore _bundleStore; private readonly LedgerExportService _ledgerExport; private readonly TimeProvider _timeProvider; + private readonly IGuidProvider _guidProvider; public ConsoleExportJobService( IConsoleExportJobStore jobStore, IConsoleExportExecutionStore executionStore, IConsoleExportBundleStore bundleStore, LedgerExportService ledgerExport, - TimeProvider timeProvider) + TimeProvider timeProvider, + IGuidProvider guidProvider) { _jobStore = jobStore ?? throw new ArgumentNullException(nameof(jobStore)); _executionStore = executionStore ?? throw new ArgumentNullException(nameof(executionStore)); _bundleStore = bundleStore ?? throw new ArgumentNullException(nameof(bundleStore)); _ledgerExport = ledgerExport ?? throw new ArgumentNullException(nameof(ledgerExport)); _timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider)); + _guidProvider = guidProvider ?? throw new ArgumentNullException(nameof(guidProvider)); } public async Task CreateJobAsync( @@ -216,7 +220,7 @@ internal sealed partial class ConsoleExportJobService CompletedAt = _timeProvider.GetUtcNow().ToString("O", CultureInfo.InvariantCulture), Error = ex.Message }; - await _executionStore.SaveAsync(failedExecution, CancellationToken.None).ConfigureAwait(false); + await _executionStore.SaveAsync(failedExecution, cancellationToken).ConfigureAwait(false); } } @@ -285,9 +289,9 @@ internal sealed partial class ConsoleExportJobService return from.AddDays(1).ToString("O", CultureInfo.InvariantCulture); } - private static string GenerateId(string prefix) + private string GenerateId(string prefix) { - return $"{prefix}-{Guid.NewGuid():N}"[..16]; + return $"{prefix}-{_guidProvider.NewGuid():N}"[..16]; } private static string ComputeSha256(byte[] data) diff --git a/src/Policy/StellaOps.Policy.Engine/DependencyInjection/PolicyEngineServiceCollectionExtensions.cs b/src/Policy/StellaOps.Policy.Engine/DependencyInjection/PolicyEngineServiceCollectionExtensions.cs index 6f19558da..5427d243c 100644 --- a/src/Policy/StellaOps.Policy.Engine/DependencyInjection/PolicyEngineServiceCollectionExtensions.cs +++ b/src/Policy/StellaOps.Policy.Engine/DependencyInjection/PolicyEngineServiceCollectionExtensions.cs @@ -1,6 +1,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Http; +using StellaOps.Determinism; using StellaOps.Policy.Confidence.Configuration; using StellaOps.Policy.Confidence.Services; using StellaOps.Policy.Engine.Attestation; @@ -35,8 +36,8 @@ public static class PolicyEngineServiceCollectionExtensions /// public static IServiceCollection AddPolicyEngineCore(this IServiceCollection services) { - // Time provider - services.TryAddSingleton(TimeProvider.System); + // Determinism defaults (TimeProvider + IGuidProvider) + services.AddDeterminismDefaults(); // Core compilation and evaluation services services.TryAddSingleton(); diff --git a/src/Policy/StellaOps.Policy.Engine/Endpoints/PolicyLintEndpoints.cs b/src/Policy/StellaOps.Policy.Engine/Endpoints/PolicyLintEndpoints.cs index 8401092f0..08e36d5f8 100644 --- a/src/Policy/StellaOps.Policy.Engine/Endpoints/PolicyLintEndpoints.cs +++ b/src/Policy/StellaOps.Policy.Engine/Endpoints/PolicyLintEndpoints.cs @@ -26,7 +26,7 @@ public static class PolicyLintEndpoints group.MapGet("/rules", GetLintRulesAsync) .WithName("Policy.Lint.GetRules") .WithDescription("Get available lint rules and their severities") - .AllowAnonymous(); + .RequireAuthorization(policy => policy.RequireClaim("scope", "policy:read")); return routes; } diff --git a/src/Policy/StellaOps.Policy.Engine/Endpoints/PolicyPackEndpoints.cs b/src/Policy/StellaOps.Policy.Engine/Endpoints/PolicyPackEndpoints.cs index 4a786238b..2b0712e89 100644 --- a/src/Policy/StellaOps.Policy.Engine/Endpoints/PolicyPackEndpoints.cs +++ b/src/Policy/StellaOps.Policy.Engine/Endpoints/PolicyPackEndpoints.cs @@ -2,6 +2,7 @@ using System.Security.Claims; using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using StellaOps.Auth.Abstractions; +using StellaOps.Determinism; using StellaOps.Policy.Engine.Domain; using StellaOps.Policy.Engine.Services; @@ -59,6 +60,7 @@ internal static class PolicyPackEndpoints HttpContext context, [FromBody] CreatePolicyPackRequest request, IPolicyPackRepository repository, + IGuidProvider guidProvider, CancellationToken cancellationToken) { var scopeResult = ScopeAuthorization.RequireScope(context, StellaOpsScopes.PolicyEdit); @@ -78,7 +80,7 @@ internal static class PolicyPackEndpoints } var packId = string.IsNullOrWhiteSpace(request.PackId) - ? $"pack-{Guid.NewGuid():n}" + ? $"pack-{guidProvider.NewGuid():n}" : request.PackId.Trim(); var pack = await repository.CreateAsync(packId, request.DisplayName?.Trim(), cancellationToken).ConfigureAwait(false); @@ -157,6 +159,7 @@ internal static class PolicyPackEndpoints [FromBody] ActivatePolicyRevisionRequest request, IPolicyPackRepository repository, IPolicyActivationAuditor auditor, + TimeProvider timeProvider, CancellationToken cancellationToken) { var scopeResult = ScopeAuthorization.RequireScope(context, StellaOpsScopes.PolicyActivate); @@ -185,7 +188,7 @@ internal static class PolicyPackEndpoints packId, version, actorId, - DateTimeOffset.UtcNow, + timeProvider.GetUtcNow(), request.Comment, cancellationToken).ConfigureAwait(false); diff --git a/src/Policy/StellaOps.Policy.Engine/Endpoints/PolicySnapshotEndpoints.cs b/src/Policy/StellaOps.Policy.Engine/Endpoints/PolicySnapshotEndpoints.cs new file mode 100644 index 000000000..2c762518c --- /dev/null +++ b/src/Policy/StellaOps.Policy.Engine/Endpoints/PolicySnapshotEndpoints.cs @@ -0,0 +1,84 @@ +using Microsoft.AspNetCore.Mvc; +using StellaOps.Auth.Abstractions; +using StellaOps.Policy.Engine.Snapshots; + +namespace StellaOps.Policy.Engine.Endpoints; + +/// +/// API endpoints for snapshot CRUD under /api/policy. +/// +internal static class PolicySnapshotEndpoints +{ + public static IEndpointRouteBuilder MapPolicySnapshotsApi(this IEndpointRouteBuilder endpoints) + { + var group = endpoints.MapGroup("/api/policy/snapshots") + .RequireAuthorization() + .WithTags("Policy Snapshots"); + + group.MapPost(string.Empty, CreateAsync) + .WithName("PolicyEngine.Api.Snapshots.Create"); + + group.MapGet(string.Empty, ListAsync) + .WithName("PolicyEngine.Api.Snapshots.List"); + + group.MapGet("/{snapshotId}", GetAsync) + .WithName("PolicyEngine.Api.Snapshots.Get"); + + return endpoints; + } + + private static async Task CreateAsync( + HttpContext context, + [FromBody] SnapshotRequest request, + SnapshotService service, + CancellationToken cancellationToken) + { + var scopeResult = ScopeAuthorization.RequireScope(context, StellaOpsScopes.PolicyEdit); + if (scopeResult is not null) + { + return scopeResult; + } + + try + { + var snapshot = await service.CreateAsync(request, cancellationToken).ConfigureAwait(false); + return Results.Json(snapshot); + } + catch (ArgumentException ex) + { + return Results.BadRequest(new { message = ex.Message }); + } + } + + private static async Task ListAsync( + HttpContext context, + [FromQuery(Name = "tenant_id")] string? tenantId, + SnapshotService service, + CancellationToken cancellationToken) + { + var scopeResult = ScopeAuthorization.RequireScope(context, StellaOpsScopes.PolicyRead); + if (scopeResult is not null) + { + return scopeResult; + } + + var (items, cursor) = await service.ListAsync(tenantId, cancellationToken).ConfigureAwait(false); + return Results.Json(new { items, next_cursor = cursor }); + } + + private static async Task GetAsync( + HttpContext context, + [FromRoute] string snapshotId, + SnapshotService service, + CancellationToken cancellationToken) + { + var scopeResult = ScopeAuthorization.RequireScope(context, StellaOpsScopes.PolicyRead); + if (scopeResult is not null) + { + return scopeResult; + } + + var snapshot = await service.GetAsync(snapshotId, cancellationToken).ConfigureAwait(false); + return snapshot is null ? Results.NotFound() : Results.Json(snapshot); + } +} diff --git a/src/Policy/StellaOps.Policy.Engine/Endpoints/RiskProfileSchemaEndpoints.cs b/src/Policy/StellaOps.Policy.Engine/Endpoints/RiskProfileSchemaEndpoints.cs index f0e1f0f1b..63448f3cc 100644 --- a/src/Policy/StellaOps.Policy.Engine/Endpoints/RiskProfileSchemaEndpoints.cs +++ b/src/Policy/StellaOps.Policy.Engine/Endpoints/RiskProfileSchemaEndpoints.cs @@ -18,7 +18,7 @@ internal static class RiskProfileSchemaEndpoints .WithTags("Schema Discovery") .Produces(StatusCodes.Status200OK, contentType: JsonSchemaMediaType) .Produces(StatusCodes.Status304NotModified) - .AllowAnonymous(); + .RequireAuthorization(); endpoints.MapPost("/api/risk/schema/validate", ValidateProfile) .WithName("ValidateRiskProfile") diff --git a/src/Policy/StellaOps.Policy.Engine/Gates/DriftGateEvaluator.cs b/src/Policy/StellaOps.Policy.Engine/Gates/DriftGateEvaluator.cs index 6d2d2e07a..9bf8be452 100644 --- a/src/Policy/StellaOps.Policy.Engine/Gates/DriftGateEvaluator.cs +++ b/src/Policy/StellaOps.Policy.Engine/Gates/DriftGateEvaluator.cs @@ -5,8 +5,10 @@ // ----------------------------------------------------------------------------- using System.Collections.Immutable; +using System.Globalization; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using StellaOps.Determinism; namespace StellaOps.Policy.Engine.Gates; @@ -31,15 +33,18 @@ public sealed class DriftGateEvaluator : IDriftGateEvaluator { private readonly IOptionsMonitor _options; private readonly TimeProvider _timeProvider; + private readonly IGuidProvider _guidProvider; private readonly ILogger _logger; public DriftGateEvaluator( IOptionsMonitor options, TimeProvider timeProvider, + IGuidProvider guidProvider, ILogger logger) { _options = options ?? throw new ArgumentNullException(nameof(options)); _timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider)); + _guidProvider = guidProvider ?? throw new ArgumentNullException(nameof(guidProvider)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } @@ -52,7 +57,7 @@ public sealed class DriftGateEvaluator : IDriftGateEvaluator var now = _timeProvider.GetUtcNow(); var context = request.Context; - var decisionId = $"drift-gate:{now:yyyyMMddHHmmss}:{Guid.NewGuid():N}"; + var decisionId = $"drift-gate:{now:yyyyMMddHHmmss}:{_guidProvider.NewGuid():N}"; var gateResults = new List(); string? blockedBy = null; @@ -386,23 +391,23 @@ public sealed class DriftGateEvaluator : IDriftGateEvaluator if (remainder.StartsWith(">=")) { - return double.TryParse(remainder[2..].Trim(), out var threshold) && value >= threshold; + return double.TryParse(remainder[2..].Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out var threshold) && value >= threshold; } if (remainder.StartsWith("<=")) { - return double.TryParse(remainder[2..].Trim(), out var threshold) && value <= threshold; + return double.TryParse(remainder[2..].Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out var threshold) && value <= threshold; } if (remainder.StartsWith(">")) { - return double.TryParse(remainder[1..].Trim(), out var threshold) && value > threshold; + return double.TryParse(remainder[1..].Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out var threshold) && value > threshold; } if (remainder.StartsWith("<")) { - return double.TryParse(remainder[1..].Trim(), out var threshold) && value < threshold; + return double.TryParse(remainder[1..].Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out var threshold) && value < threshold; } if (remainder.StartsWith("=")) { - return double.TryParse(remainder[1..].Trim(), out var threshold) && Math.Abs(value - threshold) < 0.001; + return double.TryParse(remainder[1..].Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out var threshold) && Math.Abs(value - threshold) < 0.001; } return false; diff --git a/src/Policy/StellaOps.Policy.Engine/Program.cs b/src/Policy/StellaOps.Policy.Engine/Program.cs index ca2ca7340..b4fdd6906 100644 --- a/src/Policy/StellaOps.Policy.Engine/Program.cs +++ b/src/Policy/StellaOps.Policy.Engine/Program.cs @@ -126,7 +126,6 @@ builder.Services.AddOptions() builder.Services.AddSingleton(sp => sp.GetRequiredService>().Value); builder.Services.AddSingleton(sp => sp.GetRequiredService().ExceptionLifecycle); -builder.Services.AddSingleton(TimeProvider.System); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); @@ -135,19 +134,10 @@ builder.Services.AddSingleton(); // Verdict attestation services builder.Services.AddSingleton(); builder.Services.AddHttpClient(); -builder.Services.AddSingleton(sp => -{ - var options = new StellaOps.Policy.Engine.Attestation.VerdictAttestationOptions - { - Enabled = false, // Disabled by default, enable via config - FailOnError = false, - RekorEnabled = false, - AttestorUrl = "http://localhost:8080", - Timeout = TimeSpan.FromSeconds(30) - }; - // TODO: Bind from configuration section "VerdictAttestation" - return options; -}); +builder.Services.AddOptions() + .Bind(builder.Configuration.GetSection("VerdictAttestation")) + .ValidateOnStart(); +builder.Services.AddSingleton(sp => sp.GetRequiredService>().Value); builder.Services.AddSingleton(); builder.Services.AddSingleton(); @@ -368,8 +358,7 @@ app.MapProfileEvents(); app.MapCvssReceipts(); // CVSS v4 receipt CRUD & history // Phase 5: Multi-tenant PostgreSQL-backed API endpoints -// TODO: Fix missing MapPolicySnapshotsApi method -// app.MapPolicySnapshotsApi(); +app.MapPolicySnapshotsApi(); app.MapViolationEventsApi(); app.MapConflictsApi(); diff --git a/src/Policy/StellaOps.Policy.Engine/Scoring/EvidenceWeightedScore/EvidenceWeightedScoreEnricher.cs b/src/Policy/StellaOps.Policy.Engine/Scoring/EvidenceWeightedScore/EvidenceWeightedScoreEnricher.cs index 27f4abe4a..5ae322502 100644 --- a/src/Policy/StellaOps.Policy.Engine/Scoring/EvidenceWeightedScore/EvidenceWeightedScoreEnricher.cs +++ b/src/Policy/StellaOps.Policy.Engine/Scoring/EvidenceWeightedScore/EvidenceWeightedScoreEnricher.cs @@ -50,11 +50,11 @@ public sealed class EvidenceWeightedScoreEnricher : IFindingScoreEnricher { // For now, the implementation is synchronous - async is for future when // we might need to fetch additional evidence asynchronously - return ValueTask.FromResult(Enrich(evidence)); + return ValueTask.FromResult(Enrich(evidence, cancellationToken)); } /// - public ScoreEnrichmentResult Enrich(FindingEvidence evidence) + public ScoreEnrichmentResult Enrich(FindingEvidence evidence, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(evidence); @@ -90,7 +90,7 @@ public sealed class EvidenceWeightedScoreEnricher : IFindingScoreEnricher var input = _aggregator.Aggregate(evidence); // Get policy (use configured digest or default) - var policy = GetPolicy(options); + var policy = GetPolicy(options, cancellationToken); // Calculate score var score = _calculator.Calculate(input, policy); @@ -142,12 +142,12 @@ public sealed class EvidenceWeightedScoreEnricher : IFindingScoreEnricher } } - private EvidenceWeightPolicy GetPolicy(PolicyEvidenceWeightedScoreOptions options) + private EvidenceWeightPolicy GetPolicy(PolicyEvidenceWeightedScoreOptions options, CancellationToken cancellationToken) { // Get default policy synchronously (blocking call) - use cached policy in production // The async API is available but for the sync Enrich method we need sync access var defaultPolicy = _policyProvider - .GetDefaultPolicyAsync("default", CancellationToken.None) + .GetDefaultPolicyAsync("default", cancellationToken) .GetAwaiter() .GetResult(); diff --git a/src/Policy/StellaOps.Policy.Engine/Scoring/EvidenceWeightedScore/IFindingScoreEnricher.cs b/src/Policy/StellaOps.Policy.Engine/Scoring/EvidenceWeightedScore/IFindingScoreEnricher.cs index 3356fb90b..56d65ce89 100644 --- a/src/Policy/StellaOps.Policy.Engine/Scoring/EvidenceWeightedScore/IFindingScoreEnricher.cs +++ b/src/Policy/StellaOps.Policy.Engine/Scoring/EvidenceWeightedScore/IFindingScoreEnricher.cs @@ -97,8 +97,11 @@ public interface IFindingScoreEnricher /// Enriches a finding synchronously (for pipeline integration). /// /// Evidence collected for the finding. + /// Cancellation token. /// Score enrichment result. - ScoreEnrichmentResult Enrich(FindingEvidence evidence); + ScoreEnrichmentResult Enrich( + FindingEvidence evidence, + CancellationToken cancellationToken = default); /// /// Enriches multiple findings in batch. @@ -172,7 +175,7 @@ public sealed class NullFindingScoreEnricher : IFindingScoreEnricher } /// - public ScoreEnrichmentResult Enrich(FindingEvidence evidence) + public ScoreEnrichmentResult Enrich(FindingEvidence evidence, CancellationToken cancellationToken = default) { return ScoreEnrichmentResult.Skipped(evidence.FindingId); } diff --git a/src/Policy/StellaOps.Policy.Engine/Scoring/EvidenceWeightedScore/MigrationTelemetryService.cs b/src/Policy/StellaOps.Policy.Engine/Scoring/EvidenceWeightedScore/MigrationTelemetryService.cs index c5e08895e..0cf03abab 100644 --- a/src/Policy/StellaOps.Policy.Engine/Scoring/EvidenceWeightedScore/MigrationTelemetryService.cs +++ b/src/Policy/StellaOps.Policy.Engine/Scoring/EvidenceWeightedScore/MigrationTelemetryService.cs @@ -79,7 +79,7 @@ public sealed record MigrationTelemetryStats /// /// Timestamp when stats were captured. /// - public DateTimeOffset CapturedAt { get; init; } = DateTimeOffset.UtcNow; + public DateTimeOffset CapturedAt { get; init; } } /// @@ -164,6 +164,7 @@ public sealed class MigrationTelemetryService : IMigrationTelemetryService { private readonly IOptionsMonitor _options; private readonly ILogger _logger; + private readonly TimeProvider _timeProvider; // Counters private long _totalVerdicts; @@ -193,10 +194,12 @@ public sealed class MigrationTelemetryService : IMigrationTelemetryService public MigrationTelemetryService( IOptionsMonitor options, ILogger logger, + TimeProvider? timeProvider = null, IMeterFactory? meterFactory = null) { _options = options ?? throw new ArgumentNullException(nameof(options)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _timeProvider = timeProvider ?? TimeProvider.System; var meter = meterFactory?.Create("StellaOps.Policy.Migration") ?? new Meter("StellaOps.Policy.Migration"); @@ -311,7 +314,7 @@ public sealed class MigrationTelemetryService : IMigrationTelemetryService scoreDifference: scoreDiff, isAligned: isAligned, tierBucketMatch: tierMatch, - timestamp: DateTimeOffset.UtcNow + timestamp: _timeProvider.GetUtcNow() ); _recentSamples.Enqueue(sample); @@ -360,7 +363,7 @@ public sealed class MigrationTelemetryService : IMigrationTelemetryService ScoreDifferenceDistribution = new Dictionary(_scoreDiffDistribution), ByConfidenceTier = new Dictionary(_byConfidenceTier), ByEwsBucket = new Dictionary(_byEwsBucket), - CapturedAt = DateTimeOffset.UtcNow + CapturedAt = _timeProvider.GetUtcNow() }; } @@ -452,7 +455,7 @@ public static class MigrationTelemetryExtensions lines.Add($" {bucket}: {count:N0}"); } - return string.Join(Environment.NewLine, lines); + return string.Join("\n", lines); } /// diff --git a/src/Policy/StellaOps.Policy.Engine/Services/InMemoryPolicyPackRepository.cs b/src/Policy/StellaOps.Policy.Engine/Services/InMemoryPolicyPackRepository.cs index 5cc5c4bed..988ac4234 100644 --- a/src/Policy/StellaOps.Policy.Engine/Services/InMemoryPolicyPackRepository.cs +++ b/src/Policy/StellaOps.Policy.Engine/Services/InMemoryPolicyPackRepository.cs @@ -8,9 +8,9 @@ internal sealed class InMemoryPolicyPackRepository : IPolicyPackRepository private readonly ConcurrentDictionary packs = new(StringComparer.OrdinalIgnoreCase); private readonly TimeProvider _timeProvider; - public InMemoryPolicyPackRepository(TimeProvider? timeProvider = null) + public InMemoryPolicyPackRepository(TimeProvider timeProvider) { - _timeProvider = timeProvider ?? TimeProvider.System; + _timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider)); } public Task CreateAsync(string packId, string? displayName, CancellationToken cancellationToken) diff --git a/src/Policy/StellaOps.Policy.Engine/Simulation/RiskSimulationService.cs b/src/Policy/StellaOps.Policy.Engine/Simulation/RiskSimulationService.cs index 6bdf9f389..5bbe3aa5b 100644 --- a/src/Policy/StellaOps.Policy.Engine/Simulation/RiskSimulationService.cs +++ b/src/Policy/StellaOps.Policy.Engine/Simulation/RiskSimulationService.cs @@ -1,8 +1,10 @@ using System.Diagnostics; +using System.Globalization; using System.Text; using System.Text.Json; using Microsoft.Extensions.Logging; using StellaOps.Cryptography; +using StellaOps.Determinism; using StellaOps.Policy.Engine.Services; using StellaOps.Policy.Engine.Telemetry; using StellaOps.Policy.RiskProfile.Hashing; @@ -21,6 +23,7 @@ public sealed class RiskSimulationService private readonly RiskProfileConfigurationService _profileService; private readonly RiskProfileHasher _hasher; private readonly ICryptoHash _cryptoHash; + private readonly IGuidProvider _guidProvider; private readonly RiskSimulationBreakdownService? _breakdownService; private static readonly double[] PercentileLevels = { 0.25, 0.50, 0.75, 0.90, 0.95, 0.99 }; @@ -32,12 +35,14 @@ public sealed class RiskSimulationService TimeProvider timeProvider, RiskProfileConfigurationService profileService, ICryptoHash cryptoHash, + IGuidProvider guidProvider, RiskSimulationBreakdownService? breakdownService = null) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider)); _profileService = profileService ?? throw new ArgumentNullException(nameof(profileService)); _cryptoHash = cryptoHash ?? throw new ArgumentNullException(nameof(cryptoHash)); + _guidProvider = guidProvider ?? throw new ArgumentNullException(nameof(guidProvider)); _hasher = new RiskProfileHasher(cryptoHash); _breakdownService = breakdownService; } @@ -226,7 +231,7 @@ public sealed class RiskSimulationService long l => l, decimal dec => (double)dec, JsonElement je when je.TryGetDouble(out var d) => d, - string s when double.TryParse(s, out var d) => d, + string s when double.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out var d) => d, _ => 0.0 }, RiskSignalType.Categorical => value switch @@ -461,7 +466,7 @@ public sealed class RiskSimulationService private string GenerateSimulationId(RiskSimulationRequest request, string profileHash) { - var seed = $"{request.ProfileId}|{profileHash}|{request.Findings.Count}|{Guid.NewGuid()}"; + var seed = $"{request.ProfileId}|{profileHash}|{request.Findings.Count}|{_guidProvider.NewGuid()}"; var hash = _cryptoHash.ComputeHashHexForPurpose(Encoding.UTF8.GetBytes(seed), HashPurpose.Content); return $"rsim-{hash[..16]}"; } diff --git a/src/Policy/StellaOps.Policy.Engine/TASKS.md b/src/Policy/StellaOps.Policy.Engine/TASKS.md index 5770ec2b7..46233de88 100644 --- a/src/Policy/StellaOps.Policy.Engine/TASKS.md +++ b/src/Policy/StellaOps.Policy.Engine/TASKS.md @@ -7,4 +7,5 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229 | --- | --- | --- | | AUDIT-0440-M | DONE | Revalidated 2026-01-07; maintainability audit for StellaOps.Policy.Engine. | | AUDIT-0440-T | DONE | Revalidated 2026-01-07; test coverage audit for StellaOps.Policy.Engine. | -| AUDIT-0440-A | TODO | Revalidated 2026-01-07 (open findings). | +| AUDIT-0440-A | DOING | Revalidated 2026-01-07 (open findings). | +| AUDIT-HOTLIST-POLICY-ENGINE-0001 | DOING | Apply approved hotlist fixes and tests from audit tracker. | diff --git a/src/Policy/StellaOps.Policy.Engine/Telemetry/RuleHitTrace.cs b/src/Policy/StellaOps.Policy.Engine/Telemetry/RuleHitTrace.cs index d78346a1e..2295c9e0f 100644 --- a/src/Policy/StellaOps.Policy.Engine/Telemetry/RuleHitTrace.cs +++ b/src/Policy/StellaOps.Policy.Engine/Telemetry/RuleHitTrace.cs @@ -1,9 +1,9 @@ using System.Collections.Immutable; using System.Diagnostics; -using System.Security.Cryptography; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; +using StellaOps.Determinism; namespace StellaOps.Policy.Engine.Telemetry; @@ -161,7 +161,7 @@ public sealed record RuleHitTrace /// /// Creates a trace ID from the current activity or generates a new one. /// - public static string GetOrCreateTraceId() + public static string GetOrCreateTraceId(IGuidProvider? guidProvider = null) { var activity = Activity.Current; if (activity is not null) @@ -169,15 +169,14 @@ public sealed record RuleHitTrace return activity.TraceId.ToString(); } - Span bytes = stackalloc byte[16]; - RandomNumberGenerator.Fill(bytes); - return Convert.ToHexStringLower(bytes); + var provider = guidProvider ?? SystemGuidProvider.Instance; + return provider.NewGuid().ToString("N"); } /// /// Creates a span ID from the current activity or generates a new one. /// - public static string GetOrCreateSpanId() + public static string GetOrCreateSpanId(IGuidProvider? guidProvider = null) { var activity = Activity.Current; if (activity is not null) @@ -185,9 +184,9 @@ public sealed record RuleHitTrace return activity.SpanId.ToString(); } - Span bytes = stackalloc byte[8]; - RandomNumberGenerator.Fill(bytes); - return Convert.ToHexStringLower(bytes); + var provider = guidProvider ?? SystemGuidProvider.Instance; + var bytes = provider.NewGuid().ToByteArray(); + return Convert.ToHexStringLower(bytes.AsSpan(0, 8)); } } @@ -304,11 +303,12 @@ public static class RuleHitTraceFactory bool expressionResult = false, long evaluationMicroseconds = 0, bool isSampled = false, - ImmutableDictionary? attributes = null) + ImmutableDictionary? attributes = null, + IGuidProvider? guidProvider = null) { var time = timeProvider ?? TimeProvider.System; - var traceId = RuleHitTrace.GetOrCreateTraceId(); - var spanId = RuleHitTrace.GetOrCreateSpanId(); + var traceId = RuleHitTrace.GetOrCreateTraceId(guidProvider); + var spanId = RuleHitTrace.GetOrCreateSpanId(guidProvider); var parentSpanId = Activity.Current?.ParentSpanId.ToString(); return new RuleHitTrace diff --git a/src/Policy/StellaOps.Policy.Engine/Telemetry/RuleHitTraceCollector.cs b/src/Policy/StellaOps.Policy.Engine/Telemetry/RuleHitTraceCollector.cs index 1cf531a7c..e67b69e8a 100644 --- a/src/Policy/StellaOps.Policy.Engine/Telemetry/RuleHitTraceCollector.cs +++ b/src/Policy/StellaOps.Policy.Engine/Telemetry/RuleHitTraceCollector.cs @@ -1,6 +1,9 @@ using System.Collections.Concurrent; using System.Collections.Immutable; +using System.Buffers.Binary; using System.Diagnostics; +using System.Security.Cryptography; +using System.Text; namespace StellaOps.Policy.Engine.Telemetry; @@ -84,7 +87,7 @@ public interface IRuleHitTraceCollector /// /// Records a rule hit trace. /// - void Record(RuleHitTrace trace); + void Record(RuleHitTrace trace, CancellationToken cancellationToken = default); /// /// Gets all traces for a run. @@ -131,8 +134,6 @@ public sealed class RuleHitTraceCollector : IRuleHitTraceCollector, IDisposable private readonly TimeProvider _timeProvider; private readonly IReadOnlyList _exporters; private readonly ConcurrentDictionary _runBuffers = new(); - private readonly Random _sampler; - private readonly object _samplerLock = new(); private volatile bool _incidentMode; private bool _disposed; @@ -144,7 +145,6 @@ public sealed class RuleHitTraceCollector : IRuleHitTraceCollector, IDisposable _options = options ?? RuleHitSamplingOptions.Default; _timeProvider = timeProvider ?? TimeProvider.System; _exporters = exporters?.ToList() ?? new List(); - _sampler = new Random(); } /// @@ -159,7 +159,7 @@ public sealed class RuleHitTraceCollector : IRuleHitTraceCollector, IDisposable /// /// Records a rule hit trace with sampling. /// - public void Record(RuleHitTrace trace) + public void Record(RuleHitTrace trace, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(trace); @@ -181,7 +181,7 @@ public sealed class RuleHitTraceCollector : IRuleHitTraceCollector, IDisposable if (buffer.Count >= _options.MaxBufferSizePerRun) { // Async flush without blocking - _ = FlushAsync(trace.RunId, CancellationToken.None); + _ = FlushAsync(trace.RunId, cancellationToken); } } @@ -316,26 +316,26 @@ public sealed class RuleHitTraceCollector : IRuleHitTraceCollector, IDisposable // VEX overrides get elevated sampling if (trace.IsVexOverride) { - return Sample(_options.VexOverrideSamplingRate); + return Sample(_options.VexOverrideSamplingRate, trace); } // High-severity outcomes get elevated sampling if (_options.HighSeverityOutcomes.Contains(trace.Outcome)) { - return Sample(_options.HighSeveritySamplingRate); + return Sample(_options.HighSeveritySamplingRate, trace); } if (!string.IsNullOrWhiteSpace(trace.AssignedSeverity) && _options.HighSeverityOutcomes.Contains(trace.AssignedSeverity)) { - return Sample(_options.HighSeveritySamplingRate); + return Sample(_options.HighSeveritySamplingRate, trace); } // Base sampling rate - return Sample(_options.BaseSamplingRate); + return Sample(_options.BaseSamplingRate, trace); } - private bool Sample(double rate) + private static bool Sample(double rate, RuleHitTrace trace) { if (rate >= 1.0) { @@ -347,10 +347,29 @@ public sealed class RuleHitTraceCollector : IRuleHitTraceCollector, IDisposable return false; } - lock (_samplerLock) + var hashValue = ComputeSampleHash(trace); + var sample = hashValue / (double)ulong.MaxValue; + return sample < rate; + } + + private static ulong ComputeSampleHash(RuleHitTrace trace) + { + var key = string.Join("|", new[] { - return _sampler.NextDouble() < rate; - } + trace.RunId, + trace.PolicyId, + trace.RuleName, + trace.Outcome, + trace.AssignedSeverity ?? string.Empty, + trace.VulnerabilityId ?? string.Empty, + trace.ComponentPurl ?? string.Empty, + trace.VexStatus ?? string.Empty, + trace.VexVendor ?? string.Empty, + trace.IsVexOverride ? "1" : "0" + }); + + var hash = SHA256.HashData(Encoding.UTF8.GetBytes(key)); + return BinaryPrimitives.ReadUInt64BigEndian(hash.AsSpan(0, 8)); } private static void RecordMetrics(RuleHitTrace trace) diff --git a/src/Policy/StellaOps.Policy.Engine/Tenancy/TenantContextMiddleware.cs b/src/Policy/StellaOps.Policy.Engine/Tenancy/TenantContextMiddleware.cs index a542a39bc..6d1347340 100644 --- a/src/Policy/StellaOps.Policy.Engine/Tenancy/TenantContextMiddleware.cs +++ b/src/Policy/StellaOps.Policy.Engine/Tenancy/TenantContextMiddleware.cs @@ -14,6 +14,7 @@ public sealed partial class TenantContextMiddleware private readonly RequestDelegate _next; private readonly TenantContextOptions _options; private readonly ILogger _logger; + private readonly TimeProvider _timeProvider; // Valid tenant/project ID pattern: alphanumeric, dashes, underscores [GeneratedRegex("^[a-zA-Z0-9_-]+$", RegexOptions.Compiled)] @@ -28,11 +29,13 @@ public sealed partial class TenantContextMiddleware public TenantContextMiddleware( RequestDelegate next, IOptions options, - ILogger logger) + ILogger logger, + TimeProvider timeProvider) { _next = next ?? throw new ArgumentNullException(nameof(next)); _options = options?.Value ?? new TenantContextOptions(); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider)); } public async Task InvokeAsync(HttpContext context, ITenantContextAccessor tenantContextAccessor) @@ -138,7 +141,8 @@ public sealed partial class TenantContextMiddleware tenantHeader, string.IsNullOrWhiteSpace(projectHeader) ? null : projectHeader, canWrite, - actorId); + actorId, + _timeProvider); _logger.LogDebug( "Tenant context established: tenant={TenantId}, project={ProjectId}, canWrite={CanWrite}, actor={ActorId}", diff --git a/src/Policy/StellaOps.Policy.Engine/Tenancy/TenantContextModels.cs b/src/Policy/StellaOps.Policy.Engine/Tenancy/TenantContextModels.cs index d59a1ef83..41a3e846d 100644 --- a/src/Policy/StellaOps.Policy.Engine/Tenancy/TenantContextModels.cs +++ b/src/Policy/StellaOps.Policy.Engine/Tenancy/TenantContextModels.cs @@ -80,22 +80,29 @@ public sealed record TenantContext /// /// Timestamp when the context was created. /// - public DateTimeOffset CreatedAt { get; init; } = DateTimeOffset.UtcNow; + public DateTimeOffset CreatedAt { get; init; } /// /// Creates a tenant context for a specific tenant. /// - public static TenantContext ForTenant(string tenantId, string? projectId = null, bool canWrite = false, string? actorId = null) + public static TenantContext ForTenant( + string tenantId, + string? projectId = null, + bool canWrite = false, + string? actorId = null, + TimeProvider? timeProvider = null) { ArgumentException.ThrowIfNullOrWhiteSpace(tenantId); + var clock = timeProvider ?? TimeProvider.System; + return new TenantContext { TenantId = tenantId, ProjectId = projectId, CanWrite = canWrite, ActorId = actorId, - CreatedAt = DateTimeOffset.UtcNow + CreatedAt = clock.GetUtcNow() }; } } diff --git a/src/Policy/StellaOps.Policy.Engine/Vex/VexDecisionEmitter.cs b/src/Policy/StellaOps.Policy.Engine/Vex/VexDecisionEmitter.cs index 907defac5..77c1d2a4c 100644 --- a/src/Policy/StellaOps.Policy.Engine/Vex/VexDecisionEmitter.cs +++ b/src/Policy/StellaOps.Policy.Engine/Vex/VexDecisionEmitter.cs @@ -2,6 +2,7 @@ using System.Collections.Immutable; using System.Diagnostics; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using StellaOps.Determinism; using StellaOps.Policy.Engine.Gates; using StellaOps.Policy.Engine.ReachabilityFacts; using StellaOps.Policy.Engine.Services; @@ -51,6 +52,7 @@ public sealed class VexDecisionEmitter : IVexDecisionEmitter private readonly IPolicyGateEvaluator _gateEvaluator; private readonly IOptionsMonitor _options; private readonly TimeProvider _timeProvider; + private readonly IGuidProvider _guidProvider; private readonly ILogger _logger; // LIN-BE-012: Optional verdict link service for SBOM linking private readonly IVerdictLinkService? _verdictLinkService; @@ -76,6 +78,7 @@ public sealed class VexDecisionEmitter : IVexDecisionEmitter IPolicyGateEvaluator gateEvaluator, IOptionsMonitor options, TimeProvider timeProvider, + IGuidProvider guidProvider, ILogger logger, IVerdictLinkService? verdictLinkService = null) { @@ -83,6 +86,7 @@ public sealed class VexDecisionEmitter : IVexDecisionEmitter _gateEvaluator = gateEvaluator ?? throw new ArgumentNullException(nameof(gateEvaluator)); _options = options ?? throw new ArgumentNullException(nameof(options)); _timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider)); + _guidProvider = guidProvider ?? throw new ArgumentNullException(nameof(guidProvider)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _verdictLinkService = verdictLinkService; } @@ -185,7 +189,7 @@ public sealed class VexDecisionEmitter : IVexDecisionEmitter } // Build document - var documentId = $"urn:uuid:{Guid.NewGuid()}"; + var documentId = $"urn:uuid:{_guidProvider.NewGuid()}"; var document = new VexDecisionDocument { Id = documentId, diff --git a/src/Policy/StellaOps.Policy.Engine/Vex/VexDecisionSigningService.cs b/src/Policy/StellaOps.Policy.Engine/Vex/VexDecisionSigningService.cs index 5553ca48f..a15cc232d 100644 --- a/src/Policy/StellaOps.Policy.Engine/Vex/VexDecisionSigningService.cs +++ b/src/Policy/StellaOps.Policy.Engine/Vex/VexDecisionSigningService.cs @@ -454,8 +454,7 @@ public sealed class VexDecisionSigningService : IVexDecisionSigningService continue; } - // TODO: Verify actual signature if signer client provides public key resolution - // For now, we just verify the signature is well-formed base64 + // Signature verification is limited to base64 validation when key resolution is unavailable. try { _ = Convert.FromBase64String(sig.Sig); diff --git a/src/Policy/StellaOps.Policy.Engine/WhatIfSimulation/WhatIfSimulationService.cs b/src/Policy/StellaOps.Policy.Engine/WhatIfSimulation/WhatIfSimulationService.cs index f1bcdcf46..1fce1a64a 100644 --- a/src/Policy/StellaOps.Policy.Engine/WhatIfSimulation/WhatIfSimulationService.cs +++ b/src/Policy/StellaOps.Policy.Engine/WhatIfSimulation/WhatIfSimulationService.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.Security.Cryptography; using System.Text; using Microsoft.Extensions.Logging; +using StellaOps.Determinism; using StellaOps.Policy.Engine.Domain; using StellaOps.Policy.Engine.EffectiveDecisionMap; using StellaOps.Policy.Engine.Services; @@ -21,19 +22,22 @@ internal sealed class WhatIfSimulationService private readonly PolicyCompilationService _compilationService; private readonly ILogger _logger; private readonly TimeProvider _timeProvider; + private readonly IGuidProvider _guidProvider; public WhatIfSimulationService( IEffectiveDecisionMap decisionMap, IPolicyPackRepository policyRepository, PolicyCompilationService compilationService, ILogger logger, - TimeProvider timeProvider) + TimeProvider timeProvider, + IGuidProvider guidProvider) { _decisionMap = decisionMap ?? throw new ArgumentNullException(nameof(decisionMap)); _policyRepository = policyRepository ?? throw new ArgumentNullException(nameof(policyRepository)); _compilationService = compilationService ?? throw new ArgumentNullException(nameof(compilationService)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider)); + _guidProvider = guidProvider ?? throw new ArgumentNullException(nameof(guidProvider)); } /// @@ -533,9 +537,9 @@ internal sealed class WhatIfSimulationService }; } - private static string GenerateSimulationId(WhatIfSimulationRequest request) + private string GenerateSimulationId(WhatIfSimulationRequest request) { - var seed = $"{request.TenantId}|{request.BaseSnapshotId}|{request.DraftPolicy?.PackId}|{Guid.NewGuid()}"; + var seed = $"{request.TenantId}|{request.BaseSnapshotId}|{request.DraftPolicy?.PackId}|{_guidProvider.NewGuid()}"; var hash = SHA256.HashData(Encoding.UTF8.GetBytes(seed)); return $"whatif-{Convert.ToHexStringLower(hash)[..16]}"; } diff --git a/src/Policy/__Tests/StellaOps.Policy.Engine.Tests/AirGap/RiskProfileAirGapExportServiceTests.cs b/src/Policy/__Tests/StellaOps.Policy.Engine.Tests/AirGap/RiskProfileAirGapExportServiceTests.cs index 8750888d7..cc512fff3 100644 --- a/src/Policy/__Tests/StellaOps.Policy.Engine.Tests/AirGap/RiskProfileAirGapExportServiceTests.cs +++ b/src/Policy/__Tests/StellaOps.Policy.Engine.Tests/AirGap/RiskProfileAirGapExportServiceTests.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.Logging.Abstractions; using StellaOps.Cryptography; +using StellaOps.Determinism; using StellaOps.Policy.Engine.AirGap; using StellaOps.Policy.RiskProfile.Models; using Xunit; @@ -10,6 +11,7 @@ public sealed class RiskProfileAirGapExportServiceTests { private readonly FakeCryptoHash _cryptoHash = new(); private readonly FakeTimeProvider _timeProvider = new(); + private readonly IGuidProvider _guidProvider = new SequentialGuidProvider(); private readonly NullLogger _logger = new(); private RiskProfileAirGapExportService CreateService(ISealedModeService? sealedMode = null) @@ -17,6 +19,7 @@ public sealed class RiskProfileAirGapExportServiceTests return new RiskProfileAirGapExportService( _cryptoHash, _timeProvider, + _guidProvider, _logger, sealedMode); } diff --git a/src/Policy/__Tests/StellaOps.Policy.Engine.Tests/Integration/PolicyEngineApiHostTests.cs b/src/Policy/__Tests/StellaOps.Policy.Engine.Tests/Integration/PolicyEngineApiHostTests.cs new file mode 100644 index 000000000..8cfd61c3c --- /dev/null +++ b/src/Policy/__Tests/StellaOps.Policy.Engine.Tests/Integration/PolicyEngineApiHostTests.cs @@ -0,0 +1,156 @@ +using System.Net; +using System.Security.Claims; +using System.Text.Encodings.Web; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using StellaOps.Policy.Engine.Attestation; +using StellaOps.TestKit.Fixtures; +using Xunit; + +namespace StellaOps.Policy.Engine.Tests.Integration; + +public sealed class PolicyEngineApiHostTests : IClassFixture +{ + private readonly PolicyEngineWebServiceFixture _factory; + + public PolicyEngineApiHostTests(PolicyEngineWebServiceFixture factory) + { + _factory = factory; + } + + [Fact] + public async Task Healthz_ReturnsOk() + { + using var client = _factory.CreateClient(); + + var response = await client.GetAsync("/healthz"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + [Fact] + public async Task PolicyLintRules_RequireAuth() + { + using var client = _factory.CreateClient(); + + var response = await client.GetAsync("/api/v1/policy/lint/rules"); + + Assert.True(response.StatusCode is HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden); + } + + [Fact] + public async Task PolicyLintRules_WithAuth_ReturnsOk() + { + using var client = _factory.CreateClient(); + client.DefaultRequestHeaders.Add(TestAuthHandler.HeaderName, TestAuthHandler.HeaderValue); + + var response = await client.GetAsync("/api/v1/policy/lint/rules"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + [Fact] + public async Task PolicySnapshotsApi_RequiresAuth() + { + using var client = _factory.CreateClient(); + + var response = await client.GetAsync("/api/policy/snapshots"); + + Assert.True(response.StatusCode is HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden); + } + + [Fact] + public void VerdictAttestationOptions_BindFromConfiguration() + { + var options = _factory.Services.GetRequiredService(); + + Assert.True(options.Enabled); + Assert.True(options.FailOnError); + Assert.True(options.RekorEnabled); + Assert.Equal("http://attestor.test", options.AttestorUrl); + Assert.Equal(TimeSpan.FromSeconds(15), options.Timeout); + } +} + +public sealed class PolicyEngineWebServiceFixture : WebServiceFixture +{ + public PolicyEngineWebServiceFixture() + : base(ConfigureServices, ConfigureWebHost) + { + } + + private static void ConfigureServices(IServiceCollection services) + { + services.RemoveAll(); + + services.AddAuthentication(options => + { + options.DefaultAuthenticateScheme = TestAuthHandler.SchemeName; + options.DefaultChallengeScheme = TestAuthHandler.SchemeName; + }) + .AddScheme( + TestAuthHandler.SchemeName, + _ => { }); + } + + private static void ConfigureWebHost(IWebHostBuilder builder) + { + builder.ConfigureAppConfiguration((_, config) => + { + var settings = new Dictionary + { + ["VerdictAttestation:Enabled"] = "true", + ["VerdictAttestation:FailOnError"] = "true", + ["VerdictAttestation:RekorEnabled"] = "true", + ["VerdictAttestation:AttestorUrl"] = "http://attestor.test", + ["VerdictAttestation:Timeout"] = "00:00:15" + }; + + config.AddInMemoryCollection(settings); + }); + } +} + +internal sealed class TestAuthHandler : AuthenticationHandler +{ + public const string SchemeName = "Test"; + public const string HeaderName = "X-Test-Auth"; + public const string HeaderValue = "true"; + + public TestAuthHandler( + IOptionsMonitor options, + ILoggerFactory logger, + UrlEncoder encoder, + ISystemClock clock) + : base(options, logger, encoder, clock) + { + } + + protected override Task HandleAuthenticateAsync() + { + if (!Request.Headers.TryGetValue(HeaderName, out var value) || + !string.Equals(value, HeaderValue, StringComparison.Ordinal)) + { + return Task.FromResult(AuthenticateResult.Fail("Missing test auth header.")); + } + + var claims = new[] + { + new Claim("scope", "policy:read"), + new Claim("tenant_id", "test-tenant"), + new Claim(ClaimTypes.NameIdentifier, "test-user") + }; + + var identity = new ClaimsIdentity(claims, SchemeName); + var principal = new ClaimsPrincipal(identity); + var ticket = new AuthenticationTicket(principal, SchemeName); + + return Task.FromResult(AuthenticateResult.Success(ticket)); + } +} diff --git a/src/Policy/__Tests/StellaOps.Policy.Engine.Tests/PolicyBundleServiceTests.cs b/src/Policy/__Tests/StellaOps.Policy.Engine.Tests/PolicyBundleServiceTests.cs index 0e2322bbc..ea83ca9a9 100644 --- a/src/Policy/__Tests/StellaOps.Policy.Engine.Tests/PolicyBundleServiceTests.cs +++ b/src/Policy/__Tests/StellaOps.Policy.Engine.Tests/PolicyBundleServiceTests.cs @@ -137,7 +137,7 @@ public sealed class PolicyBundleServiceTests var options = Microsoft.Extensions.Options.Options.Create(new PolicyEngineOptions()); var metadataExtractor = new PolicyMetadataExtractor(); var compilationService = new PolicyCompilationService(compiler, complexity, metadataExtractor, new StaticOptionsMonitor(options.Value), TimeProvider.System); - var repo = new InMemoryPolicyPackRepository(); + var repo = new InMemoryPolicyPackRepository(TimeProvider.System); return new ServiceHarness( new PolicyBundleService(compilationService, repo, TimeProvider.System), repo); diff --git a/src/Policy/__Tests/StellaOps.Policy.Engine.Tests/PolicyRuntimeEvaluationServiceTests.cs b/src/Policy/__Tests/StellaOps.Policy.Engine.Tests/PolicyRuntimeEvaluationServiceTests.cs index d5f8b5ece..964b423c4 100644 --- a/src/Policy/__Tests/StellaOps.Policy.Engine.Tests/PolicyRuntimeEvaluationServiceTests.cs +++ b/src/Policy/__Tests/StellaOps.Policy.Engine.Tests/PolicyRuntimeEvaluationServiceTests.cs @@ -436,7 +436,7 @@ public sealed class PolicyRuntimeEvaluationServiceTests private static TestHarness CreateHarness() { - var repository = new InMemoryPolicyPackRepository(); + var repository = new InMemoryPolicyPackRepository(TimeProvider.System); var cacheLogger = NullLogger.Instance; var serviceLogger = NullLogger.Instance; var options = Microsoft.Extensions.Options.Options.Create(new PolicyEngineOptions()); diff --git a/src/Policy/__Tests/StellaOps.Policy.Engine.Tests/PolicyRuntimeEvaluatorTests.cs b/src/Policy/__Tests/StellaOps.Policy.Engine.Tests/PolicyRuntimeEvaluatorTests.cs index 98101bc96..209714aa3 100644 --- a/src/Policy/__Tests/StellaOps.Policy.Engine.Tests/PolicyRuntimeEvaluatorTests.cs +++ b/src/Policy/__Tests/StellaOps.Policy.Engine.Tests/PolicyRuntimeEvaluatorTests.cs @@ -12,7 +12,7 @@ public sealed class PolicyRuntimeEvaluatorTests [Fact] public async Task EvaluateAsync_ReturnsDeterministicDecisionAndCaches() { - var repo = new InMemoryPolicyPackRepository(); + var repo = new InMemoryPolicyPackRepository(TimeProvider.System); await repo.StoreBundleAsync( "pack-1", 1, @@ -41,7 +41,7 @@ public sealed class PolicyRuntimeEvaluatorTests [Fact] public async Task EvaluateAsync_ThrowsWhenBundleMissing() { - var evaluator = new PolicyRuntimeEvaluator(new InMemoryPolicyPackRepository()); + var evaluator = new PolicyRuntimeEvaluator(new InMemoryPolicyPackRepository(TimeProvider.System)); var request = new PolicyEvaluationRequest("pack-x", 1, "subject-a"); await Assert.ThrowsAsync(() => evaluator.EvaluateAsync(request, TestContext.Current.CancellationToken)); diff --git a/src/Policy/__Tests/StellaOps.Policy.Engine.Tests/Tenancy/TenantContextTests.cs b/src/Policy/__Tests/StellaOps.Policy.Engine.Tests/Tenancy/TenantContextTests.cs index 5f75a372d..ad8ee02d1 100644 --- a/src/Policy/__Tests/StellaOps.Policy.Engine.Tests/Tenancy/TenantContextTests.cs +++ b/src/Policy/__Tests/StellaOps.Policy.Engine.Tests/Tenancy/TenantContextTests.cs @@ -2,6 +2,7 @@ using System.Security.Claims; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Time.Testing; using StellaOps.Policy.Engine.Tenancy; using Xunit; @@ -139,11 +140,13 @@ public sealed class TenantContextMiddlewareTests private readonly NullLogger _logger; private readonly TenantContextAccessor _tenantAccessor; private readonly TenantContextOptions _options; + private readonly TimeProvider _timeProvider; public TenantContextMiddlewareTests() { _logger = NullLogger.Instance; _tenantAccessor = new TenantContextAccessor(); + _timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero)); _options = new TenantContextOptions { Enabled = true, @@ -166,7 +169,8 @@ public sealed class TenantContextMiddlewareTests return Task.CompletedTask; }, MsOptions.Options.Create(_options), - _logger); + _logger, + _timeProvider); var context = CreateHttpContext("/api/risk/profiles", "tenant-123"); @@ -192,7 +196,8 @@ public sealed class TenantContextMiddlewareTests return Task.CompletedTask; }, MsOptions.Options.Create(_options), - _logger); + _logger, + _timeProvider); var context = CreateHttpContext("/api/risk/profiles", "tenant-123", "project-456"); @@ -214,7 +219,8 @@ public sealed class TenantContextMiddlewareTests var middleware = new TenantContextMiddleware( _ => { nextCalled = true; return Task.CompletedTask; }, MsOptions.Options.Create(_options), - _logger); + _logger, + _timeProvider); var context = CreateHttpContext("/api/risk/profiles", tenantId: null); @@ -245,7 +251,8 @@ public sealed class TenantContextMiddlewareTests return Task.CompletedTask; }, MsOptions.Options.Create(optionsNotRequired), - _logger); + _logger, + _timeProvider); var context = CreateHttpContext("/api/risk/profiles", tenantId: null); @@ -266,7 +273,8 @@ public sealed class TenantContextMiddlewareTests var middleware = new TenantContextMiddleware( _ => { nextCalled = true; return Task.CompletedTask; }, MsOptions.Options.Create(_options), - _logger); + _logger, + _timeProvider); var context = CreateHttpContext("/healthz", tenantId: null); @@ -287,7 +295,8 @@ public sealed class TenantContextMiddlewareTests var middleware = new TenantContextMiddleware( _ => { nextCalled = true; return Task.CompletedTask; }, MsOptions.Options.Create(disabledOptions), - _logger); + _logger, + _timeProvider); var context = CreateHttpContext("/api/risk/profiles", tenantId: null); @@ -313,7 +322,8 @@ public sealed class TenantContextMiddlewareTests return Task.CompletedTask; }, MsOptions.Options.Create(_options), - _logger); + _logger, + _timeProvider); var context = CreateHttpContext("/api/risk/profiles", tenantId); @@ -338,7 +348,8 @@ public sealed class TenantContextMiddlewareTests var middleware = new TenantContextMiddleware( _ => { nextCalled = true; return Task.CompletedTask; }, MsOptions.Options.Create(_options), - _logger); + _logger, + _timeProvider); var context = CreateHttpContext("/api/risk/profiles", tenantId); @@ -358,7 +369,8 @@ public sealed class TenantContextMiddlewareTests var middleware = new TenantContextMiddleware( _ => Task.CompletedTask, MsOptions.Options.Create(_options), - _logger); + _logger, + _timeProvider); var context = CreateHttpContext("/api/risk/profiles", longTenantId); @@ -384,7 +396,8 @@ public sealed class TenantContextMiddlewareTests return Task.CompletedTask; }, MsOptions.Options.Create(_options), - _logger); + _logger, + _timeProvider); var context = CreateHttpContext("/api/risk/profiles", "tenant-123", projectId); @@ -409,7 +422,8 @@ public sealed class TenantContextMiddlewareTests return Task.CompletedTask; }, MsOptions.Options.Create(_options), - _logger); + _logger, + _timeProvider); var context = CreateHttpContext("/api/risk/profiles", "tenant-123"); var claims = new[] @@ -440,7 +454,8 @@ public sealed class TenantContextMiddlewareTests return Task.CompletedTask; }, MsOptions.Options.Create(_options), - _logger); + _logger, + _timeProvider); var context = CreateHttpContext("/api/risk/profiles", "tenant-123"); var claims = new[] @@ -471,7 +486,8 @@ public sealed class TenantContextMiddlewareTests return Task.CompletedTask; }, MsOptions.Options.Create(_options), - _logger); + _logger, + _timeProvider); var context = CreateHttpContext("/api/risk/profiles", "tenant-123"); var claims = new[] { new Claim("sub", "user-id-123") }; @@ -498,7 +514,8 @@ public sealed class TenantContextMiddlewareTests return Task.CompletedTask; }, MsOptions.Options.Create(_options), - _logger); + _logger, + _timeProvider); var context = CreateHttpContext("/api/risk/profiles", "tenant-123"); context.Request.Headers["X-StellaOps-Actor"] = "service-account-123"; diff --git a/src/Policy/__Tests/StellaOps.Policy.Engine.Tests/Vex/VexDecisionEmitterTests.cs b/src/Policy/__Tests/StellaOps.Policy.Engine.Tests/Vex/VexDecisionEmitterTests.cs index 666763de3..cd95b5429 100644 --- a/src/Policy/__Tests/StellaOps.Policy.Engine.Tests/Vex/VexDecisionEmitterTests.cs +++ b/src/Policy/__Tests/StellaOps.Policy.Engine.Tests/Vex/VexDecisionEmitterTests.cs @@ -1,6 +1,7 @@ using System.Collections.Immutable; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; +using StellaOps.Determinism; using StellaOps.Policy.Engine.Gates; using StellaOps.Policy.Engine.ReachabilityFacts; using StellaOps.Policy.Engine.Vex; @@ -398,6 +399,7 @@ public class VexDecisionEmitterTests gateEvaluator, options, TimeProvider.System, + SystemGuidProvider.Instance, NullLogger.Instance); } diff --git a/src/Policy/__Tests/StellaOps.Policy.Engine.Tests/Vex/VexDecisionReachabilityIntegrationTests.cs b/src/Policy/__Tests/StellaOps.Policy.Engine.Tests/Vex/VexDecisionReachabilityIntegrationTests.cs index bfb12b289..a4f543cc0 100644 --- a/src/Policy/__Tests/StellaOps.Policy.Engine.Tests/Vex/VexDecisionReachabilityIntegrationTests.cs +++ b/src/Policy/__Tests/StellaOps.Policy.Engine.Tests/Vex/VexDecisionReachabilityIntegrationTests.cs @@ -8,6 +8,7 @@ using FluentAssertions; using Microsoft.Extensions.Logging.Abstractions; using MsOptions = Microsoft.Extensions.Options; using Moq; +using StellaOps.Determinism; using StellaOps.Policy.Engine.Gates; using StellaOps.Policy.Engine.ReachabilityFacts; using StellaOps.Policy.Engine.Vex; @@ -499,6 +500,7 @@ public sealed class VexDecisionReachabilityIntegrationTests gateEvaluator, new OptionsMonitorWrapper(options.Value), timeProvider ?? TimeProvider.System, + SystemGuidProvider.Instance, NullLogger.Instance); } diff --git a/src/Scanner/StellaOps.Scanner.Analyzers.Lang.Deno/TASKS.md b/src/Scanner/StellaOps.Scanner.Analyzers.Lang.Deno/TASKS.md new file mode 100644 index 000000000..c6490e1c9 --- /dev/null +++ b/src/Scanner/StellaOps.Scanner.Analyzers.Lang.Deno/TASKS.md @@ -0,0 +1,8 @@ +# Scanner Deno Analyzer Task Board + +This board tracks audit follow-ups for this module. +Source of truth: `docs/implplan/SPRINT_20260112_003_BE_csproj_audit_pending_apply.md`. + +| Task ID | Status | Notes | +| --- | --- | --- | +| AUDIT-DENO-0001 | DONE | Remediated Deno runtime hardening, determinism fixes, and tests for the audit hotlist. | diff --git a/src/Scanner/StellaOps.Scanner.Analyzers.Native/Hardening/ElfHardeningExtractor.cs b/src/Scanner/StellaOps.Scanner.Analyzers.Native/Hardening/ElfHardeningExtractor.cs index 7c83a42a9..b61b073ee 100644 --- a/src/Scanner/StellaOps.Scanner.Analyzers.Native/Hardening/ElfHardeningExtractor.cs +++ b/src/Scanner/StellaOps.Scanner.Analyzers.Native/Hardening/ElfHardeningExtractor.cs @@ -13,9 +13,10 @@ public sealed class ElfHardeningExtractor : IHardeningExtractor { private readonly TimeProvider _timeProvider; - public ElfHardeningExtractor(TimeProvider? timeProvider = null) + public ElfHardeningExtractor(TimeProvider timeProvider) { - _timeProvider = timeProvider ?? TimeProvider.System; + ArgumentNullException.ThrowIfNull(timeProvider); + _timeProvider = timeProvider; } // ELF magic bytes diff --git a/src/Scanner/StellaOps.Scanner.Analyzers.Native/Hardening/MachoHardeningExtractor.cs b/src/Scanner/StellaOps.Scanner.Analyzers.Native/Hardening/MachoHardeningExtractor.cs index 01d0de09b..500b6bacf 100644 --- a/src/Scanner/StellaOps.Scanner.Analyzers.Native/Hardening/MachoHardeningExtractor.cs +++ b/src/Scanner/StellaOps.Scanner.Analyzers.Native/Hardening/MachoHardeningExtractor.cs @@ -19,9 +19,10 @@ public sealed class MachoHardeningExtractor : IHardeningExtractor { private readonly TimeProvider _timeProvider; - public MachoHardeningExtractor(TimeProvider? timeProvider = null) + public MachoHardeningExtractor(TimeProvider timeProvider) { - _timeProvider = timeProvider ?? TimeProvider.System; + ArgumentNullException.ThrowIfNull(timeProvider); + _timeProvider = timeProvider; } // Mach-O magic numbers @@ -105,7 +106,7 @@ public sealed class MachoHardeningExtractor : IHardeningExtractor // Handle universal binaries - just extract first architecture for now if (magic is FAT_MAGIC or FAT_CIGAM) { - var fatResult = ExtractFromFat(data, path, digest); + var fatResult = await ExtractFromFatAsync(data, path, digest, ct); if (fatResult is not null) return fatResult; return CreateResult(path, digest, [], ["Universal binary: no supported architectures"]); @@ -233,7 +234,7 @@ public sealed class MachoHardeningExtractor : IHardeningExtractor /// /// Extract hardening info from the first slice of a universal (fat) binary. /// - private BinaryHardeningFlags? ExtractFromFat(byte[] data, string path, string digest) + private async Task ExtractFromFatAsync(byte[] data, string path, string digest, CancellationToken ct) { if (data.Length < 8) return null; @@ -254,7 +255,7 @@ public sealed class MachoHardeningExtractor : IHardeningExtractor // Extract first architecture and re-parse var sliceData = data.AsSpan((int)archOffset, (int)archSize).ToArray(); using var sliceStream = new MemoryStream(sliceData); - return ExtractAsync(sliceStream, path, digest).GetAwaiter().GetResult(); + return await ExtractAsync(sliceStream, path, digest, ct); } private static uint ReadUInt32(byte[] data, int offset, bool littleEndian) diff --git a/src/Scanner/StellaOps.Scanner.Analyzers.Native/Hardening/PeHardeningExtractor.cs b/src/Scanner/StellaOps.Scanner.Analyzers.Native/Hardening/PeHardeningExtractor.cs index 1c0ec5a0a..02dcb7a9b 100644 --- a/src/Scanner/StellaOps.Scanner.Analyzers.Native/Hardening/PeHardeningExtractor.cs +++ b/src/Scanner/StellaOps.Scanner.Analyzers.Native/Hardening/PeHardeningExtractor.cs @@ -21,9 +21,10 @@ public sealed class PeHardeningExtractor : IHardeningExtractor { private readonly TimeProvider _timeProvider; - public PeHardeningExtractor(TimeProvider? timeProvider = null) + public PeHardeningExtractor(TimeProvider timeProvider) { - _timeProvider = timeProvider ?? TimeProvider.System; + ArgumentNullException.ThrowIfNull(timeProvider); + _timeProvider = timeProvider; } // PE magic bytes: MZ (DOS header) diff --git a/src/Scanner/StellaOps.Scanner.Analyzers.Native/Index/OfflineBuildIdIndex.cs b/src/Scanner/StellaOps.Scanner.Analyzers.Native/Index/OfflineBuildIdIndex.cs index 84bccc54e..786bc64ad 100644 --- a/src/Scanner/StellaOps.Scanner.Analyzers.Native/Index/OfflineBuildIdIndex.cs +++ b/src/Scanner/StellaOps.Scanner.Analyzers.Native/Index/OfflineBuildIdIndex.cs @@ -32,16 +32,17 @@ public sealed class OfflineBuildIdIndex : IBuildIdIndex public OfflineBuildIdIndex( IOptions options, ILogger logger, - IDsseSigningService? dsseSigningService = null, - TimeProvider? timeProvider = null) + TimeProvider timeProvider, + IDsseSigningService? dsseSigningService = null) { ArgumentNullException.ThrowIfNull(options); ArgumentNullException.ThrowIfNull(logger); + ArgumentNullException.ThrowIfNull(timeProvider); _options = options.Value; _logger = logger; _dsseSigningService = dsseSigningService; - _timeProvider = timeProvider ?? TimeProvider.System; + _timeProvider = timeProvider; } /// diff --git a/src/Scanner/StellaOps.Scanner.Analyzers.Native/PeImportParser.cs b/src/Scanner/StellaOps.Scanner.Analyzers.Native/PeImportParser.cs index e66206b9f..c264c2041 100644 --- a/src/Scanner/StellaOps.Scanner.Analyzers.Native/PeImportParser.cs +++ b/src/Scanner/StellaOps.Scanner.Analyzers.Native/PeImportParser.cs @@ -514,7 +514,8 @@ public static class PeImportParser { IgnoreWhitespace = true, IgnoreComments = true, - DtdProcessing = DtdProcessing.Ignore, + DtdProcessing = DtdProcessing.Prohibit, + XmlResolver = null, }); while (reader.Read()) diff --git a/src/Scanner/StellaOps.Scanner.Analyzers.Native/RuntimeCapture/CaptureDurationTimer.cs b/src/Scanner/StellaOps.Scanner.Analyzers.Native/RuntimeCapture/CaptureDurationTimer.cs new file mode 100644 index 000000000..e58751da8 --- /dev/null +++ b/src/Scanner/StellaOps.Scanner.Analyzers.Native/RuntimeCapture/CaptureDurationTimer.cs @@ -0,0 +1,30 @@ +namespace StellaOps.Scanner.Analyzers.Native.RuntimeCapture; + +internal static class CaptureDurationTimer +{ + internal static Task RunAsync( + TimeSpan duration, + Func stopAsync, + CancellationToken captureToken, + CancellationToken stopToken) + { + ArgumentNullException.ThrowIfNull(stopAsync); + return RunCoreAsync(duration, stopAsync, captureToken, stopToken); + } + + private static async Task RunCoreAsync( + TimeSpan duration, + Func stopAsync, + CancellationToken captureToken, + CancellationToken stopToken) + { + try + { + await Task.Delay(duration, captureToken).ConfigureAwait(false); + await stopAsync(stopToken).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + } + } +} diff --git a/src/Scanner/StellaOps.Scanner.Analyzers.Native/RuntimeCapture/IRuntimeCaptureAdapter.cs b/src/Scanner/StellaOps.Scanner.Analyzers.Native/RuntimeCapture/IRuntimeCaptureAdapter.cs index 460919efc..87f14c004 100644 --- a/src/Scanner/StellaOps.Scanner.Analyzers.Native/RuntimeCapture/IRuntimeCaptureAdapter.cs +++ b/src/Scanner/StellaOps.Scanner.Analyzers.Native/RuntimeCapture/IRuntimeCaptureAdapter.cs @@ -1,3 +1,5 @@ +using StellaOps.Determinism; + namespace StellaOps.Scanner.Analyzers.Native.RuntimeCapture; /// @@ -159,16 +161,19 @@ public static class RuntimeCaptureAdapterFactory /// Creates the appropriate capture adapter for the current platform. /// /// Platform-specific adapter or null if no adapter available. - public static IRuntimeCaptureAdapter? CreateForCurrentPlatform() + public static IRuntimeCaptureAdapter? CreateForCurrentPlatform(TimeProvider timeProvider, IGuidProvider guidProvider) { + ArgumentNullException.ThrowIfNull(timeProvider); + ArgumentNullException.ThrowIfNull(guidProvider); + if (OperatingSystem.IsLinux()) - return new LinuxEbpfCaptureAdapter(); + return new LinuxEbpfCaptureAdapter(timeProvider, guidProvider); if (OperatingSystem.IsWindows()) - return new WindowsEtwCaptureAdapter(); + return new WindowsEtwCaptureAdapter(timeProvider, guidProvider); if (OperatingSystem.IsMacOS()) - return new MacOsDyldCaptureAdapter(); + return new MacOsDyldCaptureAdapter(timeProvider, guidProvider); return null; } @@ -177,18 +182,21 @@ public static class RuntimeCaptureAdapterFactory /// Gets all available adapters for the current platform. /// /// List of available adapters. - public static IReadOnlyList GetAvailableAdapters() + public static IReadOnlyList GetAvailableAdapters(TimeProvider timeProvider, IGuidProvider guidProvider) { + ArgumentNullException.ThrowIfNull(timeProvider); + ArgumentNullException.ThrowIfNull(guidProvider); + var adapters = new List(); if (OperatingSystem.IsLinux()) - adapters.Add(new LinuxEbpfCaptureAdapter()); + adapters.Add(new LinuxEbpfCaptureAdapter(timeProvider, guidProvider)); if (OperatingSystem.IsWindows()) - adapters.Add(new WindowsEtwCaptureAdapter()); + adapters.Add(new WindowsEtwCaptureAdapter(timeProvider, guidProvider)); if (OperatingSystem.IsMacOS()) - adapters.Add(new MacOsDyldCaptureAdapter()); + adapters.Add(new MacOsDyldCaptureAdapter(timeProvider, guidProvider)); return adapters; } diff --git a/src/Scanner/StellaOps.Scanner.Analyzers.Native/RuntimeCapture/LinuxEbpfCaptureAdapter.cs b/src/Scanner/StellaOps.Scanner.Analyzers.Native/RuntimeCapture/LinuxEbpfCaptureAdapter.cs index 19e2b10ad..a74322bc0 100644 --- a/src/Scanner/StellaOps.Scanner.Analyzers.Native/RuntimeCapture/LinuxEbpfCaptureAdapter.cs +++ b/src/Scanner/StellaOps.Scanner.Analyzers.Native/RuntimeCapture/LinuxEbpfCaptureAdapter.cs @@ -1,5 +1,6 @@ using System.Collections.Concurrent; using System.Diagnostics; +using System.Globalization; using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Text; @@ -32,6 +33,7 @@ public sealed class LinuxEbpfCaptureAdapter : IRuntimeCaptureAdapter private DateTime _startTime; private CancellationTokenSource? _captureCts; private Task? _captureTask; + private Task? _durationTask; private Process? _bpftraceProcess; private long _droppedEvents; private int _redactedPaths; @@ -39,11 +41,12 @@ public sealed class LinuxEbpfCaptureAdapter : IRuntimeCaptureAdapter /// /// Creates a new Linux eBPF capture adapter. /// - /// Optional time provider for deterministic timestamps. + /// Time provider for deterministic timestamps. /// Optional GUID provider for deterministic session IDs. - public LinuxEbpfCaptureAdapter(TimeProvider? timeProvider = null, IGuidProvider? guidProvider = null) + public LinuxEbpfCaptureAdapter(TimeProvider timeProvider, IGuidProvider? guidProvider = null) { - _timeProvider = timeProvider ?? TimeProvider.System; + ArgumentNullException.ThrowIfNull(timeProvider); + _timeProvider = timeProvider; _guidProvider = guidProvider ?? SystemGuidProvider.Instance; } @@ -184,18 +187,11 @@ public sealed class LinuxEbpfCaptureAdapter : IRuntimeCaptureAdapter } // Start the duration timer - _ = Task.Run(async () => - { - try - { - await Task.Delay(options.MaxCaptureDuration, _captureCts.Token); - await StopCaptureAsync(CancellationToken.None); - } - catch (OperationCanceledException) - { - // Expected when capture is stopped manually - } - }, _captureCts.Token); + _durationTask = CaptureDurationTimer.RunAsync( + options.MaxCaptureDuration, + ct => StopCaptureAsync(ct), + _captureCts.Token, + cancellationToken); SetState(CaptureState.Running); return SessionId; @@ -254,6 +250,22 @@ public sealed class LinuxEbpfCaptureAdapter : IRuntimeCaptureAdapter } } + if (_durationTask != null) + { + try + { + await _durationTask.WaitAsync(TimeSpan.FromSeconds(2), cancellationToken); + } + catch (TimeoutException) + { + // Timer did not complete in time + } + catch (OperationCanceledException) + { + // Expected when canceling + } + } + var session = new RuntimeCaptureSession( SessionId: SessionId ?? "unknown", StartTime: _startTime, @@ -333,20 +345,7 @@ public sealed class LinuxEbpfCaptureAdapter : IRuntimeCaptureAdapter // Build bpftrace script for dlopen tracing var script = BuildBpftraceScript(); - var psi = new ProcessStartInfo - { - FileName = "bpftrace", - Arguments = $"-e '{script}'", - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false, - CreateNoWindow = true, - }; - - if (_options?.TargetProcessId != null) - { - psi.Arguments = $"-p {_options.TargetProcessId} -e '{script}'"; - } + var psi = CreateBpftraceStartInfo(script, _options?.TargetProcessId); _bpftraceProcess = new Process { StartInfo = psi }; _bpftraceProcess.OutputDataReceived += OnBpftraceOutput; @@ -420,8 +419,8 @@ public sealed class LinuxEbpfCaptureAdapter : IRuntimeCaptureAdapter { return new RuntimeLoadEvent( Timestamp: _timeProvider.GetUtcNow().UtcDateTime, - ProcessId: int.Parse(parts[1]), - ThreadId: int.Parse(parts[2]), + ProcessId: int.Parse(parts[1], CultureInfo.InvariantCulture), + ThreadId: int.Parse(parts[2], CultureInfo.InvariantCulture), LoadType: RuntimeLoadType.Dlopen, RequestedPath: parts[3], ResolvedPath: null, // Set on return probe @@ -512,14 +511,17 @@ public sealed class LinuxEbpfCaptureAdapter : IRuntimeCaptureAdapter { try { + if (!IsSafeToken(command)) + return false; + var psi = new ProcessStartInfo { FileName = "which", - Arguments = command, RedirectStandardOutput = true, UseShellExecute = false, CreateNoWindow = true, }; + psi.ArgumentList.Add(command); using var process = Process.Start(psi); if (process == null) @@ -545,8 +547,8 @@ public sealed class LinuxEbpfCaptureAdapter : IRuntimeCaptureAdapter if (match.Success) { return new Version( - int.Parse(match.Groups[1].Value), - int.Parse(match.Groups[2].Value)); + int.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture), + int.Parse(match.Groups[2].Value, CultureInfo.InvariantCulture)); } } } @@ -577,14 +579,17 @@ public sealed class LinuxEbpfCaptureAdapter : IRuntimeCaptureAdapter { try { + if (!IsSafeToken(capability)) + return false; + var psi = new ProcessStartInfo { FileName = "capsh", - Arguments = $"--print", RedirectStandardOutput = true, UseShellExecute = false, CreateNoWindow = true, }; + psi.ArgumentList.Add("--print"); using var process = Process.Start(psi); if (process == null) @@ -609,4 +614,43 @@ public sealed class LinuxEbpfCaptureAdapter : IRuntimeCaptureAdapter return Regex.IsMatch(path, regexPattern, RegexOptions.IgnoreCase); } + + private static ProcessStartInfo CreateBpftraceStartInfo(string script, int? targetProcessId) + { + if (string.IsNullOrWhiteSpace(script)) + throw new ArgumentException("Script cannot be empty.", nameof(script)); + + var psi = new ProcessStartInfo + { + FileName = "bpftrace", + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true, + }; + + if (targetProcessId is int pid && pid > 0) + { + psi.ArgumentList.Add("-p"); + psi.ArgumentList.Add(pid.ToString(CultureInfo.InvariantCulture)); + } + + psi.ArgumentList.Add("-e"); + psi.ArgumentList.Add(script); + return psi; + } + + private static bool IsSafeToken(string value) + { + if (string.IsNullOrWhiteSpace(value)) + return false; + + foreach (var ch in value) + { + if (!char.IsAsciiLetterOrDigit(ch) && ch != '-' && ch != '_' && ch != '.') + return false; + } + + return true; + } } diff --git a/src/Scanner/StellaOps.Scanner.Analyzers.Native/RuntimeCapture/MacOsDyldCaptureAdapter.cs b/src/Scanner/StellaOps.Scanner.Analyzers.Native/RuntimeCapture/MacOsDyldCaptureAdapter.cs index f73be2c8c..1e280b754 100644 --- a/src/Scanner/StellaOps.Scanner.Analyzers.Native/RuntimeCapture/MacOsDyldCaptureAdapter.cs +++ b/src/Scanner/StellaOps.Scanner.Analyzers.Native/RuntimeCapture/MacOsDyldCaptureAdapter.cs @@ -1,5 +1,6 @@ using System.Collections.Concurrent; using System.Diagnostics; +using System.Globalization; using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Text.RegularExpressions; @@ -33,6 +34,7 @@ public sealed class MacOsDyldCaptureAdapter : IRuntimeCaptureAdapter private DateTime _startTime; private CancellationTokenSource? _captureCts; private Task? _captureTask; + private Task? _durationTask; private Process? _dtraceProcess; private long _droppedEvents; private int _redactedPaths; @@ -40,11 +42,12 @@ public sealed class MacOsDyldCaptureAdapter : IRuntimeCaptureAdapter /// /// Creates a new macOS dyld capture adapter. /// - /// Optional time provider for deterministic timestamps. + /// Time provider for deterministic timestamps. /// Optional GUID provider for deterministic session IDs. - public MacOsDyldCaptureAdapter(TimeProvider? timeProvider = null, IGuidProvider? guidProvider = null) + public MacOsDyldCaptureAdapter(TimeProvider timeProvider, IGuidProvider? guidProvider = null) { - _timeProvider = timeProvider ?? TimeProvider.System; + ArgumentNullException.ThrowIfNull(timeProvider); + _timeProvider = timeProvider; _guidProvider = guidProvider ?? SystemGuidProvider.Instance; } @@ -188,18 +191,11 @@ public sealed class MacOsDyldCaptureAdapter : IRuntimeCaptureAdapter } // Start the duration timer - _ = Task.Run(async () => - { - try - { - await Task.Delay(options.MaxCaptureDuration, _captureCts.Token); - await StopCaptureAsync(CancellationToken.None); - } - catch (OperationCanceledException) - { - // Expected when capture is stopped manually - } - }, _captureCts.Token); + _durationTask = CaptureDurationTimer.RunAsync( + options.MaxCaptureDuration, + ct => StopCaptureAsync(ct), + _captureCts.Token, + cancellationToken); SetState(CaptureState.Running); return SessionId; @@ -258,6 +254,22 @@ public sealed class MacOsDyldCaptureAdapter : IRuntimeCaptureAdapter } } + if (_durationTask != null) + { + try + { + await _durationTask.WaitAsync(TimeSpan.FromSeconds(2), cancellationToken); + } + catch (TimeoutException) + { + // Timer did not complete in time + } + catch (OperationCanceledException) + { + // Expected when canceling + } + } + var session = new RuntimeCaptureSession( SessionId: SessionId ?? "unknown", StartTime: _startTime, @@ -337,20 +349,7 @@ public sealed class MacOsDyldCaptureAdapter : IRuntimeCaptureAdapter // Build dtrace script for dyld tracing var script = BuildDtraceScript(); - var psi = new ProcessStartInfo - { - FileName = "dtrace", - Arguments = $"-n '{script}'", - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false, - CreateNoWindow = true, - }; - - if (_options?.TargetProcessId != null) - { - psi.Arguments = $"-p {_options.TargetProcessId} -n '{script}'"; - } + var psi = CreateDtraceStartInfo(script, _options?.TargetProcessId); _dtraceProcess = new Process { StartInfo = psi }; _dtraceProcess.OutputDataReceived += OnDtraceOutput; @@ -432,8 +431,8 @@ public sealed class MacOsDyldCaptureAdapter : IRuntimeCaptureAdapter return new RuntimeLoadEvent( Timestamp: _timeProvider.GetUtcNow().UtcDateTime, - ProcessId: int.Parse(parts[1]), - ThreadId: int.Parse(parts[2]), + ProcessId: int.Parse(parts[1], CultureInfo.InvariantCulture), + ThreadId: int.Parse(parts[2], CultureInfo.InvariantCulture), LoadType: loadType, RequestedPath: parts[3], ResolvedPath: null, // Set on return probe @@ -524,14 +523,17 @@ public sealed class MacOsDyldCaptureAdapter : IRuntimeCaptureAdapter { try { + if (!IsSafeToken(command)) + return false; + var psi = new ProcessStartInfo { FileName = "which", - Arguments = command, RedirectStandardOutput = true, UseShellExecute = false, CreateNoWindow = true, }; + psi.ArgumentList.Add(command); using var process = Process.Start(psi); if (process == null) @@ -557,14 +559,17 @@ public sealed class MacOsDyldCaptureAdapter : IRuntimeCaptureAdapter { try { + if (!IsSafeToken("csrutil")) + return SipStatus.Unknown; + var psi = new ProcessStartInfo { FileName = "csrutil", - Arguments = "status", RedirectStandardOutput = true, UseShellExecute = false, CreateNoWindow = true, }; + psi.ArgumentList.Add("status"); using var process = Process.Start(psi); if (process == null) @@ -610,4 +615,43 @@ public sealed class MacOsDyldCaptureAdapter : IRuntimeCaptureAdapter return Regex.IsMatch(path, regexPattern, RegexOptions.IgnoreCase); } + + private static ProcessStartInfo CreateDtraceStartInfo(string script, int? targetProcessId) + { + if (string.IsNullOrWhiteSpace(script)) + throw new ArgumentException("Script cannot be empty.", nameof(script)); + + var psi = new ProcessStartInfo + { + FileName = "dtrace", + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true, + }; + + if (targetProcessId is int pid && pid > 0) + { + psi.ArgumentList.Add("-p"); + psi.ArgumentList.Add(pid.ToString(CultureInfo.InvariantCulture)); + } + + psi.ArgumentList.Add("-n"); + psi.ArgumentList.Add(script); + return psi; + } + + private static bool IsSafeToken(string value) + { + if (string.IsNullOrWhiteSpace(value)) + return false; + + foreach (var ch in value) + { + if (!char.IsAsciiLetterOrDigit(ch) && ch != '-' && ch != '_' && ch != '.') + return false; + } + + return true; + } } diff --git a/src/Scanner/StellaOps.Scanner.Analyzers.Native/RuntimeCapture/RuntimeEvidenceAggregator.cs b/src/Scanner/StellaOps.Scanner.Analyzers.Native/RuntimeCapture/RuntimeEvidenceAggregator.cs index 967a4dbd0..6f2590f6b 100644 --- a/src/Scanner/StellaOps.Scanner.Analyzers.Native/RuntimeCapture/RuntimeEvidenceAggregator.cs +++ b/src/Scanner/StellaOps.Scanner.Analyzers.Native/RuntimeCapture/RuntimeEvidenceAggregator.cs @@ -48,14 +48,15 @@ public static class RuntimeEvidenceAggregator /// Runtime capture evidence. /// Static analysis dependency edges. /// Heuristic analysis edges. - /// Optional time provider for deterministic timestamps. + /// Time provider for deterministic timestamps. /// Merged evidence document. public static MergedEvidence MergeWithStaticAnalysis( RuntimeEvidence runtimeEvidence, IEnumerable staticEdges, IEnumerable heuristicEdges, - TimeProvider? timeProvider = null) + TimeProvider timeProvider) { + ArgumentNullException.ThrowIfNull(timeProvider); var staticList = staticEdges.ToList(); var heuristicList = heuristicEdges.ToList(); @@ -142,7 +143,6 @@ public static class RuntimeEvidenceAggregator } } - var tp = timeProvider ?? TimeProvider.System; return new MergedEvidence( ConfirmedEdges: confirmedEdges, StaticOnlyEdges: staticOnlyEdges, @@ -151,7 +151,7 @@ public static class RuntimeEvidenceAggregator TotalRuntimeEvents: runtimeEvidence.Sessions.Sum(s => s.Events.Count), TotalDroppedEvents: runtimeEvidence.Sessions.Sum(s => s.TotalEventsDropped), CaptureStartTime: runtimeEvidence.Sessions.Min(s => s.StartTime), - CaptureEndTime: runtimeEvidence.Sessions.Max(s => s.EndTime ?? tp.GetUtcNow().UtcDateTime)); + CaptureEndTime: runtimeEvidence.Sessions.Max(s => s.EndTime ?? timeProvider.GetUtcNow().UtcDateTime)); } /// diff --git a/src/Scanner/StellaOps.Scanner.Analyzers.Native/RuntimeCapture/StackTraceCapture.cs b/src/Scanner/StellaOps.Scanner.Analyzers.Native/RuntimeCapture/StackTraceCapture.cs index cd8e49b63..35b9e86f3 100644 --- a/src/Scanner/StellaOps.Scanner.Analyzers.Native/RuntimeCapture/StackTraceCapture.cs +++ b/src/Scanner/StellaOps.Scanner.Analyzers.Native/RuntimeCapture/StackTraceCapture.cs @@ -1,3 +1,4 @@ +using System.Globalization; using System.Runtime.Versioning; namespace StellaOps.Scanner.Analyzers.Native.RuntimeCapture; @@ -274,12 +275,14 @@ public sealed record CollapsedStack /// Format: "container@digest;buildid=xxx;func;... count" /// /// The collapsed stack line to parse. - /// Optional time provider for deterministic timestamps. - public static CollapsedStack? Parse(string line, TimeProvider? timeProvider = null) + /// Time provider for deterministic timestamps. + public static CollapsedStack? Parse(string line, TimeProvider timeProvider) { if (string.IsNullOrWhiteSpace(line)) return null; + ArgumentNullException.ThrowIfNull(timeProvider); + var lastSpace = line.LastIndexOf(' '); if (lastSpace < 0) return null; @@ -287,7 +290,7 @@ public sealed record CollapsedStack var stackPart = line[..lastSpace]; var countPart = line[(lastSpace + 1)..]; - if (!int.TryParse(countPart, out var count)) + if (!int.TryParse(countPart, NumberStyles.Integer, CultureInfo.InvariantCulture, out var count)) return null; var firstSemi = stackPart.IndexOf(';'); @@ -307,8 +310,7 @@ public sealed record CollapsedStack } } - var tp = timeProvider ?? TimeProvider.System; - var now = tp.GetUtcNow().UtcDateTime; + var now = timeProvider.GetUtcNow().UtcDateTime; return new CollapsedStack { ContainerIdentifier = container, diff --git a/src/Scanner/StellaOps.Scanner.Analyzers.Native/RuntimeCapture/WindowsEtwCaptureAdapter.cs b/src/Scanner/StellaOps.Scanner.Analyzers.Native/RuntimeCapture/WindowsEtwCaptureAdapter.cs index 7da2f8da9..9a41808c1 100644 --- a/src/Scanner/StellaOps.Scanner.Analyzers.Native/RuntimeCapture/WindowsEtwCaptureAdapter.cs +++ b/src/Scanner/StellaOps.Scanner.Analyzers.Native/RuntimeCapture/WindowsEtwCaptureAdapter.cs @@ -1,5 +1,6 @@ using System.Collections.Concurrent; using System.Diagnostics; +using System.Globalization; using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Security.Principal; @@ -31,20 +32,19 @@ public sealed class WindowsEtwCaptureAdapter : IRuntimeCaptureAdapter private DateTime _startTime; private CancellationTokenSource? _captureCts; private Task? _captureTask; -#pragma warning disable CS0649 // Field is never assigned (assigned via Start/Stop ETW) - private Process? _logmanProcess; -#pragma warning restore CS0649 + private Task? _durationTask; private long _droppedEvents; private int _redactedPaths; /// /// Creates a new Windows ETW capture adapter. /// - /// Optional time provider for deterministic timestamps. + /// Time provider for deterministic timestamps. /// Optional GUID provider for deterministic session IDs. - public WindowsEtwCaptureAdapter(TimeProvider? timeProvider = null, IGuidProvider? guidProvider = null) + public WindowsEtwCaptureAdapter(TimeProvider timeProvider, IGuidProvider? guidProvider = null) { - _timeProvider = timeProvider ?? TimeProvider.System; + ArgumentNullException.ThrowIfNull(timeProvider); + _timeProvider = timeProvider; _guidProvider = guidProvider ?? SystemGuidProvider.Instance; } @@ -106,16 +106,21 @@ public sealed class WindowsEtwCaptureAdapter : IRuntimeCaptureAdapter requiresElevation = true; } - // Check for logman.exe (built-in ETW management tool) - var logmanPath = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.System), - "logman.exe"); + // Check for logman.exe and tracerpt.exe (built-in ETW tools) + var systemDir = Environment.GetFolderPath(Environment.SpecialFolder.System); + var logmanPath = Path.Combine(systemDir, "logman.exe"); + var tracerptPath = Path.Combine(systemDir, "tracerpt.exe"); if (!File.Exists(logmanPath)) { missingDeps.Add("logman.exe"); } + if (!File.Exists(tracerptPath)) + { + missingDeps.Add("tracerpt.exe"); + } + if (missingDeps.Count > 0) { return Task.FromResult(new AdapterAvailability( @@ -178,18 +183,11 @@ public sealed class WindowsEtwCaptureAdapter : IRuntimeCaptureAdapter } // Start the duration timer - _ = Task.Run(async () => - { - try - { - await Task.Delay(options.MaxCaptureDuration, _captureCts.Token); - await StopCaptureAsync(CancellationToken.None); - } - catch (OperationCanceledException) - { - // Expected when capture is stopped manually - } - }, _captureCts.Token); + _durationTask = CaptureDurationTimer.RunAsync( + options.MaxCaptureDuration, + ct => StopCaptureAsync(ct), + _captureCts.Token, + cancellationToken); SetState(CaptureState.Running); return SessionId; @@ -220,20 +218,6 @@ public sealed class WindowsEtwCaptureAdapter : IRuntimeCaptureAdapter // Stop ETW session await StopEtwSessionAsync(cancellationToken); - // Kill logman process if running - if (_logmanProcess is { HasExited: false }) - { - try - { - _logmanProcess.Kill(true); - await _logmanProcess.WaitForExitAsync(cancellationToken); - } - catch - { - // Ignore kill errors - } - } - // Wait for capture task if (_captureTask != null) { @@ -251,6 +235,22 @@ public sealed class WindowsEtwCaptureAdapter : IRuntimeCaptureAdapter } } + if (_durationTask != null) + { + try + { + await _durationTask.WaitAsync(TimeSpan.FromSeconds(2), cancellationToken); + } + catch (TimeoutException) + { + // Timer did not complete in time + } + catch (OperationCanceledException) + { + // Expected when canceling + } + } + var session = new RuntimeCaptureSession( SessionId: SessionId ?? "unknown", StartTime: _startTime, @@ -298,7 +298,6 @@ public sealed class WindowsEtwCaptureAdapter : IRuntimeCaptureAdapter } _captureCts?.Dispose(); - _logmanProcess?.Dispose(); } private async Task RunSandboxCaptureAsync(CancellationToken cancellationToken) @@ -367,17 +366,17 @@ public sealed class WindowsEtwCaptureAdapter : IRuntimeCaptureAdapter { // Create ETW session for kernel image load events // Provider GUID for Microsoft-Windows-Kernel-Process: {22FB2CD6-0E7B-422B-A0C7-2FAD1FD0E716} - var args = $"create trace \"{sessionName}\" -o \"{etlPath}\" -p \"Microsoft-Windows-Kernel-Process\" 0x10 -ets"; - - var psi = new ProcessStartInfo - { - FileName = "logman.exe", - Arguments = args, - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false, - CreateNoWindow = true, - }; + var psi = CreateLogmanStartInfo( + sessionName, + "create", + "trace", + sessionName, + "-o", + etlPath, + "-p", + "Microsoft-Windows-Kernel-Process", + "0x10", + "-ets"); using var process = Process.Start(psi); if (process == null) @@ -399,15 +398,11 @@ public sealed class WindowsEtwCaptureAdapter : IRuntimeCaptureAdapter var sessionName = $"StellaOps_ImageLoad_{SessionId}"; - var psi = new ProcessStartInfo - { - FileName = "logman.exe", - Arguments = $"stop \"{sessionName}\" -ets", - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false, - CreateNoWindow = true, - }; + var psi = CreateLogmanStartInfo( + sessionName, + "stop", + sessionName, + "-ets"); try { @@ -433,15 +428,7 @@ public sealed class WindowsEtwCaptureAdapter : IRuntimeCaptureAdapter try { - var psi = new ProcessStartInfo - { - FileName = "tracerpt.exe", - Arguments = $"\"{etlPath}\" -o \"{xmlPath}\" -of XML -summary -report", - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false, - CreateNoWindow = true, - }; + var psi = CreateTracerptStartInfo(etlPath, xmlPath); using var process = Process.Start(psi); if (process != null) @@ -482,8 +469,13 @@ public sealed class WindowsEtwCaptureAdapter : IRuntimeCaptureAdapter foreach (Match match in imageLoadPattern.Matches(content)) { var imagePath = match.Groups[1].Value; - var processId = int.Parse(match.Groups[2].Value); - var imageBase = Convert.ToUInt64(match.Groups[3].Value, 16); + var processId = int.Parse(match.Groups[2].Value, CultureInfo.InvariantCulture); + var imageBaseText = match.Groups[3].Value; + if (imageBaseText.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) + { + imageBaseText = imageBaseText[2..]; + } + var imageBase = ulong.Parse(imageBaseText, NumberStyles.HexNumber, CultureInfo.InvariantCulture); // Skip if filtering by process and doesn't match if (_options?.TargetProcessId != null && processId != _options.TargetProcessId) @@ -597,6 +589,81 @@ public sealed class WindowsEtwCaptureAdapter : IRuntimeCaptureAdapter } } + private static ProcessStartInfo CreateLogmanStartInfo(string sessionName, params string[] args) + { + if (string.IsNullOrWhiteSpace(sessionName)) + throw new ArgumentException("Session name cannot be empty.", nameof(sessionName)); + + if (!IsSafeToken(sessionName)) + throw new InvalidOperationException("Session name contains unsupported characters."); + + var psi = new ProcessStartInfo + { + FileName = GetSystemToolPath("logman.exe"), + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true, + }; + + foreach (var arg in args) + { + psi.ArgumentList.Add(arg); + } + + return psi; + } + + private static ProcessStartInfo CreateTracerptStartInfo(string etlPath, string xmlPath) + { + var psi = new ProcessStartInfo + { + FileName = GetSystemToolPath("tracerpt.exe"), + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true, + }; + + psi.ArgumentList.Add(etlPath); + psi.ArgumentList.Add("-o"); + psi.ArgumentList.Add(xmlPath); + psi.ArgumentList.Add("-of"); + psi.ArgumentList.Add("XML"); + psi.ArgumentList.Add("-summary"); + psi.ArgumentList.Add("-report"); + + return psi; + } + + private static string GetSystemToolPath(string toolName) + { + if (!IsSafeToken(toolName)) + throw new InvalidOperationException("Tool name contains unsupported characters."); + + var systemDir = Environment.GetFolderPath(Environment.SpecialFolder.System); + var path = Path.Combine(systemDir, toolName); + + if (!File.Exists(path)) + throw new FileNotFoundException($"Required tool not found: {toolName}", path); + + return path; + } + + private static bool IsSafeToken(string value) + { + if (string.IsNullOrWhiteSpace(value)) + return false; + + foreach (var ch in value) + { + if (!char.IsAsciiLetterOrDigit(ch) && ch != '-' && ch != '_' && ch != '.') + return false; + } + + return true; + } + private static bool MatchGlob(string path, string pattern) { var regexPattern = "^" + Regex.Escape(pattern) diff --git a/src/Scanner/StellaOps.Scanner.Analyzers.Native/ServiceCollectionExtensions.cs b/src/Scanner/StellaOps.Scanner.Analyzers.Native/ServiceCollectionExtensions.cs index f52138f4e..4bc65572e 100644 --- a/src/Scanner/StellaOps.Scanner.Analyzers.Native/ServiceCollectionExtensions.cs +++ b/src/Scanner/StellaOps.Scanner.Analyzers.Native/ServiceCollectionExtensions.cs @@ -1,6 +1,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; +using StellaOps.Determinism; using StellaOps.Scanner.Analyzers.Native.Plugin; using StellaOps.Scanner.Analyzers.Native.RuntimeCapture; @@ -84,6 +85,7 @@ public static class ServiceCollectionExtensions this IServiceCollection services, Action? configure = null) { + services.AddDeterminismDefaults(); var optionsBuilder = services.AddOptions(); if (configure != null) @@ -94,7 +96,9 @@ public static class ServiceCollectionExtensions // Register platform-appropriate capture adapter services.TryAddSingleton(sp => { - var adapter = RuntimeCaptureAdapterFactory.CreateForCurrentPlatform(); + var timeProvider = sp.GetRequiredService(); + var guidProvider = sp.GetRequiredService(); + var adapter = RuntimeCaptureAdapterFactory.CreateForCurrentPlatform(timeProvider, guidProvider); if (adapter == null) { throw new PlatformNotSupportedException( diff --git a/src/Scanner/StellaOps.Scanner.Sbomer.BuildXPlugin/Program.cs b/src/Scanner/StellaOps.Scanner.Sbomer.BuildXPlugin/Program.cs index 36ec44da1..36af1995b 100644 --- a/src/Scanner/StellaOps.Scanner.Sbomer.BuildXPlugin/Program.cs +++ b/src/Scanner/StellaOps.Scanner.Sbomer.BuildXPlugin/Program.cs @@ -93,7 +93,7 @@ internal static class Program var result = await casClient.VerifyWriteAsync(cancellationToken).ConfigureAwait(false); - Console.WriteLine($"handshake ok: {manifest.Id}@{manifest.Version} → {result.Algorithm}:{result.Digest}"); + Console.WriteLine($"handshake ok: {manifest.Id}@{manifest.Version} -> {result.Algorithm}:{result.Digest}"); Console.WriteLine(result.Path); return 0; } @@ -260,8 +260,9 @@ internal static class Program if (attestorUri is not null) { - using var httpClient = CreateAttestorHttpClient(attestorUri, attestorToken, attestorInsecure); - var attestorClient = new AttestorClient(httpClient); + var allowInsecure = attestorInsecure && ShouldAllowInsecureAttestor(attestorUri); + using var attestorScope = CreateAttestorHttpClientScope(attestorUri, attestorToken, allowInsecure); + var attestorClient = new AttestorClient(attestorScope.Client); await attestorClient.SendPlaceholderAsync(attestorUri, document, cancellationToken).ConfigureAwait(false); } @@ -340,7 +341,7 @@ internal static class Program ?? generatorVersion; var workerInstance = GetOption(args, "--surface-worker-instance") ?? Environment.GetEnvironmentVariable("STELLAOPS_SURFACE_WORKER_INSTANCE") - ?? Environment.MachineName; + ?? component; var attemptValue = GetOption(args, "--surface-attempt") ?? Environment.GetEnvironmentVariable("STELLAOPS_SURFACE_ATTEMPT"); var attempt = 1; @@ -445,7 +446,7 @@ internal static class Program Component: "Scanner.BuildXPlugin", SecretType: "attestation"); - using var handle = secretProvider.GetAsync(request).AsTask().GetAwaiter().GetResult(); + using var handle = secretProvider.Get(request); var secret = SurfaceSecretParser.ParseAttestationSecret(handle); // Return the API key or token for attestor authentication @@ -498,7 +499,7 @@ internal static class Program Component: "Scanner.BuildXPlugin", SecretType: "cas-access"); - using var handle = secretProvider.GetAsync(request).AsTask().GetAwaiter().GetResult(); + using var handle = secretProvider.Get(request); return SurfaceSecretParser.ParseCasAccessSecret(handle); } catch @@ -556,31 +557,71 @@ internal static class Program return value; } - private static HttpClient CreateAttestorHttpClient(Uri attestorUri, string? bearerToken, bool insecure) + private static bool ShouldAllowInsecureAttestor(Uri attestorUri) { - var handler = new HttpClientHandler + if (!string.Equals(attestorUri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase)) { - CheckCertificateRevocationList = true, - }; + Console.Error.WriteLine("Attestor insecure flag ignored for non-HTTPS endpoint."); + return false; + } - if (insecure && string.Equals(attestorUri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase)) - { + Console.Error.WriteLine("WARNING: Attestor TLS verification disabled; use only for dev/test."); + return true; + } + + private static AttestorHttpClientScope CreateAttestorHttpClientScope(Uri attestorUri, string? bearerToken, bool insecure) + { + var services = new ServiceCollection(); + services.AddHttpClient("attestor", client => + { + client.Timeout = TimeSpan.FromSeconds(30); + client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + + if (!string.IsNullOrWhiteSpace(bearerToken)) + { + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", bearerToken); + } + }) + .ConfigurePrimaryHttpMessageHandler(() => + { + var handler = new HttpClientHandler + { + CheckCertificateRevocationList = true + }; + + if (insecure && string.Equals(attestorUri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase)) + { #pragma warning disable S4830 // Explicitly gated by --attestor-insecure flag/env for dev/test usage. - handler.ServerCertificateCustomValidationCallback = (_, _, _, _) => true; + handler.ServerCertificateCustomValidationCallback = (_, _, _, _) => true; #pragma warning restore S4830 + } + + return handler; + }); + + var provider = services.BuildServiceProvider(); + var factory = provider.GetRequiredService(); + var client = factory.CreateClient("attestor"); + return new AttestorHttpClientScope(provider, client); + } + + private sealed class AttestorHttpClientScope : IDisposable + { + private readonly ServiceProvider _provider; + + public AttestorHttpClientScope(ServiceProvider provider, HttpClient client) + { + _provider = provider ?? throw new ArgumentNullException(nameof(provider)); + Client = client ?? throw new ArgumentNullException(nameof(client)); } - var client = new HttpClient(handler, disposeHandler: true) - { - Timeout = TimeSpan.FromSeconds(30) - }; - client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + public HttpClient Client { get; } - if (!string.IsNullOrWhiteSpace(bearerToken)) + public void Dispose() { - client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", bearerToken); + Client.Dispose(); + _provider.Dispose(); } - - return client; } } + diff --git a/src/Scanner/StellaOps.Scanner.Sbomer.BuildXPlugin/StellaOps.Scanner.Sbomer.BuildXPlugin.csproj b/src/Scanner/StellaOps.Scanner.Sbomer.BuildXPlugin/StellaOps.Scanner.Sbomer.BuildXPlugin.csproj index 11178c1e4..fdeb88d46 100644 --- a/src/Scanner/StellaOps.Scanner.Sbomer.BuildXPlugin/StellaOps.Scanner.Sbomer.BuildXPlugin.csproj +++ b/src/Scanner/StellaOps.Scanner.Sbomer.BuildXPlugin/StellaOps.Scanner.Sbomer.BuildXPlugin.csproj @@ -21,11 +21,13 @@ + + diff --git a/src/Scanner/StellaOps.Scanner.Sbomer.BuildXPlugin/Surface/SurfaceManifestWriter.cs b/src/Scanner/StellaOps.Scanner.Sbomer.BuildXPlugin/Surface/SurfaceManifestWriter.cs index 082d5d206..79c111412 100644 --- a/src/Scanner/StellaOps.Scanner.Sbomer.BuildXPlugin/Surface/SurfaceManifestWriter.cs +++ b/src/Scanner/StellaOps.Scanner.Sbomer.BuildXPlugin/Surface/SurfaceManifestWriter.cs @@ -2,10 +2,12 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; using System.IO; +using System.Text.Encodings.Web; using System.Text.Json; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; +using StellaOps.Canonical.Json; using StellaOps.Cryptography; using StellaOps.Scanner.Surface.FS; @@ -16,7 +18,8 @@ internal sealed class SurfaceManifestWriter private static readonly JsonSerializerOptions ManifestSerializerOptions = new(JsonSerializerDefaults.Web) { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - WriteIndented = false + WriteIndented = false, + Encoder = JavaScriptEncoder.Default }; private readonly TimeProvider _timeProvider; @@ -54,7 +57,7 @@ internal sealed class SurfaceManifestWriter ? null : options.ComponentVersion.Trim(); var workerInstance = string.IsNullOrWhiteSpace(options.WorkerInstance) - ? Environment.MachineName + ? component : options.WorkerInstance.Trim(); var attempt = options.Attempt <= 0 ? 1 : options.Attempt; var scanId = string.IsNullOrWhiteSpace(options.ScanId) @@ -129,7 +132,7 @@ internal sealed class SurfaceManifestWriter Artifacts = orderedArtifacts }; - var manifestBytes = JsonSerializer.SerializeToUtf8Bytes(manifestDocument, ManifestSerializerOptions); + var manifestBytes = CanonJson.Canonicalize(manifestDocument, ManifestSerializerOptions); var manifestDigest = SurfaceCasLayout.ComputeDigest(_hash, manifestBytes); var manifestKey = SurfaceCasLayout.BuildObjectKey(rootPrefix, SurfaceCasKind.Manifest, manifestDigest); var manifestPath = await SurfaceCasLayout.WriteBytesAsync(cacheRoot, manifestKey, manifestBytes, cancellationToken).ConfigureAwait(false); diff --git a/src/Scanner/StellaOps.Scanner.WebService/Domain/ScanId.cs b/src/Scanner/StellaOps.Scanner.WebService/Domain/ScanId.cs index 9660727a8..bb0d915a3 100644 --- a/src/Scanner/StellaOps.Scanner.WebService/Domain/ScanId.cs +++ b/src/Scanner/StellaOps.Scanner.WebService/Domain/ScanId.cs @@ -1,11 +1,17 @@ +using StellaOps.Determinism; + namespace StellaOps.Scanner.WebService.Domain; public readonly record struct ScanId(string Value) { /// - /// Creates a new ScanId with a random GUID value. + /// Creates a new ScanId with a provided GUID generator. /// - public static ScanId New() => new(Guid.NewGuid().ToString("D")); + public static ScanId New(IGuidProvider guidProvider) + { + ArgumentNullException.ThrowIfNull(guidProvider); + return new ScanId(guidProvider.NewGuid().ToString("D")); + } public override string ToString() => Value; diff --git a/src/Scanner/StellaOps.Scanner.WebService/Endpoints/HealthEndpoints.cs b/src/Scanner/StellaOps.Scanner.WebService/Endpoints/HealthEndpoints.cs index b9ca0a60c..d86a36b38 100644 --- a/src/Scanner/StellaOps.Scanner.WebService/Endpoints/HealthEndpoints.cs +++ b/src/Scanner/StellaOps.Scanner.WebService/Endpoints/HealthEndpoints.cs @@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using StellaOps.Policy; using StellaOps.Scanner.WebService.Diagnostics; using StellaOps.Scanner.WebService.Options; using StellaOps.Scanner.Surface.Env; @@ -26,12 +27,12 @@ internal static class HealthEndpoints group.MapGet("/healthz", HandleHealth) .WithName("scanner.health") .Produces(StatusCodes.Status200OK) - .AllowAnonymous(); + .RequireAuthorization(ScannerPolicies.ScansRead); group.MapGet("/readyz", HandleReady) .WithName("scanner.ready") .Produces(StatusCodes.Status200OK) - .AllowAnonymous(); + .RequireAuthorization(ScannerPolicies.ScansRead); } private static IResult HandleHealth( diff --git a/src/Scanner/StellaOps.Scanner.WebService/Endpoints/PolicyEndpoints.cs b/src/Scanner/StellaOps.Scanner.WebService/Endpoints/PolicyEndpoints.cs index fa268610b..03248beb2 100644 --- a/src/Scanner/StellaOps.Scanner.WebService/Endpoints/PolicyEndpoints.cs +++ b/src/Scanner/StellaOps.Scanner.WebService/Endpoints/PolicyEndpoints.cs @@ -20,6 +20,7 @@ using RuntimePolicyVerdict = StellaOps.Zastava.Core.Contracts.PolicyVerdict; namespace StellaOps.Scanner.WebService.Endpoints; +// Suppress ASPDEPR002 for current minimal API route usage; revisit during endpoint modernization. #pragma warning disable ASPDEPR002 internal static class PolicyEndpoints diff --git a/src/Scanner/StellaOps.Scanner.WebService/Endpoints/ReportEndpoints.cs b/src/Scanner/StellaOps.Scanner.WebService/Endpoints/ReportEndpoints.cs index 097d2b8b5..56176b783 100644 --- a/src/Scanner/StellaOps.Scanner.WebService/Endpoints/ReportEndpoints.cs +++ b/src/Scanner/StellaOps.Scanner.WebService/Endpoints/ReportEndpoints.cs @@ -16,6 +16,7 @@ using StellaOps.Scanner.WebService.Services; namespace StellaOps.Scanner.WebService.Endpoints; +// Suppress ASPDEPR002 for current minimal API route usage; revisit during endpoint modernization. #pragma warning disable ASPDEPR002 internal static class ReportEndpoints diff --git a/src/Scanner/StellaOps.Scanner.WebService/Endpoints/WebhookEndpoints.cs b/src/Scanner/StellaOps.Scanner.WebService/Endpoints/WebhookEndpoints.cs index 719ae01e4..63dc9decf 100644 --- a/src/Scanner/StellaOps.Scanner.WebService/Endpoints/WebhookEndpoints.cs +++ b/src/Scanner/StellaOps.Scanner.WebService/Endpoints/WebhookEndpoints.cs @@ -134,45 +134,51 @@ internal static class WebhookEndpoints StatusCodes.Status400BadRequest); } + if (string.IsNullOrWhiteSpace(source.WebhookSecretRef)) + { + logger.LogWarning("Webhook secret not configured for source {SourceId}", sourceId); + return ProblemResultFactory.Create( + context, + ProblemTypes.Authentication, + "Webhook secret is not configured", + StatusCodes.Status401Unauthorized); + } + // Determine signature to use var signature = signatureSha256 ?? signatureSha1 ?? gitlabToken ?? ExtractBearerToken(authorization); - // Verify signature if source has a webhook secret reference - if (!string.IsNullOrEmpty(source.WebhookSecretRef)) + if (string.IsNullOrEmpty(signature)) { - if (string.IsNullOrEmpty(signature)) - { - logger.LogWarning("Webhook received without signature for source {SourceId}", sourceId); - return ProblemResultFactory.Create( - context, - ProblemTypes.Authentication, - "Missing webhook signature", - StatusCodes.Status401Unauthorized); - } + logger.LogWarning("Webhook received without signature for source {SourceId}", sourceId); + return ProblemResultFactory.Create( + context, + ProblemTypes.Authentication, + "Missing webhook signature", + StatusCodes.Status401Unauthorized); + } - // Resolve the webhook secret from the credential store - var secretCredential = await credentialResolver.ResolveAsync(source.WebhookSecretRef, ct); - var webhookSecret = secretCredential?.Token ?? secretCredential?.Password; + // Resolve the webhook secret from the credential store + var secretCredential = await credentialResolver.ResolveAsync(source.WebhookSecretRef, ct); + var webhookSecret = secretCredential?.Token ?? secretCredential?.Password; - if (string.IsNullOrEmpty(webhookSecret)) - { - logger.LogWarning("Failed to resolve webhook secret for source {SourceId}", sourceId); - return ProblemResultFactory.Create( - context, - ProblemTypes.InternalError, - "Failed to resolve webhook secret", - StatusCodes.Status500InternalServerError); - } + if (string.IsNullOrEmpty(webhookSecret)) + { + logger.LogWarning("Failed to resolve webhook secret for source {SourceId}", sourceId); + return ProblemResultFactory.Create( + context, + ProblemTypes.InternalError, + "Failed to resolve webhook secret", + StatusCodes.Status500InternalServerError); + } - if (!webhookHandler.VerifyWebhookSignature(payloadBytes, signature, webhookSecret)) - { - logger.LogWarning("Invalid webhook signature for source {SourceId}", sourceId); - return ProblemResultFactory.Create( - context, - ProblemTypes.Authentication, - "Invalid webhook signature", - StatusCodes.Status401Unauthorized); - } + if (!webhookHandler.VerifyWebhookSignature(payloadBytes, signature, webhookSecret)) + { + logger.LogWarning("Invalid webhook signature for source {SourceId}", sourceId); + return ProblemResultFactory.Create( + context, + ProblemTypes.Authentication, + "Invalid webhook signature", + StatusCodes.Status401Unauthorized); } // Parse the payload @@ -446,6 +452,16 @@ internal static class WebhookEndpoints StatusCodes.Status400BadRequest); } + if (string.IsNullOrWhiteSpace(source.WebhookSecretRef)) + { + logger.LogWarning("Webhook secret not configured for source {SourceId}", source.SourceId); + return ProblemResultFactory.Create( + context, + ProblemTypes.Authentication, + "Webhook secret is not configured", + StatusCodes.Status401Unauthorized); + } + // Get signature from header string? signature = signatureHeader switch { @@ -456,42 +472,38 @@ internal static class WebhookEndpoints _ => null }; - // Verify signature if source has a webhook secret reference - if (!string.IsNullOrEmpty(source.WebhookSecretRef)) + if (string.IsNullOrEmpty(signature)) { - if (string.IsNullOrEmpty(signature)) - { - logger.LogWarning("Webhook received without signature for source {SourceId}", source.SourceId); - return ProblemResultFactory.Create( - context, - ProblemTypes.Authentication, - "Missing webhook signature", - StatusCodes.Status401Unauthorized); - } + logger.LogWarning("Webhook received without signature for source {SourceId}", source.SourceId); + return ProblemResultFactory.Create( + context, + ProblemTypes.Authentication, + "Missing webhook signature", + StatusCodes.Status401Unauthorized); + } - // Resolve the webhook secret from the credential store - var secretCredential = await credentialResolver.ResolveAsync(source.WebhookSecretRef, ct); - var webhookSecret = secretCredential?.Token ?? secretCredential?.Password; + // Resolve the webhook secret from the credential store + var secretCredential = await credentialResolver.ResolveAsync(source.WebhookSecretRef, ct); + var webhookSecret = secretCredential?.Token ?? secretCredential?.Password; - if (string.IsNullOrEmpty(webhookSecret)) - { - logger.LogWarning("Failed to resolve webhook secret for source {SourceId}", source.SourceId); - return ProblemResultFactory.Create( - context, - ProblemTypes.InternalError, - "Failed to resolve webhook secret", - StatusCodes.Status500InternalServerError); - } + if (string.IsNullOrEmpty(webhookSecret)) + { + logger.LogWarning("Failed to resolve webhook secret for source {SourceId}", source.SourceId); + return ProblemResultFactory.Create( + context, + ProblemTypes.InternalError, + "Failed to resolve webhook secret", + StatusCodes.Status500InternalServerError); + } - if (!webhookHandler.VerifyWebhookSignature(payloadBytes, signature, webhookSecret)) - { - logger.LogWarning("Invalid webhook signature for source {SourceId}", source.SourceId); - return ProblemResultFactory.Create( - context, - ProblemTypes.Authentication, - "Invalid webhook signature", - StatusCodes.Status401Unauthorized); - } + if (!webhookHandler.VerifyWebhookSignature(payloadBytes, signature, webhookSecret)) + { + logger.LogWarning("Invalid webhook signature for source {SourceId}", source.SourceId); + return ProblemResultFactory.Create( + context, + ProblemTypes.Authentication, + "Invalid webhook signature", + StatusCodes.Status401Unauthorized); } // Parse the payload diff --git a/src/Scanner/StellaOps.Scanner.WebService/Options/ScannerSurfaceSecretConfigurator.cs b/src/Scanner/StellaOps.Scanner.WebService/Options/ScannerSurfaceSecretConfigurator.cs index 1a009ebfe..11ad2a612 100644 --- a/src/Scanner/StellaOps.Scanner.WebService/Options/ScannerSurfaceSecretConfigurator.cs +++ b/src/Scanner/StellaOps.Scanner.WebService/Options/ScannerSurfaceSecretConfigurator.cs @@ -44,7 +44,7 @@ internal sealed class ScannerSurfaceSecretConfigurator : IConfigureOptions(); builder.Services.AddHttpContextAccessor(); diff --git a/src/Scanner/StellaOps.Scanner.WebService/Serialization/OrchestratorEventSerializer.cs b/src/Scanner/StellaOps.Scanner.WebService/Serialization/OrchestratorEventSerializer.cs index 5511e10d2..303e8f232 100644 --- a/src/Scanner/StellaOps.Scanner.WebService/Serialization/OrchestratorEventSerializer.cs +++ b/src/Scanner/StellaOps.Scanner.WebService/Serialization/OrchestratorEventSerializer.cs @@ -1,32 +1,41 @@ using System; using System.Collections.Immutable; using System.Linq; +using System.Text; using System.Text.Encodings.Web; using System.Text.Json; using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; +using StellaOps.Canonical.Json; using StellaOps.Scanner.WebService.Contracts; namespace StellaOps.Scanner.WebService.Serialization; internal static class OrchestratorEventSerializer { - private static readonly JsonSerializerOptions CompactOptions = CreateOptions(writeIndented: false); - private static readonly JsonSerializerOptions PrettyOptions = CreateOptions(writeIndented: true); + private static readonly JsonSerializerOptions CanonicalOptions = CreateOptions(); + private static readonly JsonSerializerOptions PrettyOptions = new() + { + WriteIndented = true, + Encoder = JavaScriptEncoder.Default + }; public static string Serialize(OrchestratorEvent @event) - => JsonSerializer.Serialize(@event, CompactOptions); + => Encoding.UTF8.GetString(CanonJson.Canonicalize(@event, CanonicalOptions)); public static string SerializeIndented(OrchestratorEvent @event) - => JsonSerializer.Serialize(@event, PrettyOptions); + { + var canonicalBytes = CanonJson.Canonicalize(@event, CanonicalOptions); + using var document = JsonDocument.Parse(canonicalBytes); + return JsonSerializer.Serialize(document.RootElement, PrettyOptions); + } - private static JsonSerializerOptions CreateOptions(bool writeIndented) + private static JsonSerializerOptions CreateOptions() { var options = new JsonSerializerOptions(JsonSerializerDefaults.Web) { - WriteIndented = writeIndented, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping + Encoder = JavaScriptEncoder.Default }; var baselineResolver = options.TypeInfoResolver ?? new DefaultJsonTypeInfoResolver(); diff --git a/src/Scanner/StellaOps.Scanner.WebService/Services/HumanApprovalAttestationService.cs b/src/Scanner/StellaOps.Scanner.WebService/Services/HumanApprovalAttestationService.cs index 145fa0c96..03828d5ab 100644 --- a/src/Scanner/StellaOps.Scanner.WebService/Services/HumanApprovalAttestationService.cs +++ b/src/Scanner/StellaOps.Scanner.WebService/Services/HumanApprovalAttestationService.cs @@ -15,6 +15,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using StellaOps.Determinism; using StellaOps.Scanner.WebService.Contracts; using StellaOps.Scanner.WebService.Domain; @@ -28,6 +29,7 @@ public sealed class HumanApprovalAttestationService : IHumanApprovalAttestationS private readonly ILogger _logger; private readonly HumanApprovalAttestationOptions _options; private readonly TimeProvider _timeProvider; + private readonly IGuidProvider _guidProvider; /// /// In-memory attestation store. In production, this would be backed by a database. @@ -41,10 +43,12 @@ public sealed class HumanApprovalAttestationService : IHumanApprovalAttestationS public HumanApprovalAttestationService( ILogger logger, IOptions options, + IGuidProvider guidProvider, TimeProvider timeProvider) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _options = options?.Value ?? throw new ArgumentNullException(nameof(options)); + _guidProvider = guidProvider ?? throw new ArgumentNullException(nameof(guidProvider)); _timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider)); } @@ -79,7 +83,7 @@ public sealed class HumanApprovalAttestationService : IHumanApprovalAttestationS var ttl = input.ApprovalTtl ?? TimeSpan.FromDays(_options.DefaultApprovalTtlDays); var expiresAt = now.Add(ttl); - var approvalId = $"approval-{Guid.NewGuid():N}"; + var approvalId = $"approval-{_guidProvider.NewGuid():N}"; var statement = BuildStatement(input, approvalId, now, expiresAt); var attestationId = ComputeAttestationId(statement); diff --git a/src/Scanner/StellaOps.Scanner.WebService/Services/LayerSbomService.cs b/src/Scanner/StellaOps.Scanner.WebService/Services/LayerSbomService.cs index b53f3e317..685b2b8fa 100644 --- a/src/Scanner/StellaOps.Scanner.WebService/Services/LayerSbomService.cs +++ b/src/Scanner/StellaOps.Scanner.WebService/Services/LayerSbomService.cs @@ -15,12 +15,14 @@ namespace StellaOps.Scanner.WebService.Services; public sealed class LayerSbomService : ILayerSbomService { private readonly ICompositionRecipeService _recipeService; + private readonly TimeProvider _timeProvider; // In-memory cache for layer SBOMs (would be replaced with CAS in production) private static readonly ConcurrentDictionary LayerSbomCache = new(StringComparer.Ordinal); - public LayerSbomService(ICompositionRecipeService? recipeService = null) + public LayerSbomService(TimeProvider timeProvider, ICompositionRecipeService? recipeService = null) { + _timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider)); _recipeService = recipeService ?? new CompositionRecipeService(); } @@ -166,7 +168,7 @@ public sealed class LayerSbomService : ILayerSbomService { ScanId = scanId, ImageDigest = imageDigest, - CreatedAt = DateTimeOffset.UtcNow.ToString("O", CultureInfo.InvariantCulture), + CreatedAt = _timeProvider.GetUtcNow().ToString("O", CultureInfo.InvariantCulture), Recipe = new CompositionRecipe { Version = "1.0.0", diff --git a/src/Scanner/StellaOps.Scanner.WebService/Services/NullGitHubCodeScanningService.cs b/src/Scanner/StellaOps.Scanner.WebService/Services/NullGitHubCodeScanningService.cs index 845f9337f..b87c4f651 100644 --- a/src/Scanner/StellaOps.Scanner.WebService/Services/NullGitHubCodeScanningService.cs +++ b/src/Scanner/StellaOps.Scanner.WebService/Services/NullGitHubCodeScanningService.cs @@ -2,6 +2,7 @@ // Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later. // +using StellaOps.Determinism; using StellaOps.Scanner.WebService.Endpoints; namespace StellaOps.Scanner.WebService.Services; @@ -13,6 +14,13 @@ namespace StellaOps.Scanner.WebService.Services; /// internal sealed class NullGitHubCodeScanningService : IGitHubCodeScanningService { + private readonly IGuidProvider _guidProvider; + + public NullGitHubCodeScanningService(IGuidProvider guidProvider) + { + _guidProvider = guidProvider ?? throw new ArgumentNullException(nameof(guidProvider)); + } + public Task UploadSarifAsync( string owner, string repo, @@ -24,7 +32,7 @@ internal sealed class NullGitHubCodeScanningService : IGitHubCodeScanningService // Return a mock result for development/testing return Task.FromResult(new GitHubUploadResult { - SarifId = $"mock-sarif-{Guid.NewGuid():N}", + SarifId = $"mock-sarif-{_guidProvider.NewGuid():N}", Url = null }); } diff --git a/src/Scanner/StellaOps.Scanner.WebService/Services/OfflineAttestationVerifier.cs b/src/Scanner/StellaOps.Scanner.WebService/Services/OfflineAttestationVerifier.cs index 392c3b5cb..2caacc4ea 100644 --- a/src/Scanner/StellaOps.Scanner.WebService/Services/OfflineAttestationVerifier.cs +++ b/src/Scanner/StellaOps.Scanner.WebService/Services/OfflineAttestationVerifier.cs @@ -12,6 +12,7 @@ using System.Text; using System.Text.Json; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using StellaOps.Attestation; using StellaOps.Scanner.WebService.Contracts; using StellaOps.Scanner.WebService.Domain; @@ -186,7 +187,7 @@ public sealed class OfflineAttestationVerifier : IOfflineAttestationVerifier } // Compute PAE (Pre-Authentication Encoding) per DSSE spec - var pae = ComputePae(envelope.PayloadType, payloadBytes); + var pae = DsseHelper.PreAuthenticationEncoding(envelope.PayloadType, payloadBytes); // Try to verify at least one signature foreach (var sig in envelope.Signatures) @@ -587,29 +588,6 @@ public sealed class OfflineAttestationVerifier : IOfflineAttestationVerifier } } - private static byte[] ComputePae(string payloadType, byte[] payload) - { - // Pre-Authentication Encoding per DSSE spec: - // PAE(type, body) = "DSSEv1" + SP + LEN(type) + SP + type + SP + LEN(body) + SP + body - const string DssePrefix = "DSSEv1"; - var typeBytes = Encoding.UTF8.GetBytes(payloadType); - - using var ms = new MemoryStream(); - using var writer = new BinaryWriter(ms); - - writer.Write(Encoding.UTF8.GetBytes(DssePrefix)); - writer.Write((byte)' '); - writer.Write(BitConverter.GetBytes((long)typeBytes.Length)); - writer.Write((byte)' '); - writer.Write(typeBytes); - writer.Write((byte)' '); - writer.Write(BitConverter.GetBytes((long)payload.Length)); - writer.Write((byte)' '); - writer.Write(payload); - - return ms.ToArray(); - } - private static string? ExtractSignerIdentity(X509Certificate2 cert) { // Try to get SAN (Subject Alternative Name) email diff --git a/src/Scanner/StellaOps.Scanner.WebService/Services/OfflineKitImportService.cs b/src/Scanner/StellaOps.Scanner.WebService/Services/OfflineKitImportService.cs index ec7bc6d76..55c5eccda 100644 --- a/src/Scanner/StellaOps.Scanner.WebService/Services/OfflineKitImportService.cs +++ b/src/Scanner/StellaOps.Scanner.WebService/Services/OfflineKitImportService.cs @@ -5,6 +5,7 @@ using System.Text.Json; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using StellaOps.Attestation; using StellaOps.AirGap.Importer.Contracts; using StellaOps.AirGap.Importer.Validation; using StellaOps.Authority.Persistence.Postgres.Models; @@ -257,7 +258,8 @@ internal sealed class OfflineKitImportService } var trustRoots = BuildTrustRoots(resolution, options.TrustRootDirectory ?? string.Empty); - var pae = BuildPreAuthEncoding(envelope.PayloadType, envelope.Payload); + var payloadBytes = Convert.FromBase64String(envelope.Payload); + var pae = DsseHelper.PreAuthenticationEncoding(envelope.PayloadType, payloadBytes); var verified = 0; foreach (var signature in envelope.Signatures) @@ -310,26 +312,6 @@ internal sealed class OfflineKitImportService PublicKeys: publicKeys); } - private static byte[] BuildPreAuthEncoding(string payloadType, string payloadBase64) - { - const string paePrefix = "DSSEv1"; - var payloadBytes = Convert.FromBase64String(payloadBase64); - var parts = new[] { paePrefix, payloadType, Encoding.UTF8.GetString(payloadBytes) }; - - var paeBuilder = new StringBuilder(); - paeBuilder.Append("PAE:"); - paeBuilder.Append(parts.Length); - foreach (var part in parts) - { - paeBuilder.Append(' '); - paeBuilder.Append(part.Length); - paeBuilder.Append(' '); - paeBuilder.Append(part); - } - - return Encoding.UTF8.GetBytes(paeBuilder.ToString()); - } - private static bool TryVerifySignature(TrustRootConfig trustRoots, DsseSignature signature, byte[] pae) { if (!trustRoots.PublicKeys.TryGetValue(signature.KeyId, out var keyBytes)) diff --git a/src/Scanner/StellaOps.Scanner.WebService/Services/ReportEventDispatcher.cs b/src/Scanner/StellaOps.Scanner.WebService/Services/ReportEventDispatcher.cs index 9456a250d..880a7ae91 100644 --- a/src/Scanner/StellaOps.Scanner.WebService/Services/ReportEventDispatcher.cs +++ b/src/Scanner/StellaOps.Scanner.WebService/Services/ReportEventDispatcher.cs @@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using StellaOps.Auth.Abstractions; +using StellaOps.Determinism; using StellaOps.Policy; using StellaOps.Scanner.Core.Utility; using StellaOps.Scanner.Storage.Models; @@ -29,6 +30,7 @@ internal sealed class ReportEventDispatcher : IReportEventDispatcher private readonly IPlatformEventPublisher _publisher; private readonly IClassificationChangeTracker _classificationChangeTracker; + private readonly IGuidProvider _guidProvider; private readonly TimeProvider _timeProvider; private readonly ILogger _logger; private readonly string[] _apiBaseSegments; @@ -43,11 +45,13 @@ internal sealed class ReportEventDispatcher : IReportEventDispatcher IPlatformEventPublisher publisher, IClassificationChangeTracker classificationChangeTracker, IOptions options, + IGuidProvider guidProvider, TimeProvider timeProvider, ILogger logger) { _publisher = publisher ?? throw new ArgumentNullException(nameof(publisher)); _classificationChangeTracker = classificationChangeTracker ?? throw new ArgumentNullException(nameof(classificationChangeTracker)); + _guidProvider = guidProvider ?? throw new ArgumentNullException(nameof(guidProvider)); if (options is null) { throw new ArgumentNullException(nameof(options)); @@ -102,7 +106,7 @@ internal sealed class ReportEventDispatcher : IReportEventDispatcher var reportEvent = new OrchestratorEvent { - EventId = Guid.NewGuid(), + EventId = _guidProvider.NewGuid(), Kind = OrchestratorEventKinds.ScannerReportReady, Version = 1, Tenant = tenant, @@ -124,7 +128,7 @@ internal sealed class ReportEventDispatcher : IReportEventDispatcher var scanCompletedEvent = new OrchestratorEvent { - EventId = Guid.NewGuid(), + EventId = _guidProvider.NewGuid(), Kind = OrchestratorEventKinds.ScannerScanCompleted, Version = 1, Tenant = tenant, diff --git a/src/Scanner/StellaOps.Scanner.WebService/Services/SurfacePointerService.cs b/src/Scanner/StellaOps.Scanner.WebService/Services/SurfacePointerService.cs index 3a04cbc6b..c74ef435a 100644 --- a/src/Scanner/StellaOps.Scanner.WebService/Services/SurfacePointerService.cs +++ b/src/Scanner/StellaOps.Scanner.WebService/Services/SurfacePointerService.cs @@ -3,10 +3,12 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Security.Cryptography; +using System.Text.Encodings.Web; using System.Text.Json; using System.Text.Json.Serialization; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using StellaOps.Canonical.Json; using StellaOps.Cryptography; using StellaOps.Scanner.Storage; using StellaOps.Scanner.Storage.Catalog; @@ -29,7 +31,8 @@ internal sealed class SurfacePointerService : ISurfacePointerService private static readonly JsonSerializerOptions ManifestSerializerOptions = new(JsonSerializerDefaults.Web) { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - WriteIndented = false + WriteIndented = false, + Encoder = JavaScriptEncoder.Default }; private readonly LinkRepository _linkRepository; @@ -152,7 +155,7 @@ internal sealed class SurfacePointerService : ISurfacePointerService Artifacts = orderedArtifacts }; - var manifestJson = JsonSerializer.SerializeToUtf8Bytes(manifest, ManifestSerializerOptions); + var manifestJson = CanonJson.Canonicalize(manifest, ManifestSerializerOptions); var manifestDigest = ComputeDigest(manifestJson); var manifestUri = BuildManifestUri(bucket, rootPrefix, tenant, manifestDigest); diff --git a/src/Scanner/StellaOps.Scanner.WebService/Services/TriageStatusService.cs b/src/Scanner/StellaOps.Scanner.WebService/Services/TriageStatusService.cs index 984e3c2b2..9cafc76c4 100644 --- a/src/Scanner/StellaOps.Scanner.WebService/Services/TriageStatusService.cs +++ b/src/Scanner/StellaOps.Scanner.WebService/Services/TriageStatusService.cs @@ -5,6 +5,7 @@ // ----------------------------------------------------------------------------- using Microsoft.Extensions.Logging; +using StellaOps.Determinism; using StellaOps.Policy.Counterfactuals; using StellaOps.Scanner.Triage.Entities; using StellaOps.Scanner.WebService.Contracts; @@ -21,15 +22,18 @@ public sealed class TriageStatusService : ITriageStatusService private readonly ITriageQueryService _queryService; private readonly ICounterfactualEngine? _counterfactualEngine; private readonly TimeProvider _timeProvider; + private readonly IGuidProvider _guidProvider; public TriageStatusService( ILogger logger, ITriageQueryService queryService, + IGuidProvider guidProvider, TimeProvider timeProvider, ICounterfactualEngine? counterfactualEngine = null) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _queryService = queryService ?? throw new ArgumentNullException(nameof(queryService)); + _guidProvider = guidProvider ?? throw new ArgumentNullException(nameof(guidProvider)); _timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider)); _counterfactualEngine = counterfactualEngine; } @@ -85,7 +89,7 @@ public sealed class TriageStatusService : ITriageStatusService NewLane = newLane, PreviousVerdict = previousVerdict, NewVerdict = newVerdict, - SnapshotId = $"snap-{Guid.NewGuid():N}", + SnapshotId = $"snap-{_guidProvider.NewGuid():N}", AppliedAt = _timeProvider.GetUtcNow() }; } @@ -105,7 +109,7 @@ public sealed class TriageStatusService : ITriageStatusService } var previousVerdict = GetCurrentVerdict(finding); - var vexStatementId = $"vex-{Guid.NewGuid():N}"; + var vexStatementId = $"vex-{_guidProvider.NewGuid():N}"; // Determine if verdict changes based on VEX status var verdictChanged = false; diff --git a/src/Scanner/StellaOps.Scanner.WebService/StellaOps.Scanner.WebService.csproj b/src/Scanner/StellaOps.Scanner.WebService/StellaOps.Scanner.WebService.csproj index a6c21ff7e..77e549095 100644 --- a/src/Scanner/StellaOps.Scanner.WebService/StellaOps.Scanner.WebService.csproj +++ b/src/Scanner/StellaOps.Scanner.WebService/StellaOps.Scanner.WebService.csproj @@ -34,6 +34,7 @@ + diff --git a/src/Scanner/StellaOps.Scanner.WebService/TASKS.md b/src/Scanner/StellaOps.Scanner.WebService/TASKS.md new file mode 100644 index 000000000..e3a8407dc --- /dev/null +++ b/src/Scanner/StellaOps.Scanner.WebService/TASKS.md @@ -0,0 +1,11 @@ +# Scanner WebService Task Board + +This board tracks TODO/FIXME/HACK markers and audit follow-ups for this module. +Source of truth: `docs/implplan/SPRINT_20260112_003_BE_csproj_audit_pending_apply.md`. + +| Task ID | Status | Notes | +| --- | --- | --- | +| TODO-WEB-001 | TODO | Load tenant-specific policy configuration in `src/Scanner/StellaOps.Scanner.WebService/Services/VexGateQueryService.cs`. | +| TODO-WEB-002 | TODO | Implement CAS retrieval for slices in `src/Scanner/StellaOps.Scanner.WebService/Services/SliceQueryService.cs`. | +| TODO-WEB-003 | TODO | Add VEX expiry once integrated in `src/Scanner/StellaOps.Scanner.WebService/Services/EvidenceCompositionService.cs`. | +| PRAGMA-WEB-001 | DONE | Documented ASPDEPR002 suppressions in `src/Scanner/StellaOps.Scanner.WebService/Endpoints/ReportEndpoints.cs`, `src/Scanner/StellaOps.Scanner.WebService/Endpoints/PolicyEndpoints.cs`, and `src/Scanner/StellaOps.Scanner.WebService/Endpoints/EpssEndpoints.cs`. | diff --git a/src/Scanner/StellaOps.Scanner.sln b/src/Scanner/StellaOps.Scanner.sln index 91db339e6..642f8a240 100644 --- a/src/Scanner/StellaOps.Scanner.sln +++ b/src/Scanner/StellaOps.Scanner.sln @@ -1,1966 +1,3921 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.0.31903.59 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.Native", "StellaOps.Scanner.Analyzers.Native", "{83CDC626-3D1B-02BE-1DE9-82175D01430B}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Sbomer.BuildXPlugin", "StellaOps.Scanner.Sbomer.BuildXPlugin", "{513870EC-4723-146F-A8F5-D6A7981C32B5}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.WebService", "StellaOps.Scanner.WebService", "{BE1D16DA-78A9-22D8-F49D-94719ECB5132}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Worker", "StellaOps.Scanner.Worker", "{C35AAD13-5F80-85D6-1702-E3E0C55D6A99}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "__Benchmarks", "__Benchmarks", "{EB157E4F-3EA5-5CBF-9694-18E3FA265E53}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.Lang.Deno.Benchmarks", "StellaOps.Scanner.Analyzers.Lang.Deno.Benchmarks", "{1B2C9807-0067-AAD6-69CD-7FD799689BDD}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.Lang.Php.Benchmarks", "StellaOps.Scanner.Analyzers.Lang.Php.Benchmarks", "{4B16E448-1B2D-28B7-2417-D2D191FE524F}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.Lang.Rust.Benchmarks", "StellaOps.Scanner.Analyzers.Lang.Rust.Benchmarks", "{9EAD58C8-4DCD-4933-B9F9-7D211C66D73F}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Storage.Epss.Perf", "StellaOps.Scanner.Storage.Epss.Perf", "{0E5FA5F8-5C99-7868-B1D0-1D38A9624795}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "__External", "__External", "{5B52EF8A-3661-DCFF-797D-BC4D6AC60BDA}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AirGap", "AirGap", "{F310596E-88BB-9E54-885E-21C61971917E}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.AirGap.Importer", "StellaOps.AirGap.Importer", "{EA6E5683-3A20-2E52-1CE6-AE0D6D36AC4D}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.AirGap.Policy", "StellaOps.AirGap.Policy", "{D9492ED1-A812-924B-65E4-F518592B49BB}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.AirGap.Policy", "StellaOps.AirGap.Policy", "{3823DE1E-2ACE-C956-99E1-00DB786D9E1D}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Aoc", "Aoc", "{03DFF14F-7321-1784-D4C7-4E99D4120F48}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "__Libraries", "__Libraries", "{BDD326D6-7616-84F0-B914-74743BFBA520}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Aoc", "StellaOps.Aoc", "{EC506DBE-AB6D-492E-786E-8B176021BF2E}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Attestor", "Attestor", "{5AC09D9A-F2A5-9CFA-B3C5-8D25F257651C}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Attestor", "StellaOps.Attestor", "{33B1AE27-692A-1778-48C1-CCEC2B9BC78F}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Attestor.Envelope", "StellaOps.Attestor.Envelope", "{018E0E11-1CCE-A2BE-641D-21EE14D2E90D}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Attestor.Core", "StellaOps.Attestor.Core", "{5F27FB4E-CF09-3A6B-F5B4-BF5A709FA609}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "__Libraries", "__Libraries", "{AB67BDB9-D701-3AC9-9CDF-ECCDCCD8DB6D}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Attestor.GraphRoot", "StellaOps.Attestor.GraphRoot", "{3F605548-87E2-8A1D-306D-0CE6960B8242}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Attestor.ProofChain", "StellaOps.Attestor.ProofChain", "{45F7FA87-7451-6970-7F6E-F8BAE45E081B}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Authority", "Authority", "{C1DCEFBD-12A5-EAAE-632E-8EEB9BE491B6}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Authority", "StellaOps.Authority", "{A6928CBA-4D4D-AB2B-CA19-FEE6E73ECE70}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Auth.Abstractions", "StellaOps.Auth.Abstractions", "{F2E6CB0E-DF77-1FAA-582B-62B040DF3848}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Auth.Client", "StellaOps.Auth.Client", "{C494ECBE-DEA5-3576-D2AF-200FF12BC144}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Auth.ServerIntegration", "StellaOps.Auth.ServerIntegration", "{7E890DF9-B715-B6DF-2498-FD74DDA87D71}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Authority.Plugins.Abstractions", "StellaOps.Authority.Plugins.Abstractions", "{64689413-46D7-8499-68A6-B6367ACBC597}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "__Libraries", "__Libraries", "{5827F4DE-0AA7-FC85-641D-09E3D890DB27}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Authority.Core", "StellaOps.Authority.Core", "{9BD75659-58CB-06D1-E198-C39007E82C6A}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Authority.Persistence", "StellaOps.Authority.Persistence", "{7BF13935-F1DD-D23B-8347-DB1550C69D69}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "BinaryIndex", "BinaryIndex", "{2949F2E7-0E10-76D0-5672-8B1662588F74}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "__Libraries", "__Libraries", "{3ADE81EE-6F71-CB5C-016B-36E8AC854713}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.BinaryIndex.Contracts", "StellaOps.BinaryIndex.Contracts", "{24D60BC6-6A93-C97D-1238-113DDB928700}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.BinaryIndex.Core", "StellaOps.BinaryIndex.Core", "{CCF230F8-F75D-A766-7EAE-0C9FEF5AF6C2}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.BinaryIndex.Corpus", "StellaOps.BinaryIndex.Corpus", "{066DF6C9-826C-F223-47D2-BDF53D59F6C3}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.BinaryIndex.Fingerprints", "StellaOps.BinaryIndex.Fingerprints", "{61C7FDA3-83AA-3EE6-6321-1C1ACD1073DF}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.BinaryIndex.FixIndex", "StellaOps.BinaryIndex.FixIndex", "{BF6C9274-4DBD-2FDE-B94C-1B208F6C53BC}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.BinaryIndex.Persistence", "StellaOps.BinaryIndex.Persistence", "{4EAAC62E-EBD2-DFF1-7B37-7E131C75DEC3}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Concelier", "Concelier", "{157C3671-CA0B-69FA-A7C9-74A1FDA97B99}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "__Libraries", "__Libraries", "{F39E09D6-BF93-B64A-CFE7-2BA92815C0FE}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Concelier.Cache.Valkey", "StellaOps.Concelier.Cache.Valkey", "{39EFDA5B-F5EE-8212-D5BA-90E1B82013E7}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Concelier.Connector.Common", "StellaOps.Concelier.Connector.Common", "{3B82DBF3-3DAE-EA97-85F4-6DCFA09940DF}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Concelier.Core", "StellaOps.Concelier.Core", "{6844B539-C2A3-9D4F-139D-9D533BCABADA}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Concelier.Interest", "StellaOps.Concelier.Interest", "{4263AA71-0335-3F44-9A9B-423C3A3D05E6}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Concelier.Merge", "StellaOps.Concelier.Merge", "{F1B1DB47-D2D7-59CB-679B-23E4928E8328}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Concelier.Models", "StellaOps.Concelier.Models", "{BC35DE94-4F04-3436-27A3-F11647FEDD5C}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Concelier.Normalization", "StellaOps.Concelier.Normalization", "{864C8B80-771A-0C15-30A5-558F99006E0D}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Concelier.Persistence", "StellaOps.Concelier.Persistence", "{603E7A23-1D6B-D3A9-B0E6-3E332B13ED5C}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Concelier.ProofService", "StellaOps.Concelier.ProofService", "{D2F7E58B-47D4-5205-D917-144CA1CFF4F1}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Concelier.RawModels", "StellaOps.Concelier.RawModels", "{1DCF4EBB-DBC4-752C-13D4-D1EECE4E8907}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Concelier.SbomIntegration", "StellaOps.Concelier.SbomIntegration", "{1B37A859-E733-60CB-4806-1A24B6F10E05}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Concelier.SourceIntel", "StellaOps.Concelier.SourceIntel", "{F2B58F4E-6F28-A25F-5BFB-CDEBAD6B9A3E}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Excititor", "Excititor", "{7D49FA52-6EA1-EAC8-4C5A-AC07188D6C57}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "__Libraries", "__Libraries", "{C9CF27FC-12DB-954F-863C-576BA8E309A5}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Excititor.Core", "StellaOps.Excititor.Core", "{6DCAF6F3-717F-27A9-D96C-F2BFA5550347}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Feedser", "Feedser", "{C4A90603-BE42-0044-CAB4-3EB910AD51A5}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Feedser.BinaryAnalysis", "StellaOps.Feedser.BinaryAnalysis", "{054761F9-16D3-B2F8-6F4D-EFC2248805CD}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Feedser.Core", "StellaOps.Feedser.Core", "{B54CE64C-4167-1DD1-B7D6-2FD7A5AEF715}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Notify", "Notify", "{D2162FEA-AFA4-2A88-6444-2F6D845260BB}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "__Libraries", "__Libraries", "{63EAEA3B-ADC9-631D-774E-7AA04490EDDD}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Notify.Models", "StellaOps.Notify.Models", "{B0F64757-F7A7-1A11-8DEC-BAC72EB5EC29}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Policy", "Policy", "{8E6B774C-CC4E-CE7C-AD4B-8AF7C92889A6}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Policy.RiskProfile", "StellaOps.Policy.RiskProfile", "{BC12ED55-6015-7C8B-8384-B39CE93C76D6}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "__Libraries", "__Libraries", "{FF70543D-AFF9-1D38-4950-4F8EE18D60BB}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Policy", "StellaOps.Policy", "{831265B0-8896-9C95-3488-E12FD9F6DC53}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Provenance", "Provenance", "{316BBD0A-04D2-85C9-52EA-7993CC6C8930}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Provenance.Attestation", "StellaOps.Provenance.Attestation", "{9D6AB85A-85EA-D85A-5566-A121D34016E6}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Router", "Router", "{FC018E5B-1E2F-DE19-1E97-0C845058C469}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "__Libraries", "__Libraries", "{1BE5B76C-B486-560B-6CB2-44C6537249AA}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Messaging", "StellaOps.Messaging", "{F4F1CBE2-1CDD-CAA4-41F0-266DB4677C05}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Microservice", "StellaOps.Microservice", "{3DE1DCDC-C845-4AC7-7B66-34B0A9E8626B}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Microservice.AspNetCore", "StellaOps.Microservice.AspNetCore", "{6FA01E92-606B-0CB8-8583-6F693A903CFC}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Router.AspNet", "StellaOps.Router.AspNet", "{A5994E92-7E0E-89FE-5628-DE1A0176B8BA}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Router.Common", "StellaOps.Router.Common", "{54C11B29-4C54-7255-AB44-BEB63AF9BD1F}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Signals", "Signals", "{AD65DDE7-9FEA-7380-8C10-FA165F745354}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Signals", "StellaOps.Signals", "{076B8074-5735-5367-1EEA-CA16A5B8ABD7}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Signer", "Signer", "{3247EE0D-B3E9-9C11-B0AE-FE719410390B}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Signer", "StellaOps.Signer", "{CD7C09DA-FEC8-2CC5-D00C-E525638DFF4A}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Signer.Core", "StellaOps.Signer.Core", "{79B10804-91E9-972E-1913-EE0F0B11663E}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Unknowns", "Unknowns", "{A4E2971A-7DCF-D72F-631D-98564D1D1E5D}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "__Libraries", "__Libraries", "{01B2B5BA-58CD-9920-5A1E-6B801BF36685}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Unknowns.Core", "StellaOps.Unknowns.Core", "{3C8A19A6-5578-51E9-E592-6DABBE5739E5}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Zastava", "Zastava", "{8403C362-DC2B-AF41-C9D8-E4F2D892DF8E}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "__Libraries", "__Libraries", "{B75836D4-A244-20B8-4707-64E17C725DAA}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Zastava.Core", "StellaOps.Zastava.Core", "{0910C958-24C8-947F-359A-218ED1199AAE}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "__Libraries", "__Libraries", "{1345DD29-BB3A-FB5F-4B3D-E29F6045A27A}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Auth.Security", "StellaOps.Auth.Security", "{9C2DD234-FA33-FDB6-86F0-EF9B75A13450}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Canonical.Json", "StellaOps.Canonical.Json", "{79E122F4-2325-3E92-438E-5825A307B594}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Configuration", "StellaOps.Configuration", "{538E2D98-5325-3F54-BE74-EFE5FC1ECBD8}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Cryptography", "StellaOps.Cryptography", "{66557252-B5C4-664B-D807-07018C627474}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Cryptography.DependencyInjection", "StellaOps.Cryptography.DependencyInjection", "{7203223D-FF02-7BEB-2798-D1639ACC01C4}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Cryptography.Kms", "StellaOps.Cryptography.Kms", "{5AC9EE40-1881-5F8A-46A2-2C303950D3C8}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Cryptography.Plugin.BouncyCastle", "StellaOps.Cryptography.Plugin.BouncyCastle", "{927E3CD3-4C20-4DE5-A395-D0977152A8D3}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Cryptography.Plugin.CryptoPro", "StellaOps.Cryptography.Plugin.CryptoPro", "{3C69853C-90E3-D889-1960-3B9229882590}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Cryptography.Plugin.OfflineVerification", "StellaOps.Cryptography.Plugin.OfflineVerification", "{9FB0DDD7-7A77-8DA4-F9E2-A94E60ED8FC7}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Cryptography.Plugin.OpenSslGost", "StellaOps.Cryptography.Plugin.OpenSslGost", "{643E4D4C-BC96-A37F-E0EC-488127F0B127}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Cryptography.Plugin.Pkcs11Gost", "StellaOps.Cryptography.Plugin.Pkcs11Gost", "{6F2CA7F5-3E7C-C61B-94E6-E7DD1227B5B1}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Cryptography.Plugin.PqSoft", "StellaOps.Cryptography.Plugin.PqSoft", "{F04B7DBB-77A5-C978-B2DE-8C189A32AA72}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Cryptography.Plugin.SimRemote", "StellaOps.Cryptography.Plugin.SimRemote", "{7C72F22A-20FF-DF5B-9191-6DFD0D497DB2}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Cryptography.Plugin.SmRemote", "StellaOps.Cryptography.Plugin.SmRemote", "{C896CC0A-F5E6-9AA4-C582-E691441F8D32}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Cryptography.Plugin.SmSoft", "StellaOps.Cryptography.Plugin.SmSoft", "{0AA3A418-AB45-CCA4-46D4-EEBFE011FECA}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Cryptography.Plugin.WineCsp", "StellaOps.Cryptography.Plugin.WineCsp", "{225D9926-4AE8-E539-70AD-8698E688F271}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Cryptography.PluginLoader", "StellaOps.Cryptography.PluginLoader", "{D6E8E69C-F721-BBCB-8C39-9716D53D72AD}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.DeltaVerdict", "StellaOps.DeltaVerdict", "{9529EE99-D6A5-B570-EB1F-15BD2D57DFE2}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.DependencyInjection", "StellaOps.DependencyInjection", "{589A43FD-8213-E9E3-6CFF-9CBA72D53E98}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Evidence.Bundle", "StellaOps.Evidence.Bundle", "{2BACF7E3-1278-FE99-8343-8221E6FBA9DE}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Evidence.Core", "StellaOps.Evidence.Core", "{75E47125-E4D7-8482-F1A4-726564970864}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Infrastructure.EfCore", "StellaOps.Infrastructure.EfCore", "{FCD529E0-DD17-6587-B29C-12D425C0AD0C}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Infrastructure.Postgres", "StellaOps.Infrastructure.Postgres", "{61B23570-4F2D-B060-BE1F-37995682E494}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Ingestion.Telemetry", "StellaOps.Ingestion.Telemetry", "{1182764D-2143-EEF0-9270-3DCE392F5D06}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Plugin", "StellaOps.Plugin", "{772B02B5-6280-E1D4-3E2E-248D0455C2FB}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Provcache", "StellaOps.Provcache", "{48F90289-938C-CCA7-B60F-D2143E7C9A69}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Provenance", "StellaOps.Provenance", "{E69FA1A0-6D1B-A6E4-2DC0-8F4C5F21BF04}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Replay.Core", "StellaOps.Replay.Core", "{083067CF-CE89-EF39-9BD3-4741919E26F3}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.TestKit", "StellaOps.TestKit", "{8380A20C-A5B8-EE91-1A58-270323688CB9}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.VersionComparison", "StellaOps.VersionComparison", "{A7542386-71EB-4F34-E1CE-27D399325955}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "__Tests", "__Tests", "{90659617-4DF7-809A-4E5B-29BB5A98E8E1}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "__Libraries", "__Libraries", "{AB8B269C-5A2A-A4B8-0488-B5F81E55B4D9}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Infrastructure.Postgres.Testing", "StellaOps.Infrastructure.Postgres.Testing", "{CEDC2447-F717-3C95-7E08-F214D575A7B7}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "__Libraries", "__Libraries", "{A5C98087-E847-D2C4-2143-20869479839D}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Advisory", "StellaOps.Scanner.Advisory", "{2F4EB1B2-4185-C535-85ED-53EA2D2C0573}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.Lang", "StellaOps.Scanner.Analyzers.Lang", "{79987C33-9903-4F75-1045-284F537155FE}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.Lang.Bun", "StellaOps.Scanner.Analyzers.Lang.Bun", "{730E1137-BA40-3899-17CB-93922F4DA4CA}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.Lang.Deno", "StellaOps.Scanner.Analyzers.Lang.Deno", "{EEF284CB-436C-439C-9606-E4DD1FD5F6D3}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.Lang.DotNet", "StellaOps.Scanner.Analyzers.Lang.DotNet", "{891DD979-9F3C-981D-FB41-EB66B7C61938}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.Lang.Go", "StellaOps.Scanner.Analyzers.Lang.Go", "{E5B2A8AA-8162-7E98-9D31-3DA40BB8EDC2}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.Lang.Java", "StellaOps.Scanner.Analyzers.Lang.Java", "{29E594C5-9DE1-D06F-4F19-4166C3935CAE}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.Lang.Node", "StellaOps.Scanner.Analyzers.Lang.Node", "{BE6FF48D-FDD8-A0C3-69E0-CAE30D3FC13F}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.Lang.Php", "StellaOps.Scanner.Analyzers.Lang.Php", "{8FF79C15-5E56-BB59-A473-0E42F8395C89}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.Lang.Python", "StellaOps.Scanner.Analyzers.Lang.Python", "{1003D3C6-91C9-D165-8325-20F3A16D9A5B}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.Lang.Ruby", "StellaOps.Scanner.Analyzers.Lang.Ruby", "{278C2043-B1CE-CC57-7F08-9C4B69C7A65D}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.Lang.Rust", "StellaOps.Scanner.Analyzers.Lang.Rust", "{F783A1A9-4991-592A-57C3-E18C41986545}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.Native", "StellaOps.Scanner.Analyzers.Native", "{9646F3EA-8DFC-F221-D415-D489C166889B}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.OS", "StellaOps.Scanner.Analyzers.OS", "{079CEB57-D0A3-20A1-131E-37D0CA4C0410}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.OS.Apk", "StellaOps.Scanner.Analyzers.OS.Apk", "{49ECEC98-A019-AEB6-1632-D05B0DCB6EEA}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.OS.Dpkg", "StellaOps.Scanner.Analyzers.OS.Dpkg", "{7C05D5D7-330C-204C-430C-8B2B27AB9995}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.OS.Homebrew", "StellaOps.Scanner.Analyzers.OS.Homebrew", "{AA9E4361-01CE-AC2F-E20D-0CA5C1D438CC}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.OS.MacOsBundle", "StellaOps.Scanner.Analyzers.OS.MacOsBundle", "{3C0D80C5-B033-CE52-8868-614826F45D72}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.OS.Pkgutil", "StellaOps.Scanner.Analyzers.OS.Pkgutil", "{C28FED05-DEDA-E1BA-F538-4BCAA8DAF2FE}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.OS.Rpm", "StellaOps.Scanner.Analyzers.OS.Rpm", "{53094E1B-EF1A-C3F8-6EB5-45A43B0B8A9C}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.OS.Windows.Chocolatey", "StellaOps.Scanner.Analyzers.OS.Windows.Chocolatey", "{9AA450C7-BBED-3722-DB4B-4DA97D885E00}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.OS.Windows.Msi", "StellaOps.Scanner.Analyzers.OS.Windows.Msi", "{31219219-DCBE-766A-A54F-4975DA7BBB80}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.OS.Windows.WinSxS", "StellaOps.Scanner.Analyzers.OS.Windows.WinSxS", "{0CBB3D0E-CC76-CDE9-4A2D-CE5F73B59420}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Benchmark", "StellaOps.Scanner.Benchmark", "{66B51CD4-BCE3-757B-BEFC-0F2BF96C7D52}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Benchmarks", "StellaOps.Scanner.Benchmarks", "{3C13055F-5B62-4AF8-F8BB-83CFDDEEDDCF}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Cache", "StellaOps.Scanner.Cache", "{4DAC6B77-D825-3085-4263-3226D14F61AC}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.CallGraph", "StellaOps.Scanner.CallGraph", "{12AF40C6-8379-4786-4DF4-B8CCCC54E2A8}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Core", "StellaOps.Scanner.Core", "{160F000D-9DA4-3AAA-8370-15C6BF460E1B}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Diff", "StellaOps.Scanner.Diff", "{5F222E3F-1E9A-331A-F5BC-7C22CC4DC27B}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Emit", "StellaOps.Scanner.Emit", "{7E234D3A-B714-8683-EF17-12E573F8796B}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.EntryTrace", "StellaOps.Scanner.EntryTrace", "{A92B2733-D971-E795-C42E-763B8D28BE6A}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Evidence", "StellaOps.Scanner.Evidence", "{56F1436F-34A3-0E62-F76D-7D33B0F6CF9A}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Explainability", "StellaOps.Scanner.Explainability", "{A5F6CD9E-7F26-36C4-0785-86FECE0484FE}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Orchestration", "StellaOps.Scanner.Orchestration", "{340ACF7A-5050-5BEF-8E3E-56EFD1EA2CFF}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.ProofIntegration", "StellaOps.Scanner.ProofIntegration", "{6C13C210-5443-5941-7273-A830E6A0F2FD}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.ProofSpine", "StellaOps.Scanner.ProofSpine", "{7CC450E2-1730-BE2C-9AE2-AF8567982B8E}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Queue", "StellaOps.Scanner.Queue", "{680B3990-2960-BCCC-7C3B-2C4BAB9B8134}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Reachability", "StellaOps.Scanner.Reachability", "{20D1D291-07F1-1389-74B4-F82B49847CED}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.ReachabilityDrift", "StellaOps.Scanner.ReachabilityDrift", "{7A7A0652-CB76-9B73-1FD1-D67F5510DB1F}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.SmartDiff", "StellaOps.Scanner.SmartDiff", "{4BE45036-7A8F-9072-5160-3BB53505A68F}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Storage", "StellaOps.Scanner.Storage", "{C484B2AA-2582-1395-840E-61DE9FED5313}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Storage.Oci", "StellaOps.Scanner.Storage.Oci", "{A81B4B21-F490-7DE9-024B-4959F9172735}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Surface", "StellaOps.Scanner.Surface", "{68C21EB9-A635-48B1-98E8-A0DD36C545F5}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Surface.Env", "StellaOps.Scanner.Surface.Env", "{7A608DF5-E8F5-38DA-4E15-2A7A2B6C508F}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Surface.FS", "StellaOps.Scanner.Surface.FS", "{0D24FEF1-02AE-8265-6524-64DE307CB4CE}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Surface.Secrets", "StellaOps.Scanner.Surface.Secrets", "{7DB7DF6E-FCAA-E569-29D8-C0C4FBFC5625}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Surface.Validation", "StellaOps.Scanner.Surface.Validation", "{D655B564-92E5-1E96-7A72-112F368418C4}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Triage", "StellaOps.Scanner.Triage", "{BAF3E8BB-263A-8E31-3576-D18BE2B46A22}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.VulnSurfaces", "StellaOps.Scanner.VulnSurfaces", "{DB888E44-BB55-C4A2-CDFF-7B04F0E2D6E5}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.VulnSurfaces.Tests", "StellaOps.Scanner.VulnSurfaces.Tests", "{56026A9E-B2DA-48EC-60C3-3193857A238C}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "__Tests", "__Tests", "{BB76B5A5-14BA-E317-828D-110B711D71F5}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Advisory.Tests", "StellaOps.Scanner.Advisory.Tests", "{3B2531C6-790E-BD6E-AE00-3F5F95EFE5F9}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.Lang.Bun.Tests", "StellaOps.Scanner.Analyzers.Lang.Bun.Tests", "{75E6454F-DC5F-C6E5-CB1D-47AA2AB72FDF}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.Lang.Deno.Tests", "StellaOps.Scanner.Analyzers.Lang.Deno.Tests", "{E49112A1-06DA-43F4-4A8D-5E805A6466FC}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.Lang.DotNet.Tests", "StellaOps.Scanner.Analyzers.Lang.DotNet.Tests", "{446F1BC4-7609-555E-1FA7-901560438951}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.Lang.Go.Tests", "StellaOps.Scanner.Analyzers.Lang.Go.Tests", "{327615A7-CCA8-CAAA-A279-54A2EB3C8D8C}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.Lang.Java.Tests", "StellaOps.Scanner.Analyzers.Lang.Java.Tests", "{1006432F-63A0-8569-EEC7-4393A1918480}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.Lang.Node.SmokeTests", "StellaOps.Scanner.Analyzers.Lang.Node.SmokeTests", "{4B6B949F-4BF5-2817-4818-71E26BF57749}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.Lang.Node.Tests", "StellaOps.Scanner.Analyzers.Lang.Node.Tests", "{FBBE5FEA-8CD5-8521-DC93-06C239F7876D}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.Lang.Php.Tests", "StellaOps.Scanner.Analyzers.Lang.Php.Tests", "{1F48E1F9-072B-1F1D-7E58-9C065D0F4ED1}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.Lang.Python.Tests", "StellaOps.Scanner.Analyzers.Lang.Python.Tests", "{5CC00E13-DAA8-7285-CDB0-D482D750FFBD}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.Lang.Ruby.Tests", "StellaOps.Scanner.Analyzers.Lang.Ruby.Tests", "{D2BC4D25-D54D-1778-798D-6547A38EEB05}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.Lang.Tests", "StellaOps.Scanner.Analyzers.Lang.Tests", "{7AC29AE8-7BDA-4243-5869-D89EF3921DC3}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.Native.Tests", "StellaOps.Scanner.Analyzers.Native.Tests", "{6E200246-2354-ECCC-0D81-F58C27F7551C}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.OS.Homebrew.Tests", "StellaOps.Scanner.Analyzers.OS.Homebrew.Tests", "{2F13F88C-7820-12E7-1FEF-4DB0DDE34DAB}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.OS.MacOsBundle.Tests", "StellaOps.Scanner.Analyzers.OS.MacOsBundle.Tests", "{ED480238-AEBE-3C29-8D80-5C0AB45E13D3}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.OS.Pkgutil.Tests", "StellaOps.Scanner.Analyzers.OS.Pkgutil.Tests", "{633ED6DB-FFB9-F767-070D-09C342DB4B74}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.OS.Tests", "StellaOps.Scanner.Analyzers.OS.Tests", "{235A45A7-331C-25BE-DB53-C2C328987E22}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.OS.Windows.Chocolatey.Tests", "StellaOps.Scanner.Analyzers.OS.Windows.Chocolatey.Tests", "{63E6AEF3-80BE-0FCE-E677-D01F65E1424F}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.OS.Windows.Msi.Tests", "StellaOps.Scanner.Analyzers.OS.Windows.Msi.Tests", "{D17D4B09-569F-CD45-70A8-2E4DDDE53E69}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.OS.Windows.WinSxS.Tests", "StellaOps.Scanner.Analyzers.OS.Windows.WinSxS.Tests", "{C1767CE0-3470-0BE7-DF63-57D1C7455A8A}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Benchmarks.Tests", "StellaOps.Scanner.Benchmarks.Tests", "{4E63ED70-BA5F-324B-9E64-B04E64B5C0C7}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Cache.Tests", "StellaOps.Scanner.Cache.Tests", "{F381910D-309A-2065-DD77-7494C2FD23BF}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.CallGraph.Tests", "StellaOps.Scanner.CallGraph.Tests", "{51A857B4-DBDA-1B5A-0F80-83580CA223B4}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Core.Tests", "StellaOps.Scanner.Core.Tests", "{63B739C4-3FFB-AD71-3B39-8EA5128D8127}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Diff.Tests", "StellaOps.Scanner.Diff.Tests", "{4DCC3C48-6209-AF26-3864-EAB021E65AD5}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Emit.Lineage.Tests", "StellaOps.Scanner.Emit.Lineage.Tests", "{7C0B72FC-8CBC-D831-9614-5C4CAE8197D3}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Emit.Tests", "StellaOps.Scanner.Emit.Tests", "{1AD4003A-C2C9-E202-16B4-973BE4ECFEB0}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.EntryTrace.Tests", "StellaOps.Scanner.EntryTrace.Tests", "{A684BCCD-046B-5314-B492-13A65C7AAC2A}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Evidence.Tests", "StellaOps.Scanner.Evidence.Tests", "{7A54278E-939F-447A-17F4-15DDB3F6B403}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Explainability.Tests", "StellaOps.Scanner.Explainability.Tests", "{AF719B05-6089-CA3F-96A7-8F59902FF781}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Integration.Tests", "StellaOps.Scanner.Integration.Tests", "{D6EFF950-E1CE-F003-26E6-CCD4CE93C144}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.ProofSpine.Tests", "StellaOps.Scanner.ProofSpine.Tests", "{44F1D673-6AB1-2635-567A-4512FEA53D32}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Queue.Tests", "StellaOps.Scanner.Queue.Tests", "{4B435085-030A-E80A-F018-FAEE801E2FB4}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Reachability.Stack.Tests", "StellaOps.Scanner.Reachability.Stack.Tests", "{5E698EC5-3B12-9266-8F90-ED9B6C841C6A}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Reachability.Tests", "StellaOps.Scanner.Reachability.Tests", "{D767CC98-7156-89E4-C89E-0B4624D83584}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.ReachabilityDrift.Tests", "StellaOps.Scanner.ReachabilityDrift.Tests", "{DE5FDAC5-0355-823F-552F-5A37661C9FE5}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Sbomer.BuildXPlugin.Tests", "StellaOps.Scanner.Sbomer.BuildXPlugin.Tests", "{0BA16F05-266F-4BCC-04A6-25E7D566E769}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.SmartDiff.Tests", "StellaOps.Scanner.SmartDiff.Tests", "{540911AA-2FA7-DF51-2203-B6109B580346}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Storage.Oci.Tests", "StellaOps.Scanner.Storage.Oci.Tests", "{B4801F0F-50AB-5613-AB78-5225D80E0421}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Storage.Tests", "StellaOps.Scanner.Storage.Tests", "{42729022-D163-60E6-B0BA-6FA7E84DA52F}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Surface.Env.Tests", "StellaOps.Scanner.Surface.Env.Tests", "{6A2FEC90-D817-C92B-89B8-8B2E2DD18FF1}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Surface.FS.Tests", "StellaOps.Scanner.Surface.FS.Tests", "{EA3E79FB-BEC9-7590-EC7C-64D2CF8E8782}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Surface.Secrets.Tests", "StellaOps.Scanner.Surface.Secrets.Tests", "{D835F664-32BC-4CA8-2ABB-97D30953F05C}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Surface.Tests", "StellaOps.Scanner.Surface.Tests", "{BC4C56DA-A389-5A58-7CB0-D7AA2AE44430}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Surface.Validation.Tests", "StellaOps.Scanner.Surface.Validation.Tests", "{AF8D5B15-8ADF-FA1F-342A-73D095FD5B21}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Triage.Tests", "StellaOps.Scanner.Triage.Tests", "{A27B1BB8-9415-4C14-CA41-828B66357E0B}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.WebService.Tests", "StellaOps.Scanner.WebService.Tests", "{69D6403C-9C1F-04C3-BADF-8CFFBD3848E7}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Worker.Tests", "StellaOps.Scanner.Worker.Tests", "{C26F680C-684A-ECC6-BB6C-EBD19DC43B4C}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.AirGap.Importer", "E:\dev\git.stella-ops.org\src\AirGap\StellaOps.AirGap.Importer\StellaOps.AirGap.Importer.csproj", "{22B129C7-C609-3B90-AD56-64C746A1505E}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.AirGap.Policy", "E:\dev\git.stella-ops.org\src\AirGap\StellaOps.AirGap.Policy\StellaOps.AirGap.Policy\StellaOps.AirGap.Policy.csproj", "{AD31623A-BC43-52C2-D906-AC1D8784A541}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Aoc", "E:\dev\git.stella-ops.org\src\Aoc\__Libraries\StellaOps.Aoc\StellaOps.Aoc.csproj", "{776E2142-804F-03B9-C804-D061D64C6092}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Attestor.Core", "E:\dev\git.stella-ops.org\src\Attestor\StellaOps.Attestor\StellaOps.Attestor.Core\StellaOps.Attestor.Core.csproj", "{5B4DF41E-C8CC-2606-FA2D-967118BD3C59}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Attestor.Envelope", "E:\dev\git.stella-ops.org\src\Attestor\StellaOps.Attestor.Envelope\StellaOps.Attestor.Envelope.csproj", "{3D8C5A6C-462D-7487-5BD0-A3EF6B657EB6}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Attestor.GraphRoot", "E:\dev\git.stella-ops.org\src\Attestor\__Libraries\StellaOps.Attestor.GraphRoot\StellaOps.Attestor.GraphRoot.csproj", "{2609BC1A-6765-29BE-78CC-C0F1D2814F10}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Attestor.ProofChain", "E:\dev\git.stella-ops.org\src\Attestor\__Libraries\StellaOps.Attestor.ProofChain\StellaOps.Attestor.ProofChain.csproj", "{C6822231-A4F4-9E69-6CE2-4FDB3E81C728}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Auth.Abstractions", "E:\dev\git.stella-ops.org\src\Authority\StellaOps.Authority\StellaOps.Auth.Abstractions\StellaOps.Auth.Abstractions.csproj", "{55D9B653-FB76-FCE8-1A3C-67B1BEDEC214}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Auth.Client", "E:\dev\git.stella-ops.org\src\Authority\StellaOps.Authority\StellaOps.Auth.Client\StellaOps.Auth.Client.csproj", "{DE5BF139-1E5C-D6EA-4FAA-661EF353A194}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Auth.Security", "E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.Auth.Security\StellaOps.Auth.Security.csproj", "{335E62C0-9E69-A952-680B-753B1B17C6D0}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Auth.ServerIntegration", "E:\dev\git.stella-ops.org\src\Authority\StellaOps.Authority\StellaOps.Auth.ServerIntegration\StellaOps.Auth.ServerIntegration.csproj", "{ECA25786-A3A8-92C4-4AA3-D4A73C69FDCA}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Authority.Core", "E:\dev\git.stella-ops.org\src\Authority\__Libraries\StellaOps.Authority.Core\StellaOps.Authority.Core.csproj", "{5A6CD890-8142-F920-3734-D67CA3E65F61}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Authority.Persistence", "E:\dev\git.stella-ops.org\src\Authority\__Libraries\StellaOps.Authority.Persistence\StellaOps.Authority.Persistence.csproj", "{A260E14F-DBA4-862E-53CD-18D3B92ADA3D}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Authority.Plugins.Abstractions", "E:\dev\git.stella-ops.org\src\Authority\StellaOps.Authority\StellaOps.Authority.Plugins.Abstractions\StellaOps.Authority.Plugins.Abstractions.csproj", "{97F94029-5419-6187-5A63-5C8FD9232FAE}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.BinaryIndex.Contracts", "E:\dev\git.stella-ops.org\src\BinaryIndex\__Libraries\StellaOps.BinaryIndex.Contracts\StellaOps.BinaryIndex.Contracts.csproj", "{03DF5914-2390-A82D-7464-642D0B95E068}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.BinaryIndex.Core", "E:\dev\git.stella-ops.org\src\BinaryIndex\__Libraries\StellaOps.BinaryIndex.Core\StellaOps.BinaryIndex.Core.csproj", "{CF633BDA-9F2E-D0C8-702F-BC9D27363B4B}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.BinaryIndex.Corpus", "E:\dev\git.stella-ops.org\src\BinaryIndex\__Libraries\StellaOps.BinaryIndex.Corpus\StellaOps.BinaryIndex.Corpus.csproj", "{73DE9C04-CEFE-53BA-A527-3A36D478DEFE}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.BinaryIndex.Fingerprints", "E:\dev\git.stella-ops.org\src\BinaryIndex\__Libraries\StellaOps.BinaryIndex.Fingerprints\StellaOps.BinaryIndex.Fingerprints.csproj", "{B7DC1B0A-EBD8-B1E8-28C8-9D5F19E118AD}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.BinaryIndex.FixIndex", "E:\dev\git.stella-ops.org\src\BinaryIndex\__Libraries\StellaOps.BinaryIndex.FixIndex\StellaOps.BinaryIndex.FixIndex.csproj", "{0B56708E-B56C-E058-DE31-FCDFF30031F7}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.BinaryIndex.Persistence", "E:\dev\git.stella-ops.org\src\BinaryIndex\__Libraries\StellaOps.BinaryIndex.Persistence\StellaOps.BinaryIndex.Persistence.csproj", "{78FAD457-CE1B-D78E-A602-510EAD85E0AF}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Canonical.Json", "E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.Canonical.Json\StellaOps.Canonical.Json.csproj", "{AF9E7F02-25AD-3540-18D7-F6A4F8BA5A60}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Cache.Valkey", "E:\dev\git.stella-ops.org\src\Concelier\__Libraries\StellaOps.Concelier.Cache.Valkey\StellaOps.Concelier.Cache.Valkey.csproj", "{AB6AE2B6-8D6B-2D9F-2A88-7C596C59F4FC}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Common", "E:\dev\git.stella-ops.org\src\Concelier\__Libraries\StellaOps.Concelier.Connector.Common\StellaOps.Concelier.Connector.Common.csproj", "{375F5AD0-F7EE-1782-7B34-E181CDB61B9F}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Core", "E:\dev\git.stella-ops.org\src\Concelier\__Libraries\StellaOps.Concelier.Core\StellaOps.Concelier.Core.csproj", "{BA45605A-1CCE-6B0C-489D-C113915B243F}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Interest", "E:\dev\git.stella-ops.org\src\Concelier\__Libraries\StellaOps.Concelier.Interest\StellaOps.Concelier.Interest.csproj", "{9D31FC8A-2A69-B78A-D3E5-4F867B16D971}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Merge", "E:\dev\git.stella-ops.org\src\Concelier\__Libraries\StellaOps.Concelier.Merge\StellaOps.Concelier.Merge.csproj", "{92268008-FBB0-C7AD-ECC2-7B75BED9F5E1}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Models", "E:\dev\git.stella-ops.org\src\Concelier\__Libraries\StellaOps.Concelier.Models\StellaOps.Concelier.Models.csproj", "{8DCCAF70-D364-4C8B-4E90-AF65091DE0C5}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Normalization", "E:\dev\git.stella-ops.org\src\Concelier\__Libraries\StellaOps.Concelier.Normalization\StellaOps.Concelier.Normalization.csproj", "{7828C164-DD01-2809-CCB3-364486834F60}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Persistence", "E:\dev\git.stella-ops.org\src\Concelier\__Libraries\StellaOps.Concelier.Persistence\StellaOps.Concelier.Persistence.csproj", "{DE95E7B2-0937-A980-441F-829E023BC43E}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.ProofService", "E:\dev\git.stella-ops.org\src\Concelier\__Libraries\StellaOps.Concelier.ProofService\StellaOps.Concelier.ProofService.csproj", "{91D69463-23E2-E2C7-AA7E-A78B13CED620}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.RawModels", "E:\dev\git.stella-ops.org\src\Concelier\__Libraries\StellaOps.Concelier.RawModels\StellaOps.Concelier.RawModels.csproj", "{34EFF636-81A7-8DF6-7CC9-4DA784BAC7F3}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.SbomIntegration", "E:\dev\git.stella-ops.org\src\Concelier\__Libraries\StellaOps.Concelier.SbomIntegration\StellaOps.Concelier.SbomIntegration.csproj", "{5DCF16A8-97C6-2CB4-6A63-0370239039EB}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.SourceIntel", "E:\dev\git.stella-ops.org\src\Concelier\__Libraries\StellaOps.Concelier.SourceIntel\StellaOps.Concelier.SourceIntel.csproj", "{EB093C48-CDAC-106B-1196-AE34809B34C0}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Configuration", "E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.Configuration\StellaOps.Configuration.csproj", "{92C62F7B-8028-6EE1-B71B-F45F459B8E97}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Cryptography", "E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.Cryptography\StellaOps.Cryptography.csproj", "{F664A948-E352-5808-E780-77A03F19E93E}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Cryptography.DependencyInjection", "E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.Cryptography.DependencyInjection\StellaOps.Cryptography.DependencyInjection.csproj", "{FA83F778-5252-0B80-5555-E69F790322EA}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Cryptography.Kms", "E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.Cryptography.Kms\StellaOps.Cryptography.Kms.csproj", "{F3A27846-6DE0-3448-222C-25A273E86B2E}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Cryptography.Plugin.BouncyCastle", "E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.Cryptography.Plugin.BouncyCastle\StellaOps.Cryptography.Plugin.BouncyCastle.csproj", "{166F4DEC-9886-92D5-6496-085664E9F08F}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Cryptography.Plugin.CryptoPro", "E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.Cryptography.Plugin.CryptoPro\StellaOps.Cryptography.Plugin.CryptoPro.csproj", "{C53E0895-879A-D9E6-0A43-24AD17A2F270}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Cryptography.Plugin.OfflineVerification", "E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.Cryptography.Plugin.OfflineVerification\StellaOps.Cryptography.Plugin.OfflineVerification.csproj", "{246FCC7C-1437-742D-BAE5-E77A24164F08}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Cryptography.Plugin.OpenSslGost", "E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.Cryptography.Plugin.OpenSslGost\StellaOps.Cryptography.Plugin.OpenSslGost.csproj", "{0AED303F-69E6-238F-EF80-81985080EDB7}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Cryptography.Plugin.Pkcs11Gost", "E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.Cryptography.Plugin.Pkcs11Gost\StellaOps.Cryptography.Plugin.Pkcs11Gost.csproj", "{2904D288-CE64-A565-2C46-C2E85A96A1EE}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Cryptography.Plugin.PqSoft", "E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.Cryptography.Plugin.PqSoft\StellaOps.Cryptography.Plugin.PqSoft.csproj", "{A6667CC3-B77F-023E-3A67-05F99E9FF46A}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Cryptography.Plugin.SimRemote", "E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.Cryptography.Plugin.SimRemote\StellaOps.Cryptography.Plugin.SimRemote.csproj", "{A26E2816-F787-F76B-1D6C-E086DD3E19CE}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Cryptography.Plugin.SmRemote", "E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.Cryptography.Plugin.SmRemote\StellaOps.Cryptography.Plugin.SmRemote.csproj", "{B3DEC619-67AC-1B5A-4F3E-A1F24C3F6877}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Cryptography.Plugin.SmSoft", "E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.Cryptography.Plugin.SmSoft\StellaOps.Cryptography.Plugin.SmSoft.csproj", "{90DB65B4-8F6E-FB8E-0281-505AD8BC6BA6}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Cryptography.Plugin.WineCsp", "E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.Cryptography.Plugin.WineCsp\StellaOps.Cryptography.Plugin.WineCsp.csproj", "{059FBB86-DEE6-8207-3F23-2A1A3EC00DEA}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Cryptography.PluginLoader", "E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.Cryptography.PluginLoader\StellaOps.Cryptography.PluginLoader.csproj", "{8BBA3159-C4CC-F685-A28C-7FE6CBD3D2A1}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.DeltaVerdict", "E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.DeltaVerdict\StellaOps.DeltaVerdict.csproj", "{EA0974E3-CD2B-5792-EF1E-9B5B7CCBDF00}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.DependencyInjection", "E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.DependencyInjection\StellaOps.DependencyInjection.csproj", "{632A1F0D-1BA5-C84B-B716-2BE638A92780}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Evidence.Bundle", "E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.Evidence.Bundle\StellaOps.Evidence.Bundle.csproj", "{9DE7852B-7E2D-257E-B0F1-45D2687854ED}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Evidence.Core", "E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.Evidence.Core\StellaOps.Evidence.Core.csproj", "{DC2AFC89-C3C8-4E9B-13A7-027EB6386EFA}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Excititor.Core", "E:\dev\git.stella-ops.org\src\Excititor\__Libraries\StellaOps.Excititor.Core\StellaOps.Excititor.Core.csproj", "{9151601C-8784-01A6-C2E7-A5C0FAAB0AEF}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Feedser.BinaryAnalysis", "E:\dev\git.stella-ops.org\src\Feedser\StellaOps.Feedser.BinaryAnalysis\StellaOps.Feedser.BinaryAnalysis.csproj", "{CB296A20-2732-77C1-7F23-27D5BAEDD0C7}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Feedser.Core", "E:\dev\git.stella-ops.org\src\Feedser\StellaOps.Feedser.Core\StellaOps.Feedser.Core.csproj", "{0DBEC9BA-FE1D-3898-B2C6-E4357DC23E0F}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Infrastructure.EfCore", "E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.Infrastructure.EfCore\StellaOps.Infrastructure.EfCore.csproj", "{A63897D9-9531-989B-7309-E384BCFC2BB9}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Infrastructure.Postgres", "E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.Infrastructure.Postgres\StellaOps.Infrastructure.Postgres.csproj", "{8C594D82-3463-3367-4F06-900AC707753D}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Infrastructure.Postgres.Testing", "E:\dev\git.stella-ops.org\src\__Tests\__Libraries\StellaOps.Infrastructure.Postgres.Testing\StellaOps.Infrastructure.Postgres.Testing.csproj", "{52F400CD-D473-7A1F-7986-89011CD2A887}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Ingestion.Telemetry", "E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.Ingestion.Telemetry\StellaOps.Ingestion.Telemetry.csproj", "{9588FBF9-C37E-D16E-2E8F-CFA226EAC01D}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Messaging", "E:\dev\git.stella-ops.org\src\Router\__Libraries\StellaOps.Messaging\StellaOps.Messaging.csproj", "{97998C88-E6E1-D5E2-B632-537B58E00CBF}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Microservice", "E:\dev\git.stella-ops.org\src\Router\__Libraries\StellaOps.Microservice\StellaOps.Microservice.csproj", "{BAD08D96-A80A-D27F-5D9C-656AEEB3D568}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Microservice.AspNetCore", "E:\dev\git.stella-ops.org\src\Router\__Libraries\StellaOps.Microservice.AspNetCore\StellaOps.Microservice.AspNetCore.csproj", "{F63694F1-B56D-6E72-3F5D-5D38B1541F0F}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Notify.Models", "E:\dev\git.stella-ops.org\src\Notify\__Libraries\StellaOps.Notify.Models\StellaOps.Notify.Models.csproj", "{20D1569C-2A47-38B8-075E-47225B674394}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Plugin", "E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.Plugin\StellaOps.Plugin.csproj", "{38A9EE9B-6FC8-93BC-0D43-2A906E678D66}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Policy", "E:\dev\git.stella-ops.org\src\Policy\__Libraries\StellaOps.Policy\StellaOps.Policy.csproj", "{19868E2D-7163-2108-1094-F13887C4F070}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Policy.RiskProfile", "E:\dev\git.stella-ops.org\src\Policy\StellaOps.Policy.RiskProfile\StellaOps.Policy.RiskProfile.csproj", "{CC319FC5-F4B1-C3DD-7310-4DAD343E0125}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Provcache", "E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.Provcache\StellaOps.Provcache.csproj", "{84F711C2-C210-28D2-F0D9-B13733FEE23D}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Provenance", "E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.Provenance\StellaOps.Provenance.csproj", "{CBB14B90-27F9-8DD6-DFC4-3507DBD1FBC6}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Provenance.Attestation", "E:\dev\git.stella-ops.org\src\Provenance\StellaOps.Provenance.Attestation\StellaOps.Provenance.Attestation.csproj", "{A78EBC0F-C62C-8F56-95C0-330E376242A2}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Replay.Core", "E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.Replay.Core\StellaOps.Replay.Core.csproj", "{6D26FB21-7E48-024B-E5D4-E3F0F31976BB}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Router.AspNet", "E:\dev\git.stella-ops.org\src\Router\__Libraries\StellaOps.Router.AspNet\StellaOps.Router.AspNet.csproj", "{79104479-B087-E5D0-5523-F1803282A246}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Router.Common", "E:\dev\git.stella-ops.org\src\Router\__Libraries\StellaOps.Router.Common\StellaOps.Router.Common.csproj", "{F17A6F0B-3120-2BA9-84D8-5F8BA0B9705D}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Advisory", "__Libraries\StellaOps.Scanner.Advisory\StellaOps.Scanner.Advisory.csproj", "{FBD908D6-AF93-CC62-C09D-F0BB3E0CEA7F}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Advisory.Tests", "__Tests\StellaOps.Scanner.Advisory.Tests\StellaOps.Scanner.Advisory.Tests.csproj", "{37F9B25E-81CF-95C5-0311-EA6DA191E415}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Lang", "__Libraries\StellaOps.Scanner.Analyzers.Lang\StellaOps.Scanner.Analyzers.Lang.csproj", "{28D91816-206C-576E-1A83-FD98E08C2E3C}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Lang.Bun", "__Libraries\StellaOps.Scanner.Analyzers.Lang.Bun\StellaOps.Scanner.Analyzers.Lang.Bun.csproj", "{5EFEC79C-A9F1-96A4-692C-733566107170}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Lang.Bun.Tests", "__Tests\StellaOps.Scanner.Analyzers.Lang.Bun.Tests\StellaOps.Scanner.Analyzers.Lang.Bun.Tests.csproj", "{F4E7E32B-D78B-5A08-F7B5-E8D4C7ED20D3}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Lang.Deno", "__Libraries\StellaOps.Scanner.Analyzers.Lang.Deno\StellaOps.Scanner.Analyzers.Lang.Deno.csproj", "{3A1CFB24-6EAA-9A87-7783-BFC56B0E5394}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Lang.Deno.Benchmarks", "__Benchmarks\StellaOps.Scanner.Analyzers.Lang.Deno.Benchmarks\StellaOps.Scanner.Analyzers.Lang.Deno.Benchmarks.csproj", "{B1969736-DE03-ADEB-2659-55B2B82B38A8}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Lang.Deno.Tests", "__Tests\StellaOps.Scanner.Analyzers.Lang.Deno.Tests\StellaOps.Scanner.Analyzers.Lang.Deno.Tests.csproj", "{D166FCF0-F220-A013-133A-620521740411}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Lang.DotNet", "__Libraries\StellaOps.Scanner.Analyzers.Lang.DotNet\StellaOps.Scanner.Analyzers.Lang.DotNet.csproj", "{F638D731-2DB2-2278-D9F8-019418A264F2}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Lang.DotNet.Tests", "__Tests\StellaOps.Scanner.Analyzers.Lang.DotNet.Tests\StellaOps.Scanner.Analyzers.Lang.DotNet.Tests.csproj", "{CAEB1FEB-B3F1-4B9D-7FEC-1983BCA60D81}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Lang.Go", "__Libraries\StellaOps.Scanner.Analyzers.Lang.Go\StellaOps.Scanner.Analyzers.Lang.Go.csproj", "{B07074FE-3D4E-5957-5F81-B75B5D25BD1B}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Lang.Go.Tests", "__Tests\StellaOps.Scanner.Analyzers.Lang.Go.Tests\StellaOps.Scanner.Analyzers.Lang.Go.Tests.csproj", "{91B8E22B-C90B-AEBD-707E-57BBD549BA32}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Lang.Java", "__Libraries\StellaOps.Scanner.Analyzers.Lang.Java\StellaOps.Scanner.Analyzers.Lang.Java.csproj", "{B7B5D764-C3A0-1743-0739-29966F993626}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Lang.Java.Tests", "__Tests\StellaOps.Scanner.Analyzers.Lang.Java.Tests\StellaOps.Scanner.Analyzers.Lang.Java.Tests.csproj", "{E9039B92-DAFC-F20B-22D6-78F8E4EB7CF1}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Lang.Node", "__Libraries\StellaOps.Scanner.Analyzers.Lang.Node\StellaOps.Scanner.Analyzers.Lang.Node.csproj", "{C4EDBBAF-875C-4839-05A8-F6F12A5ED52D}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Lang.Node.SmokeTests", "__Tests\StellaOps.Scanner.Analyzers.Lang.Node.SmokeTests\StellaOps.Scanner.Analyzers.Lang.Node.SmokeTests.csproj", "{04444789-CEE4-3F3A-6EFA-18416E620B2A}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Lang.Node.Tests", "__Tests\StellaOps.Scanner.Analyzers.Lang.Node.Tests\StellaOps.Scanner.Analyzers.Lang.Node.Tests.csproj", "{AD1B6448-2DC9-2F9A-D143-F09BBDF6F01F}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Lang.Php", "__Libraries\StellaOps.Scanner.Analyzers.Lang.Php\StellaOps.Scanner.Analyzers.Lang.Php.csproj", "{0EAC8F64-9588-1EF0-C33A-67590CF27590}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Lang.Php.Benchmarks", "__Benchmarks\StellaOps.Scanner.Analyzers.Lang.Php.Benchmarks\StellaOps.Scanner.Analyzers.Lang.Php.Benchmarks.csproj", "{761CAD6D-98CB-1936-9065-BF1A756671FF}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Lang.Php.Tests", "__Tests\StellaOps.Scanner.Analyzers.Lang.Php.Tests\StellaOps.Scanner.Analyzers.Lang.Php.Tests.csproj", "{7974C4F0-BC89-2775-8943-2DF909F3B08B}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Lang.Python", "__Libraries\StellaOps.Scanner.Analyzers.Lang.Python\StellaOps.Scanner.Analyzers.Lang.Python.csproj", "{B1B31937-CCC8-D97A-F66D-1849734B780B}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Lang.Python.Tests", "__Tests\StellaOps.Scanner.Analyzers.Lang.Python.Tests\StellaOps.Scanner.Analyzers.Lang.Python.Tests.csproj", "{9A566B3A-E281-09AF-EC1B-BD4FDB3248CE}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Lang.Ruby", "__Libraries\StellaOps.Scanner.Analyzers.Lang.Ruby\StellaOps.Scanner.Analyzers.Lang.Ruby.csproj", "{A345E5AC-BDDB-A817-3C92-08C8865D1EF9}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Lang.Ruby.Tests", "__Tests\StellaOps.Scanner.Analyzers.Lang.Ruby.Tests\StellaOps.Scanner.Analyzers.Lang.Ruby.Tests.csproj", "{905DD8ED-3D10-7C2B-B199-B98E85267BB8}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Lang.Rust", "__Libraries\StellaOps.Scanner.Analyzers.Lang.Rust\StellaOps.Scanner.Analyzers.Lang.Rust.csproj", "{C2D3B3C7-E556-9B72-024A-FF5EDEAF4CB5}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Lang.Rust.Benchmarks", "__Benchmarks\StellaOps.Scanner.Analyzers.Lang.Rust.Benchmarks\StellaOps.Scanner.Analyzers.Lang.Rust.Benchmarks.csproj", "{31AC6B88-D6C8-E2EF-39AF-1B21AFD76C89}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Lang.Tests", "__Tests\StellaOps.Scanner.Analyzers.Lang.Tests\StellaOps.Scanner.Analyzers.Lang.Tests.csproj", "{90B84537-F992-234C-C998-91C6AD65AB12}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Native", "__Libraries\StellaOps.Scanner.Analyzers.Native\StellaOps.Scanner.Analyzers.Native.csproj", "{F22333B6-7E27-679B-8475-B4B9AB1CB186}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Native", "StellaOps.Scanner.Analyzers.Native\StellaOps.Scanner.Analyzers.Native.csproj", "{CE042F3A-6851-FAAB-9E9C-AD905B4AAC8D}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Native.Tests", "__Tests\StellaOps.Scanner.Analyzers.Native.Tests\StellaOps.Scanner.Analyzers.Native.Tests.csproj", "{D6B56A54-4057-9F76-BC7E-56E896E5D276}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.OS", "__Libraries\StellaOps.Scanner.Analyzers.OS\StellaOps.Scanner.Analyzers.OS.csproj", "{9258E4F2-762C-C780-F118-2CABD0281CC9}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.OS.Apk", "__Libraries\StellaOps.Scanner.Analyzers.OS.Apk\StellaOps.Scanner.Analyzers.OS.Apk.csproj", "{D6752A7E-0FD2-6626-80E3-CE4D4816D2B0}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.OS.Dpkg", "__Libraries\StellaOps.Scanner.Analyzers.OS.Dpkg\StellaOps.Scanner.Analyzers.OS.Dpkg.csproj", "{AF85AC87-521A-2F0E-5F10-836E416EC716}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.OS.Homebrew", "__Libraries\StellaOps.Scanner.Analyzers.OS.Homebrew\StellaOps.Scanner.Analyzers.OS.Homebrew.csproj", "{FB946C57-55B3-08C6-18AE-1672D46C5308}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.OS.Homebrew.Tests", "__Tests\StellaOps.Scanner.Analyzers.OS.Homebrew.Tests\StellaOps.Scanner.Analyzers.OS.Homebrew.Tests.csproj", "{99A47EAA-44B8-8E06-DA0E-05B225009FDF}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.OS.MacOsBundle", "__Libraries\StellaOps.Scanner.Analyzers.OS.MacOsBundle\StellaOps.Scanner.Analyzers.OS.MacOsBundle.csproj", "{4F0EF830-4308-347B-A31D-270A9812D15E}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.OS.MacOsBundle.Tests", "__Tests\StellaOps.Scanner.Analyzers.OS.MacOsBundle.Tests\StellaOps.Scanner.Analyzers.OS.MacOsBundle.Tests.csproj", "{B7EE2F70-A634-8B4D-93F4-EA1EEFD9E5E8}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.OS.Pkgutil", "__Libraries\StellaOps.Scanner.Analyzers.OS.Pkgutil\StellaOps.Scanner.Analyzers.OS.Pkgutil.csproj", "{A5298720-984E-6574-D41B-CFE7CA408182}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.OS.Pkgutil.Tests", "__Tests\StellaOps.Scanner.Analyzers.OS.Pkgutil.Tests\StellaOps.Scanner.Analyzers.OS.Pkgutil.Tests.csproj", "{CB033CB6-F90B-E201-BA86-C867544E7247}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.OS.Rpm", "__Libraries\StellaOps.Scanner.Analyzers.OS.Rpm\StellaOps.Scanner.Analyzers.OS.Rpm.csproj", "{E9F5BFF2-0D0E-7B41-9AF0-83384F4B8825}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.OS.Tests", "__Tests\StellaOps.Scanner.Analyzers.OS.Tests\StellaOps.Scanner.Analyzers.OS.Tests.csproj", "{668466AC-CD66-BAA0-0322-148549E373CB}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.OS.Windows.Chocolatey", "__Libraries\StellaOps.Scanner.Analyzers.OS.Windows.Chocolatey\StellaOps.Scanner.Analyzers.OS.Windows.Chocolatey.csproj", "{07EBBFA6-798E-76A3-CAF0-67828B00B58E}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.OS.Windows.Chocolatey.Tests", "__Tests\StellaOps.Scanner.Analyzers.OS.Windows.Chocolatey.Tests\StellaOps.Scanner.Analyzers.OS.Windows.Chocolatey.Tests.csproj", "{181ED0FE-FE20-069F-7CCF-86FF5449D7F5}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.OS.Windows.Msi", "__Libraries\StellaOps.Scanner.Analyzers.OS.Windows.Msi\StellaOps.Scanner.Analyzers.OS.Windows.Msi.csproj", "{5E683B7C-B584-0E56-C8D6-D29050DE70FB}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.OS.Windows.Msi.Tests", "__Tests\StellaOps.Scanner.Analyzers.OS.Windows.Msi.Tests\StellaOps.Scanner.Analyzers.OS.Windows.Msi.Tests.csproj", "{4163E755-1563-6A72-60E7-BB2B69F5ABA2}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.OS.Windows.WinSxS", "__Libraries\StellaOps.Scanner.Analyzers.OS.Windows.WinSxS\StellaOps.Scanner.Analyzers.OS.Windows.WinSxS.csproj", "{AE6F3DA7-2993-6926-323E-A29295D55C36}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.OS.Windows.WinSxS.Tests", "__Tests\StellaOps.Scanner.Analyzers.OS.Windows.WinSxS.Tests\StellaOps.Scanner.Analyzers.OS.Windows.WinSxS.Tests.csproj", "{D013641A-8457-6215-05A1-74BB57B58409}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Benchmark", "__Libraries\StellaOps.Scanner.Benchmark\StellaOps.Scanner.Benchmark.csproj", "{4FC29140-9C5F-8DD5-B405-6982BD1DA5A3}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Benchmarks", "__Libraries\StellaOps.Scanner.Benchmarks\StellaOps.Scanner.Benchmarks.csproj", "{B9C9A1E4-3BB8-C8BE-7819-660A582D2952}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Benchmarks.Tests", "__Tests\StellaOps.Scanner.Benchmarks.Tests\StellaOps.Scanner.Benchmarks.Tests.csproj", "{2BBAB3B4-2E18-F945-F7AB-6207D7F72714}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Cache", "__Libraries\StellaOps.Scanner.Cache\StellaOps.Scanner.Cache.csproj", "{BA492274-A505-BCD5-3DA5-EE0C94DD5748}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Cache.Tests", "__Tests\StellaOps.Scanner.Cache.Tests\StellaOps.Scanner.Cache.Tests.csproj", "{029F8300-57F5-9CCD-505E-708937686679}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.CallGraph", "__Libraries\StellaOps.Scanner.CallGraph\StellaOps.Scanner.CallGraph.csproj", "{A5D2DB78-8045-29AC-E4B1-66E72F2C7FF0}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.CallGraph.Tests", "__Tests\StellaOps.Scanner.CallGraph.Tests\StellaOps.Scanner.CallGraph.Tests.csproj", "{294792C0-DC28-3C5D-2D59-33DC99CD6C61}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Core", "__Libraries\StellaOps.Scanner.Core\StellaOps.Scanner.Core.csproj", "{58D8630F-C0F4-B772-8572-BCC98FF0F0D8}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Core.Tests", "__Tests\StellaOps.Scanner.Core.Tests\StellaOps.Scanner.Core.Tests.csproj", "{2B1B4954-1241-8F2E-75B6-2146D15D037B}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Diff", "__Libraries\StellaOps.Scanner.Diff\StellaOps.Scanner.Diff.csproj", "{97A9C869-F385-6711-6B76-F3859C86DCAC}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Diff.Tests", "__Tests\StellaOps.Scanner.Diff.Tests\StellaOps.Scanner.Diff.Tests.csproj", "{201CE292-0186-2A38-55D7-69890B5817DF}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Emit", "__Libraries\StellaOps.Scanner.Emit\StellaOps.Scanner.Emit.csproj", "{17A00031-9FF7-4F73-5319-23FA5817625F}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Emit.Lineage.Tests", "__Tests\StellaOps.Scanner.Emit.Lineage.Tests\StellaOps.Scanner.Emit.Lineage.Tests.csproj", "{11E0E129-091F-BEEB-A1B2-9BAEF5BE9FBC}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Emit.Tests", "__Tests\StellaOps.Scanner.Emit.Tests\StellaOps.Scanner.Emit.Tests.csproj", "{AEF63403-4889-5396-CDEA-3B713CEF2ED7}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.EntryTrace", "__Libraries\StellaOps.Scanner.EntryTrace\StellaOps.Scanner.EntryTrace.csproj", "{D24E7862-3930-A4F6-1DFA-DA88C759546C}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.EntryTrace.Tests", "__Tests\StellaOps.Scanner.EntryTrace.Tests\StellaOps.Scanner.EntryTrace.Tests.csproj", "{6DC62619-949E-92E6-F4F1-5A0320959929}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Evidence", "__Libraries\StellaOps.Scanner.Evidence\StellaOps.Scanner.Evidence.csproj", "{37F1D83D-073C-C165-4C53-664AD87628E6}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Evidence.Tests", "__Tests\StellaOps.Scanner.Evidence.Tests\StellaOps.Scanner.Evidence.Tests.csproj", "{CDC236E8-6881-46C4-EE95-3C386AF009D0}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Explainability", "__Libraries\StellaOps.Scanner.Explainability\StellaOps.Scanner.Explainability.csproj", "{ACC2785F-F4B9-13E4-EED2-C5D067242175}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Explainability.Tests", "__Tests\StellaOps.Scanner.Explainability.Tests\StellaOps.Scanner.Explainability.Tests.csproj", "{7F4A3CA3-C3DA-A698-5CBC-54F28D1C7DFB}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Integration.Tests", "__Tests\StellaOps.Scanner.Integration.Tests\StellaOps.Scanner.Integration.Tests.csproj", "{DAA1F516-DEC3-7FF2-3A63-78DD97BA062C}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Orchestration", "__Libraries\StellaOps.Scanner.Orchestration\StellaOps.Scanner.Orchestration.csproj", "{11EF0DE9-2648-F711-6194-70B5C40B3F3F}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.ProofIntegration", "__Libraries\StellaOps.Scanner.ProofIntegration\StellaOps.Scanner.ProofIntegration.csproj", "{01A21B47-07C5-6039-1B48-C5EACA3DBA2D}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.ProofSpine", "__Libraries\StellaOps.Scanner.ProofSpine\StellaOps.Scanner.ProofSpine.csproj", "{7CB7FEA8-8A12-A5D6-0057-AA65DB328617}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.ProofSpine.Tests", "__Tests\StellaOps.Scanner.ProofSpine.Tests\StellaOps.Scanner.ProofSpine.Tests.csproj", "{0484DB46-3E40-1A10-131C-524AF1233EA7}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Queue", "__Libraries\StellaOps.Scanner.Queue\StellaOps.Scanner.Queue.csproj", "{64E1D9B1-B944-8AA3-799F-02E7DD33FB78}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Queue.Tests", "__Tests\StellaOps.Scanner.Queue.Tests\StellaOps.Scanner.Queue.Tests.csproj", "{D37991E1-585F-FF1B-9772-07477E40AF78}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Reachability", "__Libraries\StellaOps.Scanner.Reachability\StellaOps.Scanner.Reachability.csproj", "{35A06F00-71AB-8A31-7D60-EBF41EA730CA}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Reachability.Stack.Tests", "__Tests\StellaOps.Scanner.Reachability.Stack.Tests\StellaOps.Scanner.Reachability.Stack.Tests.csproj", "{56120A54-1D4D-F07B-63B4-B15525C2ADD9}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Reachability.Tests", "__Tests\StellaOps.Scanner.Reachability.Tests\StellaOps.Scanner.Reachability.Tests.csproj", "{BE47FB74-D163-0B1F-5293-0962EA7E8585}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.ReachabilityDrift", "__Libraries\StellaOps.Scanner.ReachabilityDrift\StellaOps.Scanner.ReachabilityDrift.csproj", "{9AD932E9-0986-654C-B454-34E654C80697}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.ReachabilityDrift.Tests", "__Tests\StellaOps.Scanner.ReachabilityDrift.Tests\StellaOps.Scanner.ReachabilityDrift.Tests.csproj", "{00BE2B68-FC96-23F8-F61D-EE53B7AE06A1}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Sbomer.BuildXPlugin", "StellaOps.Scanner.Sbomer.BuildXPlugin\StellaOps.Scanner.Sbomer.BuildXPlugin.csproj", "{570BA050-81A7-46EB-3DDD-422027EE2CA2}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Sbomer.BuildXPlugin.Tests", "__Tests\StellaOps.Scanner.Sbomer.BuildXPlugin.Tests\StellaOps.Scanner.Sbomer.BuildXPlugin.Tests.csproj", "{6C43FD78-3478-F245-3EE4-E410D1E7D7C5}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.SmartDiff", "__Libraries\StellaOps.Scanner.SmartDiff\StellaOps.Scanner.SmartDiff.csproj", "{7F0FFA06-EAC8-CC9A-3386-389638F12B59}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.SmartDiff.Tests", "__Tests\StellaOps.Scanner.SmartDiff.Tests\StellaOps.Scanner.SmartDiff.Tests.csproj", "{03B9D4BE-348B-9AD6-6FE9-6C40AE22053D}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Storage", "__Libraries\StellaOps.Scanner.Storage\StellaOps.Scanner.Storage.csproj", "{35CF4CF2-8A84-378D-32F0-572F4AA900A3}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Storage.Epss.Perf", "__Benchmarks\StellaOps.Scanner.Storage.Epss.Perf\StellaOps.Scanner.Storage.Epss.Perf.csproj", "{13E03C69-0634-3330-26D9-DCF7DD136BC5}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Storage.Oci", "__Libraries\StellaOps.Scanner.Storage.Oci\StellaOps.Scanner.Storage.Oci.csproj", "{A80D212B-7E80-4251-16C0-60FA3670A5B4}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Storage.Oci.Tests", "__Tests\StellaOps.Scanner.Storage.Oci.Tests\StellaOps.Scanner.Storage.Oci.Tests.csproj", "{2F4ACEB8-76C7-D5A2-6DD1-2EF713D9A197}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Storage.Tests", "__Tests\StellaOps.Scanner.Storage.Tests\StellaOps.Scanner.Storage.Tests.csproj", "{C146A9AF-6C13-B9DC-F555-37182A54430F}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Surface", "__Libraries\StellaOps.Scanner.Surface\StellaOps.Scanner.Surface.csproj", "{E5025FCF-78CB-4B5B-3377-AE008B1BE9D2}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Surface.Env", "__Libraries\StellaOps.Scanner.Surface.Env\StellaOps.Scanner.Surface.Env.csproj", "{52698305-D6F8-C13C-0882-48FC37726404}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Surface.Env.Tests", "__Tests\StellaOps.Scanner.Surface.Env.Tests\StellaOps.Scanner.Surface.Env.Tests.csproj", "{DE10AF97-E790-9D19-2399-70940A9B83A7}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Surface.FS", "__Libraries\StellaOps.Scanner.Surface.FS\StellaOps.Scanner.Surface.FS.csproj", "{5567139C-0365-B6A0-5DD0-978A09B9F176}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Surface.FS.Tests", "__Tests\StellaOps.Scanner.Surface.FS.Tests\StellaOps.Scanner.Surface.FS.Tests.csproj", "{A56C1F0E-3E18-DBEE-7F97-B5FCBF23D1D6}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Surface.Secrets", "__Libraries\StellaOps.Scanner.Surface.Secrets\StellaOps.Scanner.Surface.Secrets.csproj", "{256D269B-35EA-F833-2F1D-8E0058908DEE}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Surface.Secrets.Tests", "__Tests\StellaOps.Scanner.Surface.Secrets.Tests\StellaOps.Scanner.Surface.Secrets.Tests.csproj", "{F02B63CD-2C69-61F7-7F96-930122D4D4D7}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Surface.Tests", "__Tests\StellaOps.Scanner.Surface.Tests\StellaOps.Scanner.Surface.Tests.csproj", "{F061C879-063E-99DE-B301-E261DB12156F}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Surface.Validation", "__Libraries\StellaOps.Scanner.Surface.Validation\StellaOps.Scanner.Surface.Validation.csproj", "{6E9C9582-67FA-2EB1-C6BA-AD4CD326E276}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Surface.Validation.Tests", "__Tests\StellaOps.Scanner.Surface.Validation.Tests\StellaOps.Scanner.Surface.Validation.Tests.csproj", "{FCF711C2-1090-7204-5E38-4BEFBE265A61}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Triage", "__Libraries\StellaOps.Scanner.Triage\StellaOps.Scanner.Triage.csproj", "{3A4AAE04-FA0C-2AF8-6548-AC22E5A41312}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Triage.Tests", "__Tests\StellaOps.Scanner.Triage.Tests\StellaOps.Scanner.Triage.Tests.csproj", "{66F8F288-C387-40E0-5F83-938671335703}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.VulnSurfaces", "__Libraries\StellaOps.Scanner.VulnSurfaces\StellaOps.Scanner.VulnSurfaces.csproj", "{7B3BDB83-918F-6760-3853-BDD70CD71B42}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.VulnSurfaces.Tests", "__Libraries\StellaOps.Scanner.VulnSurfaces.Tests\StellaOps.Scanner.VulnSurfaces.Tests.csproj", "{2669C700-5CFF-0186-F65E-8D26BE06E934}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.WebService", "StellaOps.Scanner.WebService\StellaOps.Scanner.WebService.csproj", "{0560BD84-CDBC-A79A-C665-55F6D62825EA}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.WebService.Tests", "__Tests\StellaOps.Scanner.WebService.Tests\StellaOps.Scanner.WebService.Tests.csproj", "{783A67C9-3381-6E4C-3752-423F0FC6F6F9}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Worker", "StellaOps.Scanner.Worker\StellaOps.Scanner.Worker.csproj", "{F890BD12-6CF5-4F80-9099-B7FE9A908432}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Worker.Tests", "__Tests\StellaOps.Scanner.Worker.Tests\StellaOps.Scanner.Worker.Tests.csproj", "{505C6840-5113-26EC-CEDB-D07EEABEF94B}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Signals", "E:\dev\git.stella-ops.org\src\Signals\StellaOps.Signals\StellaOps.Signals.csproj", "{A79CBC0C-5313-4ECF-A24E-27CE236BCF2C}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Signer.Core", "E:\dev\git.stella-ops.org\src\Signer\StellaOps.Signer\StellaOps.Signer.Core\StellaOps.Signer.Core.csproj", "{0AF13355-173C-3128-5AFC-D32E540DA3EF}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.TestKit", "E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.TestKit\StellaOps.TestKit.csproj", "{AF043113-CCE3-59C1-DF71-9804155F26A8}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Unknowns.Core", "E:\dev\git.stella-ops.org\src\Unknowns\__Libraries\StellaOps.Unknowns.Core\StellaOps.Unknowns.Core.csproj", "{15602821-2ABA-14BB-738D-1A53E1976E07}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.VersionComparison", "E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.VersionComparison\StellaOps.VersionComparison.csproj", "{1D761F8B-921C-53BF-DCF5-5ABD329EEB0C}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Zastava.Core", "E:\dev\git.stella-ops.org\src\Zastava\__Libraries\StellaOps.Zastava.Core\StellaOps.Zastava.Core.csproj", "{DA7634C2-9156-9B79-7A1D-90D8E605DC8A}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {22B129C7-C609-3B90-AD56-64C746A1505E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {22B129C7-C609-3B90-AD56-64C746A1505E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {22B129C7-C609-3B90-AD56-64C746A1505E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {22B129C7-C609-3B90-AD56-64C746A1505E}.Release|Any CPU.Build.0 = Release|Any CPU - {AD31623A-BC43-52C2-D906-AC1D8784A541}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AD31623A-BC43-52C2-D906-AC1D8784A541}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AD31623A-BC43-52C2-D906-AC1D8784A541}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AD31623A-BC43-52C2-D906-AC1D8784A541}.Release|Any CPU.Build.0 = Release|Any CPU - {776E2142-804F-03B9-C804-D061D64C6092}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {776E2142-804F-03B9-C804-D061D64C6092}.Debug|Any CPU.Build.0 = Debug|Any CPU - {776E2142-804F-03B9-C804-D061D64C6092}.Release|Any CPU.ActiveCfg = Release|Any CPU - {776E2142-804F-03B9-C804-D061D64C6092}.Release|Any CPU.Build.0 = Release|Any CPU - {5B4DF41E-C8CC-2606-FA2D-967118BD3C59}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5B4DF41E-C8CC-2606-FA2D-967118BD3C59}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5B4DF41E-C8CC-2606-FA2D-967118BD3C59}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5B4DF41E-C8CC-2606-FA2D-967118BD3C59}.Release|Any CPU.Build.0 = Release|Any CPU - {3D8C5A6C-462D-7487-5BD0-A3EF6B657EB6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3D8C5A6C-462D-7487-5BD0-A3EF6B657EB6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3D8C5A6C-462D-7487-5BD0-A3EF6B657EB6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3D8C5A6C-462D-7487-5BD0-A3EF6B657EB6}.Release|Any CPU.Build.0 = Release|Any CPU - {2609BC1A-6765-29BE-78CC-C0F1D2814F10}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2609BC1A-6765-29BE-78CC-C0F1D2814F10}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2609BC1A-6765-29BE-78CC-C0F1D2814F10}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2609BC1A-6765-29BE-78CC-C0F1D2814F10}.Release|Any CPU.Build.0 = Release|Any CPU - {C6822231-A4F4-9E69-6CE2-4FDB3E81C728}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C6822231-A4F4-9E69-6CE2-4FDB3E81C728}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C6822231-A4F4-9E69-6CE2-4FDB3E81C728}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C6822231-A4F4-9E69-6CE2-4FDB3E81C728}.Release|Any CPU.Build.0 = Release|Any CPU - {55D9B653-FB76-FCE8-1A3C-67B1BEDEC214}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {55D9B653-FB76-FCE8-1A3C-67B1BEDEC214}.Debug|Any CPU.Build.0 = Debug|Any CPU - {55D9B653-FB76-FCE8-1A3C-67B1BEDEC214}.Release|Any CPU.ActiveCfg = Release|Any CPU - {55D9B653-FB76-FCE8-1A3C-67B1BEDEC214}.Release|Any CPU.Build.0 = Release|Any CPU - {DE5BF139-1E5C-D6EA-4FAA-661EF353A194}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {DE5BF139-1E5C-D6EA-4FAA-661EF353A194}.Debug|Any CPU.Build.0 = Debug|Any CPU - {DE5BF139-1E5C-D6EA-4FAA-661EF353A194}.Release|Any CPU.ActiveCfg = Release|Any CPU - {DE5BF139-1E5C-D6EA-4FAA-661EF353A194}.Release|Any CPU.Build.0 = Release|Any CPU - {335E62C0-9E69-A952-680B-753B1B17C6D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {335E62C0-9E69-A952-680B-753B1B17C6D0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {335E62C0-9E69-A952-680B-753B1B17C6D0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {335E62C0-9E69-A952-680B-753B1B17C6D0}.Release|Any CPU.Build.0 = Release|Any CPU - {ECA25786-A3A8-92C4-4AA3-D4A73C69FDCA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {ECA25786-A3A8-92C4-4AA3-D4A73C69FDCA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {ECA25786-A3A8-92C4-4AA3-D4A73C69FDCA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {ECA25786-A3A8-92C4-4AA3-D4A73C69FDCA}.Release|Any CPU.Build.0 = Release|Any CPU - {5A6CD890-8142-F920-3734-D67CA3E65F61}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5A6CD890-8142-F920-3734-D67CA3E65F61}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5A6CD890-8142-F920-3734-D67CA3E65F61}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5A6CD890-8142-F920-3734-D67CA3E65F61}.Release|Any CPU.Build.0 = Release|Any CPU - {A260E14F-DBA4-862E-53CD-18D3B92ADA3D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A260E14F-DBA4-862E-53CD-18D3B92ADA3D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A260E14F-DBA4-862E-53CD-18D3B92ADA3D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A260E14F-DBA4-862E-53CD-18D3B92ADA3D}.Release|Any CPU.Build.0 = Release|Any CPU - {97F94029-5419-6187-5A63-5C8FD9232FAE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {97F94029-5419-6187-5A63-5C8FD9232FAE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {97F94029-5419-6187-5A63-5C8FD9232FAE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {97F94029-5419-6187-5A63-5C8FD9232FAE}.Release|Any CPU.Build.0 = Release|Any CPU - {03DF5914-2390-A82D-7464-642D0B95E068}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {03DF5914-2390-A82D-7464-642D0B95E068}.Debug|Any CPU.Build.0 = Debug|Any CPU - {03DF5914-2390-A82D-7464-642D0B95E068}.Release|Any CPU.ActiveCfg = Release|Any CPU - {03DF5914-2390-A82D-7464-642D0B95E068}.Release|Any CPU.Build.0 = Release|Any CPU - {CF633BDA-9F2E-D0C8-702F-BC9D27363B4B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {CF633BDA-9F2E-D0C8-702F-BC9D27363B4B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {CF633BDA-9F2E-D0C8-702F-BC9D27363B4B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {CF633BDA-9F2E-D0C8-702F-BC9D27363B4B}.Release|Any CPU.Build.0 = Release|Any CPU - {73DE9C04-CEFE-53BA-A527-3A36D478DEFE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {73DE9C04-CEFE-53BA-A527-3A36D478DEFE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {73DE9C04-CEFE-53BA-A527-3A36D478DEFE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {73DE9C04-CEFE-53BA-A527-3A36D478DEFE}.Release|Any CPU.Build.0 = Release|Any CPU - {B7DC1B0A-EBD8-B1E8-28C8-9D5F19E118AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B7DC1B0A-EBD8-B1E8-28C8-9D5F19E118AD}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B7DC1B0A-EBD8-B1E8-28C8-9D5F19E118AD}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B7DC1B0A-EBD8-B1E8-28C8-9D5F19E118AD}.Release|Any CPU.Build.0 = Release|Any CPU - {0B56708E-B56C-E058-DE31-FCDFF30031F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0B56708E-B56C-E058-DE31-FCDFF30031F7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0B56708E-B56C-E058-DE31-FCDFF30031F7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0B56708E-B56C-E058-DE31-FCDFF30031F7}.Release|Any CPU.Build.0 = Release|Any CPU - {78FAD457-CE1B-D78E-A602-510EAD85E0AF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {78FAD457-CE1B-D78E-A602-510EAD85E0AF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {78FAD457-CE1B-D78E-A602-510EAD85E0AF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {78FAD457-CE1B-D78E-A602-510EAD85E0AF}.Release|Any CPU.Build.0 = Release|Any CPU - {AF9E7F02-25AD-3540-18D7-F6A4F8BA5A60}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AF9E7F02-25AD-3540-18D7-F6A4F8BA5A60}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AF9E7F02-25AD-3540-18D7-F6A4F8BA5A60}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AF9E7F02-25AD-3540-18D7-F6A4F8BA5A60}.Release|Any CPU.Build.0 = Release|Any CPU - {AB6AE2B6-8D6B-2D9F-2A88-7C596C59F4FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AB6AE2B6-8D6B-2D9F-2A88-7C596C59F4FC}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AB6AE2B6-8D6B-2D9F-2A88-7C596C59F4FC}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AB6AE2B6-8D6B-2D9F-2A88-7C596C59F4FC}.Release|Any CPU.Build.0 = Release|Any CPU - {375F5AD0-F7EE-1782-7B34-E181CDB61B9F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {375F5AD0-F7EE-1782-7B34-E181CDB61B9F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {375F5AD0-F7EE-1782-7B34-E181CDB61B9F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {375F5AD0-F7EE-1782-7B34-E181CDB61B9F}.Release|Any CPU.Build.0 = Release|Any CPU - {BA45605A-1CCE-6B0C-489D-C113915B243F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {BA45605A-1CCE-6B0C-489D-C113915B243F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {BA45605A-1CCE-6B0C-489D-C113915B243F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {BA45605A-1CCE-6B0C-489D-C113915B243F}.Release|Any CPU.Build.0 = Release|Any CPU - {9D31FC8A-2A69-B78A-D3E5-4F867B16D971}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9D31FC8A-2A69-B78A-D3E5-4F867B16D971}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9D31FC8A-2A69-B78A-D3E5-4F867B16D971}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9D31FC8A-2A69-B78A-D3E5-4F867B16D971}.Release|Any CPU.Build.0 = Release|Any CPU - {92268008-FBB0-C7AD-ECC2-7B75BED9F5E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {92268008-FBB0-C7AD-ECC2-7B75BED9F5E1}.Debug|Any CPU.Build.0 = Debug|Any CPU - {92268008-FBB0-C7AD-ECC2-7B75BED9F5E1}.Release|Any CPU.ActiveCfg = Release|Any CPU - {92268008-FBB0-C7AD-ECC2-7B75BED9F5E1}.Release|Any CPU.Build.0 = Release|Any CPU - {8DCCAF70-D364-4C8B-4E90-AF65091DE0C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8DCCAF70-D364-4C8B-4E90-AF65091DE0C5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8DCCAF70-D364-4C8B-4E90-AF65091DE0C5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8DCCAF70-D364-4C8B-4E90-AF65091DE0C5}.Release|Any CPU.Build.0 = Release|Any CPU - {7828C164-DD01-2809-CCB3-364486834F60}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7828C164-DD01-2809-CCB3-364486834F60}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7828C164-DD01-2809-CCB3-364486834F60}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7828C164-DD01-2809-CCB3-364486834F60}.Release|Any CPU.Build.0 = Release|Any CPU - {DE95E7B2-0937-A980-441F-829E023BC43E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {DE95E7B2-0937-A980-441F-829E023BC43E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {DE95E7B2-0937-A980-441F-829E023BC43E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {DE95E7B2-0937-A980-441F-829E023BC43E}.Release|Any CPU.Build.0 = Release|Any CPU - {91D69463-23E2-E2C7-AA7E-A78B13CED620}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {91D69463-23E2-E2C7-AA7E-A78B13CED620}.Debug|Any CPU.Build.0 = Debug|Any CPU - {91D69463-23E2-E2C7-AA7E-A78B13CED620}.Release|Any CPU.ActiveCfg = Release|Any CPU - {91D69463-23E2-E2C7-AA7E-A78B13CED620}.Release|Any CPU.Build.0 = Release|Any CPU - {34EFF636-81A7-8DF6-7CC9-4DA784BAC7F3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {34EFF636-81A7-8DF6-7CC9-4DA784BAC7F3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {34EFF636-81A7-8DF6-7CC9-4DA784BAC7F3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {34EFF636-81A7-8DF6-7CC9-4DA784BAC7F3}.Release|Any CPU.Build.0 = Release|Any CPU - {5DCF16A8-97C6-2CB4-6A63-0370239039EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5DCF16A8-97C6-2CB4-6A63-0370239039EB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5DCF16A8-97C6-2CB4-6A63-0370239039EB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5DCF16A8-97C6-2CB4-6A63-0370239039EB}.Release|Any CPU.Build.0 = Release|Any CPU - {EB093C48-CDAC-106B-1196-AE34809B34C0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EB093C48-CDAC-106B-1196-AE34809B34C0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {EB093C48-CDAC-106B-1196-AE34809B34C0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EB093C48-CDAC-106B-1196-AE34809B34C0}.Release|Any CPU.Build.0 = Release|Any CPU - {92C62F7B-8028-6EE1-B71B-F45F459B8E97}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {92C62F7B-8028-6EE1-B71B-F45F459B8E97}.Debug|Any CPU.Build.0 = Debug|Any CPU - {92C62F7B-8028-6EE1-B71B-F45F459B8E97}.Release|Any CPU.ActiveCfg = Release|Any CPU - {92C62F7B-8028-6EE1-B71B-F45F459B8E97}.Release|Any CPU.Build.0 = Release|Any CPU - {F664A948-E352-5808-E780-77A03F19E93E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F664A948-E352-5808-E780-77A03F19E93E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F664A948-E352-5808-E780-77A03F19E93E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F664A948-E352-5808-E780-77A03F19E93E}.Release|Any CPU.Build.0 = Release|Any CPU - {FA83F778-5252-0B80-5555-E69F790322EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FA83F778-5252-0B80-5555-E69F790322EA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FA83F778-5252-0B80-5555-E69F790322EA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FA83F778-5252-0B80-5555-E69F790322EA}.Release|Any CPU.Build.0 = Release|Any CPU - {F3A27846-6DE0-3448-222C-25A273E86B2E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F3A27846-6DE0-3448-222C-25A273E86B2E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F3A27846-6DE0-3448-222C-25A273E86B2E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F3A27846-6DE0-3448-222C-25A273E86B2E}.Release|Any CPU.Build.0 = Release|Any CPU - {166F4DEC-9886-92D5-6496-085664E9F08F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {166F4DEC-9886-92D5-6496-085664E9F08F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {166F4DEC-9886-92D5-6496-085664E9F08F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {166F4DEC-9886-92D5-6496-085664E9F08F}.Release|Any CPU.Build.0 = Release|Any CPU - {C53E0895-879A-D9E6-0A43-24AD17A2F270}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C53E0895-879A-D9E6-0A43-24AD17A2F270}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C53E0895-879A-D9E6-0A43-24AD17A2F270}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C53E0895-879A-D9E6-0A43-24AD17A2F270}.Release|Any CPU.Build.0 = Release|Any CPU - {246FCC7C-1437-742D-BAE5-E77A24164F08}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {246FCC7C-1437-742D-BAE5-E77A24164F08}.Debug|Any CPU.Build.0 = Debug|Any CPU - {246FCC7C-1437-742D-BAE5-E77A24164F08}.Release|Any CPU.ActiveCfg = Release|Any CPU - {246FCC7C-1437-742D-BAE5-E77A24164F08}.Release|Any CPU.Build.0 = Release|Any CPU - {0AED303F-69E6-238F-EF80-81985080EDB7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0AED303F-69E6-238F-EF80-81985080EDB7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0AED303F-69E6-238F-EF80-81985080EDB7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0AED303F-69E6-238F-EF80-81985080EDB7}.Release|Any CPU.Build.0 = Release|Any CPU - {2904D288-CE64-A565-2C46-C2E85A96A1EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2904D288-CE64-A565-2C46-C2E85A96A1EE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2904D288-CE64-A565-2C46-C2E85A96A1EE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2904D288-CE64-A565-2C46-C2E85A96A1EE}.Release|Any CPU.Build.0 = Release|Any CPU - {A6667CC3-B77F-023E-3A67-05F99E9FF46A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A6667CC3-B77F-023E-3A67-05F99E9FF46A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A6667CC3-B77F-023E-3A67-05F99E9FF46A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A6667CC3-B77F-023E-3A67-05F99E9FF46A}.Release|Any CPU.Build.0 = Release|Any CPU - {A26E2816-F787-F76B-1D6C-E086DD3E19CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A26E2816-F787-F76B-1D6C-E086DD3E19CE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A26E2816-F787-F76B-1D6C-E086DD3E19CE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A26E2816-F787-F76B-1D6C-E086DD3E19CE}.Release|Any CPU.Build.0 = Release|Any CPU - {B3DEC619-67AC-1B5A-4F3E-A1F24C3F6877}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B3DEC619-67AC-1B5A-4F3E-A1F24C3F6877}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B3DEC619-67AC-1B5A-4F3E-A1F24C3F6877}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B3DEC619-67AC-1B5A-4F3E-A1F24C3F6877}.Release|Any CPU.Build.0 = Release|Any CPU - {90DB65B4-8F6E-FB8E-0281-505AD8BC6BA6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {90DB65B4-8F6E-FB8E-0281-505AD8BC6BA6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {90DB65B4-8F6E-FB8E-0281-505AD8BC6BA6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {90DB65B4-8F6E-FB8E-0281-505AD8BC6BA6}.Release|Any CPU.Build.0 = Release|Any CPU - {059FBB86-DEE6-8207-3F23-2A1A3EC00DEA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {059FBB86-DEE6-8207-3F23-2A1A3EC00DEA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {059FBB86-DEE6-8207-3F23-2A1A3EC00DEA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {059FBB86-DEE6-8207-3F23-2A1A3EC00DEA}.Release|Any CPU.Build.0 = Release|Any CPU - {8BBA3159-C4CC-F685-A28C-7FE6CBD3D2A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8BBA3159-C4CC-F685-A28C-7FE6CBD3D2A1}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8BBA3159-C4CC-F685-A28C-7FE6CBD3D2A1}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8BBA3159-C4CC-F685-A28C-7FE6CBD3D2A1}.Release|Any CPU.Build.0 = Release|Any CPU - {EA0974E3-CD2B-5792-EF1E-9B5B7CCBDF00}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EA0974E3-CD2B-5792-EF1E-9B5B7CCBDF00}.Debug|Any CPU.Build.0 = Debug|Any CPU - {EA0974E3-CD2B-5792-EF1E-9B5B7CCBDF00}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EA0974E3-CD2B-5792-EF1E-9B5B7CCBDF00}.Release|Any CPU.Build.0 = Release|Any CPU - {632A1F0D-1BA5-C84B-B716-2BE638A92780}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {632A1F0D-1BA5-C84B-B716-2BE638A92780}.Debug|Any CPU.Build.0 = Debug|Any CPU - {632A1F0D-1BA5-C84B-B716-2BE638A92780}.Release|Any CPU.ActiveCfg = Release|Any CPU - {632A1F0D-1BA5-C84B-B716-2BE638A92780}.Release|Any CPU.Build.0 = Release|Any CPU - {9DE7852B-7E2D-257E-B0F1-45D2687854ED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9DE7852B-7E2D-257E-B0F1-45D2687854ED}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9DE7852B-7E2D-257E-B0F1-45D2687854ED}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9DE7852B-7E2D-257E-B0F1-45D2687854ED}.Release|Any CPU.Build.0 = Release|Any CPU - {DC2AFC89-C3C8-4E9B-13A7-027EB6386EFA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {DC2AFC89-C3C8-4E9B-13A7-027EB6386EFA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {DC2AFC89-C3C8-4E9B-13A7-027EB6386EFA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {DC2AFC89-C3C8-4E9B-13A7-027EB6386EFA}.Release|Any CPU.Build.0 = Release|Any CPU - {9151601C-8784-01A6-C2E7-A5C0FAAB0AEF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9151601C-8784-01A6-C2E7-A5C0FAAB0AEF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9151601C-8784-01A6-C2E7-A5C0FAAB0AEF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9151601C-8784-01A6-C2E7-A5C0FAAB0AEF}.Release|Any CPU.Build.0 = Release|Any CPU - {CB296A20-2732-77C1-7F23-27D5BAEDD0C7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {CB296A20-2732-77C1-7F23-27D5BAEDD0C7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {CB296A20-2732-77C1-7F23-27D5BAEDD0C7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {CB296A20-2732-77C1-7F23-27D5BAEDD0C7}.Release|Any CPU.Build.0 = Release|Any CPU - {0DBEC9BA-FE1D-3898-B2C6-E4357DC23E0F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0DBEC9BA-FE1D-3898-B2C6-E4357DC23E0F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0DBEC9BA-FE1D-3898-B2C6-E4357DC23E0F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0DBEC9BA-FE1D-3898-B2C6-E4357DC23E0F}.Release|Any CPU.Build.0 = Release|Any CPU - {A63897D9-9531-989B-7309-E384BCFC2BB9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A63897D9-9531-989B-7309-E384BCFC2BB9}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A63897D9-9531-989B-7309-E384BCFC2BB9}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A63897D9-9531-989B-7309-E384BCFC2BB9}.Release|Any CPU.Build.0 = Release|Any CPU - {8C594D82-3463-3367-4F06-900AC707753D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8C594D82-3463-3367-4F06-900AC707753D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8C594D82-3463-3367-4F06-900AC707753D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8C594D82-3463-3367-4F06-900AC707753D}.Release|Any CPU.Build.0 = Release|Any CPU - {52F400CD-D473-7A1F-7986-89011CD2A887}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {52F400CD-D473-7A1F-7986-89011CD2A887}.Debug|Any CPU.Build.0 = Debug|Any CPU - {52F400CD-D473-7A1F-7986-89011CD2A887}.Release|Any CPU.ActiveCfg = Release|Any CPU - {52F400CD-D473-7A1F-7986-89011CD2A887}.Release|Any CPU.Build.0 = Release|Any CPU - {9588FBF9-C37E-D16E-2E8F-CFA226EAC01D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9588FBF9-C37E-D16E-2E8F-CFA226EAC01D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9588FBF9-C37E-D16E-2E8F-CFA226EAC01D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9588FBF9-C37E-D16E-2E8F-CFA226EAC01D}.Release|Any CPU.Build.0 = Release|Any CPU - {97998C88-E6E1-D5E2-B632-537B58E00CBF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {97998C88-E6E1-D5E2-B632-537B58E00CBF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {97998C88-E6E1-D5E2-B632-537B58E00CBF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {97998C88-E6E1-D5E2-B632-537B58E00CBF}.Release|Any CPU.Build.0 = Release|Any CPU - {BAD08D96-A80A-D27F-5D9C-656AEEB3D568}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {BAD08D96-A80A-D27F-5D9C-656AEEB3D568}.Debug|Any CPU.Build.0 = Debug|Any CPU - {BAD08D96-A80A-D27F-5D9C-656AEEB3D568}.Release|Any CPU.ActiveCfg = Release|Any CPU - {BAD08D96-A80A-D27F-5D9C-656AEEB3D568}.Release|Any CPU.Build.0 = Release|Any CPU - {F63694F1-B56D-6E72-3F5D-5D38B1541F0F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F63694F1-B56D-6E72-3F5D-5D38B1541F0F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F63694F1-B56D-6E72-3F5D-5D38B1541F0F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F63694F1-B56D-6E72-3F5D-5D38B1541F0F}.Release|Any CPU.Build.0 = Release|Any CPU - {20D1569C-2A47-38B8-075E-47225B674394}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {20D1569C-2A47-38B8-075E-47225B674394}.Debug|Any CPU.Build.0 = Debug|Any CPU - {20D1569C-2A47-38B8-075E-47225B674394}.Release|Any CPU.ActiveCfg = Release|Any CPU - {20D1569C-2A47-38B8-075E-47225B674394}.Release|Any CPU.Build.0 = Release|Any CPU - {38A9EE9B-6FC8-93BC-0D43-2A906E678D66}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {38A9EE9B-6FC8-93BC-0D43-2A906E678D66}.Debug|Any CPU.Build.0 = Debug|Any CPU - {38A9EE9B-6FC8-93BC-0D43-2A906E678D66}.Release|Any CPU.ActiveCfg = Release|Any CPU - {38A9EE9B-6FC8-93BC-0D43-2A906E678D66}.Release|Any CPU.Build.0 = Release|Any CPU - {19868E2D-7163-2108-1094-F13887C4F070}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {19868E2D-7163-2108-1094-F13887C4F070}.Debug|Any CPU.Build.0 = Debug|Any CPU - {19868E2D-7163-2108-1094-F13887C4F070}.Release|Any CPU.ActiveCfg = Release|Any CPU - {19868E2D-7163-2108-1094-F13887C4F070}.Release|Any CPU.Build.0 = Release|Any CPU - {CC319FC5-F4B1-C3DD-7310-4DAD343E0125}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {CC319FC5-F4B1-C3DD-7310-4DAD343E0125}.Debug|Any CPU.Build.0 = Debug|Any CPU - {CC319FC5-F4B1-C3DD-7310-4DAD343E0125}.Release|Any CPU.ActiveCfg = Release|Any CPU - {CC319FC5-F4B1-C3DD-7310-4DAD343E0125}.Release|Any CPU.Build.0 = Release|Any CPU - {84F711C2-C210-28D2-F0D9-B13733FEE23D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {84F711C2-C210-28D2-F0D9-B13733FEE23D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {84F711C2-C210-28D2-F0D9-B13733FEE23D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {84F711C2-C210-28D2-F0D9-B13733FEE23D}.Release|Any CPU.Build.0 = Release|Any CPU - {CBB14B90-27F9-8DD6-DFC4-3507DBD1FBC6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {CBB14B90-27F9-8DD6-DFC4-3507DBD1FBC6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {CBB14B90-27F9-8DD6-DFC4-3507DBD1FBC6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {CBB14B90-27F9-8DD6-DFC4-3507DBD1FBC6}.Release|Any CPU.Build.0 = Release|Any CPU - {A78EBC0F-C62C-8F56-95C0-330E376242A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A78EBC0F-C62C-8F56-95C0-330E376242A2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A78EBC0F-C62C-8F56-95C0-330E376242A2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A78EBC0F-C62C-8F56-95C0-330E376242A2}.Release|Any CPU.Build.0 = Release|Any CPU - {6D26FB21-7E48-024B-E5D4-E3F0F31976BB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6D26FB21-7E48-024B-E5D4-E3F0F31976BB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6D26FB21-7E48-024B-E5D4-E3F0F31976BB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6D26FB21-7E48-024B-E5D4-E3F0F31976BB}.Release|Any CPU.Build.0 = Release|Any CPU - {79104479-B087-E5D0-5523-F1803282A246}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {79104479-B087-E5D0-5523-F1803282A246}.Debug|Any CPU.Build.0 = Debug|Any CPU - {79104479-B087-E5D0-5523-F1803282A246}.Release|Any CPU.ActiveCfg = Release|Any CPU - {79104479-B087-E5D0-5523-F1803282A246}.Release|Any CPU.Build.0 = Release|Any CPU - {F17A6F0B-3120-2BA9-84D8-5F8BA0B9705D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F17A6F0B-3120-2BA9-84D8-5F8BA0B9705D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F17A6F0B-3120-2BA9-84D8-5F8BA0B9705D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F17A6F0B-3120-2BA9-84D8-5F8BA0B9705D}.Release|Any CPU.Build.0 = Release|Any CPU - {FBD908D6-AF93-CC62-C09D-F0BB3E0CEA7F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FBD908D6-AF93-CC62-C09D-F0BB3E0CEA7F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FBD908D6-AF93-CC62-C09D-F0BB3E0CEA7F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FBD908D6-AF93-CC62-C09D-F0BB3E0CEA7F}.Release|Any CPU.Build.0 = Release|Any CPU - {37F9B25E-81CF-95C5-0311-EA6DA191E415}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {37F9B25E-81CF-95C5-0311-EA6DA191E415}.Debug|Any CPU.Build.0 = Debug|Any CPU - {37F9B25E-81CF-95C5-0311-EA6DA191E415}.Release|Any CPU.ActiveCfg = Release|Any CPU - {37F9B25E-81CF-95C5-0311-EA6DA191E415}.Release|Any CPU.Build.0 = Release|Any CPU - {28D91816-206C-576E-1A83-FD98E08C2E3C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {28D91816-206C-576E-1A83-FD98E08C2E3C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {28D91816-206C-576E-1A83-FD98E08C2E3C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {28D91816-206C-576E-1A83-FD98E08C2E3C}.Release|Any CPU.Build.0 = Release|Any CPU - {5EFEC79C-A9F1-96A4-692C-733566107170}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5EFEC79C-A9F1-96A4-692C-733566107170}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5EFEC79C-A9F1-96A4-692C-733566107170}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5EFEC79C-A9F1-96A4-692C-733566107170}.Release|Any CPU.Build.0 = Release|Any CPU - {F4E7E32B-D78B-5A08-F7B5-E8D4C7ED20D3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F4E7E32B-D78B-5A08-F7B5-E8D4C7ED20D3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F4E7E32B-D78B-5A08-F7B5-E8D4C7ED20D3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F4E7E32B-D78B-5A08-F7B5-E8D4C7ED20D3}.Release|Any CPU.Build.0 = Release|Any CPU - {3A1CFB24-6EAA-9A87-7783-BFC56B0E5394}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3A1CFB24-6EAA-9A87-7783-BFC56B0E5394}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3A1CFB24-6EAA-9A87-7783-BFC56B0E5394}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3A1CFB24-6EAA-9A87-7783-BFC56B0E5394}.Release|Any CPU.Build.0 = Release|Any CPU - {B1969736-DE03-ADEB-2659-55B2B82B38A8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B1969736-DE03-ADEB-2659-55B2B82B38A8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B1969736-DE03-ADEB-2659-55B2B82B38A8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B1969736-DE03-ADEB-2659-55B2B82B38A8}.Release|Any CPU.Build.0 = Release|Any CPU - {D166FCF0-F220-A013-133A-620521740411}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D166FCF0-F220-A013-133A-620521740411}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D166FCF0-F220-A013-133A-620521740411}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D166FCF0-F220-A013-133A-620521740411}.Release|Any CPU.Build.0 = Release|Any CPU - {F638D731-2DB2-2278-D9F8-019418A264F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F638D731-2DB2-2278-D9F8-019418A264F2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F638D731-2DB2-2278-D9F8-019418A264F2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F638D731-2DB2-2278-D9F8-019418A264F2}.Release|Any CPU.Build.0 = Release|Any CPU - {CAEB1FEB-B3F1-4B9D-7FEC-1983BCA60D81}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {CAEB1FEB-B3F1-4B9D-7FEC-1983BCA60D81}.Debug|Any CPU.Build.0 = Debug|Any CPU - {CAEB1FEB-B3F1-4B9D-7FEC-1983BCA60D81}.Release|Any CPU.ActiveCfg = Release|Any CPU - {CAEB1FEB-B3F1-4B9D-7FEC-1983BCA60D81}.Release|Any CPU.Build.0 = Release|Any CPU - {B07074FE-3D4E-5957-5F81-B75B5D25BD1B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B07074FE-3D4E-5957-5F81-B75B5D25BD1B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B07074FE-3D4E-5957-5F81-B75B5D25BD1B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B07074FE-3D4E-5957-5F81-B75B5D25BD1B}.Release|Any CPU.Build.0 = Release|Any CPU - {91B8E22B-C90B-AEBD-707E-57BBD549BA32}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {91B8E22B-C90B-AEBD-707E-57BBD549BA32}.Debug|Any CPU.Build.0 = Debug|Any CPU - {91B8E22B-C90B-AEBD-707E-57BBD549BA32}.Release|Any CPU.ActiveCfg = Release|Any CPU - {91B8E22B-C90B-AEBD-707E-57BBD549BA32}.Release|Any CPU.Build.0 = Release|Any CPU - {B7B5D764-C3A0-1743-0739-29966F993626}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B7B5D764-C3A0-1743-0739-29966F993626}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B7B5D764-C3A0-1743-0739-29966F993626}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B7B5D764-C3A0-1743-0739-29966F993626}.Release|Any CPU.Build.0 = Release|Any CPU - {E9039B92-DAFC-F20B-22D6-78F8E4EB7CF1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E9039B92-DAFC-F20B-22D6-78F8E4EB7CF1}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E9039B92-DAFC-F20B-22D6-78F8E4EB7CF1}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E9039B92-DAFC-F20B-22D6-78F8E4EB7CF1}.Release|Any CPU.Build.0 = Release|Any CPU - {C4EDBBAF-875C-4839-05A8-F6F12A5ED52D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C4EDBBAF-875C-4839-05A8-F6F12A5ED52D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C4EDBBAF-875C-4839-05A8-F6F12A5ED52D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C4EDBBAF-875C-4839-05A8-F6F12A5ED52D}.Release|Any CPU.Build.0 = Release|Any CPU - {04444789-CEE4-3F3A-6EFA-18416E620B2A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {04444789-CEE4-3F3A-6EFA-18416E620B2A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {04444789-CEE4-3F3A-6EFA-18416E620B2A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {04444789-CEE4-3F3A-6EFA-18416E620B2A}.Release|Any CPU.Build.0 = Release|Any CPU - {AD1B6448-2DC9-2F9A-D143-F09BBDF6F01F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AD1B6448-2DC9-2F9A-D143-F09BBDF6F01F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AD1B6448-2DC9-2F9A-D143-F09BBDF6F01F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AD1B6448-2DC9-2F9A-D143-F09BBDF6F01F}.Release|Any CPU.Build.0 = Release|Any CPU - {0EAC8F64-9588-1EF0-C33A-67590CF27590}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0EAC8F64-9588-1EF0-C33A-67590CF27590}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0EAC8F64-9588-1EF0-C33A-67590CF27590}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0EAC8F64-9588-1EF0-C33A-67590CF27590}.Release|Any CPU.Build.0 = Release|Any CPU - {761CAD6D-98CB-1936-9065-BF1A756671FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {761CAD6D-98CB-1936-9065-BF1A756671FF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {761CAD6D-98CB-1936-9065-BF1A756671FF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {761CAD6D-98CB-1936-9065-BF1A756671FF}.Release|Any CPU.Build.0 = Release|Any CPU - {7974C4F0-BC89-2775-8943-2DF909F3B08B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7974C4F0-BC89-2775-8943-2DF909F3B08B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7974C4F0-BC89-2775-8943-2DF909F3B08B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7974C4F0-BC89-2775-8943-2DF909F3B08B}.Release|Any CPU.Build.0 = Release|Any CPU - {B1B31937-CCC8-D97A-F66D-1849734B780B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B1B31937-CCC8-D97A-F66D-1849734B780B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B1B31937-CCC8-D97A-F66D-1849734B780B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B1B31937-CCC8-D97A-F66D-1849734B780B}.Release|Any CPU.Build.0 = Release|Any CPU - {9A566B3A-E281-09AF-EC1B-BD4FDB3248CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9A566B3A-E281-09AF-EC1B-BD4FDB3248CE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9A566B3A-E281-09AF-EC1B-BD4FDB3248CE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9A566B3A-E281-09AF-EC1B-BD4FDB3248CE}.Release|Any CPU.Build.0 = Release|Any CPU - {A345E5AC-BDDB-A817-3C92-08C8865D1EF9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A345E5AC-BDDB-A817-3C92-08C8865D1EF9}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A345E5AC-BDDB-A817-3C92-08C8865D1EF9}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A345E5AC-BDDB-A817-3C92-08C8865D1EF9}.Release|Any CPU.Build.0 = Release|Any CPU - {905DD8ED-3D10-7C2B-B199-B98E85267BB8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {905DD8ED-3D10-7C2B-B199-B98E85267BB8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {905DD8ED-3D10-7C2B-B199-B98E85267BB8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {905DD8ED-3D10-7C2B-B199-B98E85267BB8}.Release|Any CPU.Build.0 = Release|Any CPU - {C2D3B3C7-E556-9B72-024A-FF5EDEAF4CB5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C2D3B3C7-E556-9B72-024A-FF5EDEAF4CB5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C2D3B3C7-E556-9B72-024A-FF5EDEAF4CB5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C2D3B3C7-E556-9B72-024A-FF5EDEAF4CB5}.Release|Any CPU.Build.0 = Release|Any CPU - {31AC6B88-D6C8-E2EF-39AF-1B21AFD76C89}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {31AC6B88-D6C8-E2EF-39AF-1B21AFD76C89}.Debug|Any CPU.Build.0 = Debug|Any CPU - {31AC6B88-D6C8-E2EF-39AF-1B21AFD76C89}.Release|Any CPU.ActiveCfg = Release|Any CPU - {31AC6B88-D6C8-E2EF-39AF-1B21AFD76C89}.Release|Any CPU.Build.0 = Release|Any CPU - {90B84537-F992-234C-C998-91C6AD65AB12}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {90B84537-F992-234C-C998-91C6AD65AB12}.Debug|Any CPU.Build.0 = Debug|Any CPU - {90B84537-F992-234C-C998-91C6AD65AB12}.Release|Any CPU.ActiveCfg = Release|Any CPU - {90B84537-F992-234C-C998-91C6AD65AB12}.Release|Any CPU.Build.0 = Release|Any CPU - {F22333B6-7E27-679B-8475-B4B9AB1CB186}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F22333B6-7E27-679B-8475-B4B9AB1CB186}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F22333B6-7E27-679B-8475-B4B9AB1CB186}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F22333B6-7E27-679B-8475-B4B9AB1CB186}.Release|Any CPU.Build.0 = Release|Any CPU - {CE042F3A-6851-FAAB-9E9C-AD905B4AAC8D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {CE042F3A-6851-FAAB-9E9C-AD905B4AAC8D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {CE042F3A-6851-FAAB-9E9C-AD905B4AAC8D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {CE042F3A-6851-FAAB-9E9C-AD905B4AAC8D}.Release|Any CPU.Build.0 = Release|Any CPU - {D6B56A54-4057-9F76-BC7E-56E896E5D276}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D6B56A54-4057-9F76-BC7E-56E896E5D276}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D6B56A54-4057-9F76-BC7E-56E896E5D276}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D6B56A54-4057-9F76-BC7E-56E896E5D276}.Release|Any CPU.Build.0 = Release|Any CPU - {9258E4F2-762C-C780-F118-2CABD0281CC9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9258E4F2-762C-C780-F118-2CABD0281CC9}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9258E4F2-762C-C780-F118-2CABD0281CC9}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9258E4F2-762C-C780-F118-2CABD0281CC9}.Release|Any CPU.Build.0 = Release|Any CPU - {D6752A7E-0FD2-6626-80E3-CE4D4816D2B0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D6752A7E-0FD2-6626-80E3-CE4D4816D2B0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D6752A7E-0FD2-6626-80E3-CE4D4816D2B0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D6752A7E-0FD2-6626-80E3-CE4D4816D2B0}.Release|Any CPU.Build.0 = Release|Any CPU - {AF85AC87-521A-2F0E-5F10-836E416EC716}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AF85AC87-521A-2F0E-5F10-836E416EC716}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AF85AC87-521A-2F0E-5F10-836E416EC716}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AF85AC87-521A-2F0E-5F10-836E416EC716}.Release|Any CPU.Build.0 = Release|Any CPU - {FB946C57-55B3-08C6-18AE-1672D46C5308}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FB946C57-55B3-08C6-18AE-1672D46C5308}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FB946C57-55B3-08C6-18AE-1672D46C5308}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FB946C57-55B3-08C6-18AE-1672D46C5308}.Release|Any CPU.Build.0 = Release|Any CPU - {99A47EAA-44B8-8E06-DA0E-05B225009FDF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {99A47EAA-44B8-8E06-DA0E-05B225009FDF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {99A47EAA-44B8-8E06-DA0E-05B225009FDF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {99A47EAA-44B8-8E06-DA0E-05B225009FDF}.Release|Any CPU.Build.0 = Release|Any CPU - {4F0EF830-4308-347B-A31D-270A9812D15E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4F0EF830-4308-347B-A31D-270A9812D15E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4F0EF830-4308-347B-A31D-270A9812D15E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4F0EF830-4308-347B-A31D-270A9812D15E}.Release|Any CPU.Build.0 = Release|Any CPU - {B7EE2F70-A634-8B4D-93F4-EA1EEFD9E5E8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B7EE2F70-A634-8B4D-93F4-EA1EEFD9E5E8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B7EE2F70-A634-8B4D-93F4-EA1EEFD9E5E8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B7EE2F70-A634-8B4D-93F4-EA1EEFD9E5E8}.Release|Any CPU.Build.0 = Release|Any CPU - {A5298720-984E-6574-D41B-CFE7CA408182}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A5298720-984E-6574-D41B-CFE7CA408182}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A5298720-984E-6574-D41B-CFE7CA408182}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A5298720-984E-6574-D41B-CFE7CA408182}.Release|Any CPU.Build.0 = Release|Any CPU - {CB033CB6-F90B-E201-BA86-C867544E7247}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {CB033CB6-F90B-E201-BA86-C867544E7247}.Debug|Any CPU.Build.0 = Debug|Any CPU - {CB033CB6-F90B-E201-BA86-C867544E7247}.Release|Any CPU.ActiveCfg = Release|Any CPU - {CB033CB6-F90B-E201-BA86-C867544E7247}.Release|Any CPU.Build.0 = Release|Any CPU - {E9F5BFF2-0D0E-7B41-9AF0-83384F4B8825}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E9F5BFF2-0D0E-7B41-9AF0-83384F4B8825}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E9F5BFF2-0D0E-7B41-9AF0-83384F4B8825}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E9F5BFF2-0D0E-7B41-9AF0-83384F4B8825}.Release|Any CPU.Build.0 = Release|Any CPU - {668466AC-CD66-BAA0-0322-148549E373CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {668466AC-CD66-BAA0-0322-148549E373CB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {668466AC-CD66-BAA0-0322-148549E373CB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {668466AC-CD66-BAA0-0322-148549E373CB}.Release|Any CPU.Build.0 = Release|Any CPU - {07EBBFA6-798E-76A3-CAF0-67828B00B58E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {07EBBFA6-798E-76A3-CAF0-67828B00B58E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {07EBBFA6-798E-76A3-CAF0-67828B00B58E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {07EBBFA6-798E-76A3-CAF0-67828B00B58E}.Release|Any CPU.Build.0 = Release|Any CPU - {181ED0FE-FE20-069F-7CCF-86FF5449D7F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {181ED0FE-FE20-069F-7CCF-86FF5449D7F5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {181ED0FE-FE20-069F-7CCF-86FF5449D7F5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {181ED0FE-FE20-069F-7CCF-86FF5449D7F5}.Release|Any CPU.Build.0 = Release|Any CPU - {5E683B7C-B584-0E56-C8D6-D29050DE70FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5E683B7C-B584-0E56-C8D6-D29050DE70FB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5E683B7C-B584-0E56-C8D6-D29050DE70FB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5E683B7C-B584-0E56-C8D6-D29050DE70FB}.Release|Any CPU.Build.0 = Release|Any CPU - {4163E755-1563-6A72-60E7-BB2B69F5ABA2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4163E755-1563-6A72-60E7-BB2B69F5ABA2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4163E755-1563-6A72-60E7-BB2B69F5ABA2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4163E755-1563-6A72-60E7-BB2B69F5ABA2}.Release|Any CPU.Build.0 = Release|Any CPU - {AE6F3DA7-2993-6926-323E-A29295D55C36}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AE6F3DA7-2993-6926-323E-A29295D55C36}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AE6F3DA7-2993-6926-323E-A29295D55C36}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AE6F3DA7-2993-6926-323E-A29295D55C36}.Release|Any CPU.Build.0 = Release|Any CPU - {D013641A-8457-6215-05A1-74BB57B58409}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D013641A-8457-6215-05A1-74BB57B58409}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D013641A-8457-6215-05A1-74BB57B58409}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D013641A-8457-6215-05A1-74BB57B58409}.Release|Any CPU.Build.0 = Release|Any CPU - {4FC29140-9C5F-8DD5-B405-6982BD1DA5A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4FC29140-9C5F-8DD5-B405-6982BD1DA5A3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4FC29140-9C5F-8DD5-B405-6982BD1DA5A3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4FC29140-9C5F-8DD5-B405-6982BD1DA5A3}.Release|Any CPU.Build.0 = Release|Any CPU - {B9C9A1E4-3BB8-C8BE-7819-660A582D2952}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B9C9A1E4-3BB8-C8BE-7819-660A582D2952}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B9C9A1E4-3BB8-C8BE-7819-660A582D2952}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B9C9A1E4-3BB8-C8BE-7819-660A582D2952}.Release|Any CPU.Build.0 = Release|Any CPU - {2BBAB3B4-2E18-F945-F7AB-6207D7F72714}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2BBAB3B4-2E18-F945-F7AB-6207D7F72714}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2BBAB3B4-2E18-F945-F7AB-6207D7F72714}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2BBAB3B4-2E18-F945-F7AB-6207D7F72714}.Release|Any CPU.Build.0 = Release|Any CPU - {BA492274-A505-BCD5-3DA5-EE0C94DD5748}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {BA492274-A505-BCD5-3DA5-EE0C94DD5748}.Debug|Any CPU.Build.0 = Debug|Any CPU - {BA492274-A505-BCD5-3DA5-EE0C94DD5748}.Release|Any CPU.ActiveCfg = Release|Any CPU - {BA492274-A505-BCD5-3DA5-EE0C94DD5748}.Release|Any CPU.Build.0 = Release|Any CPU - {029F8300-57F5-9CCD-505E-708937686679}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {029F8300-57F5-9CCD-505E-708937686679}.Debug|Any CPU.Build.0 = Debug|Any CPU - {029F8300-57F5-9CCD-505E-708937686679}.Release|Any CPU.ActiveCfg = Release|Any CPU - {029F8300-57F5-9CCD-505E-708937686679}.Release|Any CPU.Build.0 = Release|Any CPU - {A5D2DB78-8045-29AC-E4B1-66E72F2C7FF0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A5D2DB78-8045-29AC-E4B1-66E72F2C7FF0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A5D2DB78-8045-29AC-E4B1-66E72F2C7FF0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A5D2DB78-8045-29AC-E4B1-66E72F2C7FF0}.Release|Any CPU.Build.0 = Release|Any CPU - {294792C0-DC28-3C5D-2D59-33DC99CD6C61}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {294792C0-DC28-3C5D-2D59-33DC99CD6C61}.Debug|Any CPU.Build.0 = Debug|Any CPU - {294792C0-DC28-3C5D-2D59-33DC99CD6C61}.Release|Any CPU.ActiveCfg = Release|Any CPU - {294792C0-DC28-3C5D-2D59-33DC99CD6C61}.Release|Any CPU.Build.0 = Release|Any CPU - {58D8630F-C0F4-B772-8572-BCC98FF0F0D8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {58D8630F-C0F4-B772-8572-BCC98FF0F0D8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {58D8630F-C0F4-B772-8572-BCC98FF0F0D8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {58D8630F-C0F4-B772-8572-BCC98FF0F0D8}.Release|Any CPU.Build.0 = Release|Any CPU - {2B1B4954-1241-8F2E-75B6-2146D15D037B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2B1B4954-1241-8F2E-75B6-2146D15D037B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2B1B4954-1241-8F2E-75B6-2146D15D037B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2B1B4954-1241-8F2E-75B6-2146D15D037B}.Release|Any CPU.Build.0 = Release|Any CPU - {97A9C869-F385-6711-6B76-F3859C86DCAC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {97A9C869-F385-6711-6B76-F3859C86DCAC}.Debug|Any CPU.Build.0 = Debug|Any CPU - {97A9C869-F385-6711-6B76-F3859C86DCAC}.Release|Any CPU.ActiveCfg = Release|Any CPU - {97A9C869-F385-6711-6B76-F3859C86DCAC}.Release|Any CPU.Build.0 = Release|Any CPU - {201CE292-0186-2A38-55D7-69890B5817DF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {201CE292-0186-2A38-55D7-69890B5817DF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {201CE292-0186-2A38-55D7-69890B5817DF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {201CE292-0186-2A38-55D7-69890B5817DF}.Release|Any CPU.Build.0 = Release|Any CPU - {17A00031-9FF7-4F73-5319-23FA5817625F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {17A00031-9FF7-4F73-5319-23FA5817625F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {17A00031-9FF7-4F73-5319-23FA5817625F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {17A00031-9FF7-4F73-5319-23FA5817625F}.Release|Any CPU.Build.0 = Release|Any CPU - {11E0E129-091F-BEEB-A1B2-9BAEF5BE9FBC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {11E0E129-091F-BEEB-A1B2-9BAEF5BE9FBC}.Debug|Any CPU.Build.0 = Debug|Any CPU - {11E0E129-091F-BEEB-A1B2-9BAEF5BE9FBC}.Release|Any CPU.ActiveCfg = Release|Any CPU - {11E0E129-091F-BEEB-A1B2-9BAEF5BE9FBC}.Release|Any CPU.Build.0 = Release|Any CPU - {AEF63403-4889-5396-CDEA-3B713CEF2ED7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AEF63403-4889-5396-CDEA-3B713CEF2ED7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AEF63403-4889-5396-CDEA-3B713CEF2ED7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AEF63403-4889-5396-CDEA-3B713CEF2ED7}.Release|Any CPU.Build.0 = Release|Any CPU - {D24E7862-3930-A4F6-1DFA-DA88C759546C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D24E7862-3930-A4F6-1DFA-DA88C759546C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D24E7862-3930-A4F6-1DFA-DA88C759546C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D24E7862-3930-A4F6-1DFA-DA88C759546C}.Release|Any CPU.Build.0 = Release|Any CPU - {6DC62619-949E-92E6-F4F1-5A0320959929}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6DC62619-949E-92E6-F4F1-5A0320959929}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6DC62619-949E-92E6-F4F1-5A0320959929}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6DC62619-949E-92E6-F4F1-5A0320959929}.Release|Any CPU.Build.0 = Release|Any CPU - {37F1D83D-073C-C165-4C53-664AD87628E6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {37F1D83D-073C-C165-4C53-664AD87628E6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {37F1D83D-073C-C165-4C53-664AD87628E6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {37F1D83D-073C-C165-4C53-664AD87628E6}.Release|Any CPU.Build.0 = Release|Any CPU - {CDC236E8-6881-46C4-EE95-3C386AF009D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {CDC236E8-6881-46C4-EE95-3C386AF009D0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {CDC236E8-6881-46C4-EE95-3C386AF009D0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {CDC236E8-6881-46C4-EE95-3C386AF009D0}.Release|Any CPU.Build.0 = Release|Any CPU - {ACC2785F-F4B9-13E4-EED2-C5D067242175}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {ACC2785F-F4B9-13E4-EED2-C5D067242175}.Debug|Any CPU.Build.0 = Debug|Any CPU - {ACC2785F-F4B9-13E4-EED2-C5D067242175}.Release|Any CPU.ActiveCfg = Release|Any CPU - {ACC2785F-F4B9-13E4-EED2-C5D067242175}.Release|Any CPU.Build.0 = Release|Any CPU - {7F4A3CA3-C3DA-A698-5CBC-54F28D1C7DFB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7F4A3CA3-C3DA-A698-5CBC-54F28D1C7DFB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7F4A3CA3-C3DA-A698-5CBC-54F28D1C7DFB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7F4A3CA3-C3DA-A698-5CBC-54F28D1C7DFB}.Release|Any CPU.Build.0 = Release|Any CPU - {DAA1F516-DEC3-7FF2-3A63-78DD97BA062C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {DAA1F516-DEC3-7FF2-3A63-78DD97BA062C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {DAA1F516-DEC3-7FF2-3A63-78DD97BA062C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {DAA1F516-DEC3-7FF2-3A63-78DD97BA062C}.Release|Any CPU.Build.0 = Release|Any CPU - {11EF0DE9-2648-F711-6194-70B5C40B3F3F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {11EF0DE9-2648-F711-6194-70B5C40B3F3F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {11EF0DE9-2648-F711-6194-70B5C40B3F3F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {11EF0DE9-2648-F711-6194-70B5C40B3F3F}.Release|Any CPU.Build.0 = Release|Any CPU - {01A21B47-07C5-6039-1B48-C5EACA3DBA2D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {01A21B47-07C5-6039-1B48-C5EACA3DBA2D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {01A21B47-07C5-6039-1B48-C5EACA3DBA2D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {01A21B47-07C5-6039-1B48-C5EACA3DBA2D}.Release|Any CPU.Build.0 = Release|Any CPU - {7CB7FEA8-8A12-A5D6-0057-AA65DB328617}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7CB7FEA8-8A12-A5D6-0057-AA65DB328617}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7CB7FEA8-8A12-A5D6-0057-AA65DB328617}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7CB7FEA8-8A12-A5D6-0057-AA65DB328617}.Release|Any CPU.Build.0 = Release|Any CPU - {0484DB46-3E40-1A10-131C-524AF1233EA7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0484DB46-3E40-1A10-131C-524AF1233EA7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0484DB46-3E40-1A10-131C-524AF1233EA7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0484DB46-3E40-1A10-131C-524AF1233EA7}.Release|Any CPU.Build.0 = Release|Any CPU - {64E1D9B1-B944-8AA3-799F-02E7DD33FB78}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {64E1D9B1-B944-8AA3-799F-02E7DD33FB78}.Debug|Any CPU.Build.0 = Debug|Any CPU - {64E1D9B1-B944-8AA3-799F-02E7DD33FB78}.Release|Any CPU.ActiveCfg = Release|Any CPU - {64E1D9B1-B944-8AA3-799F-02E7DD33FB78}.Release|Any CPU.Build.0 = Release|Any CPU - {D37991E1-585F-FF1B-9772-07477E40AF78}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D37991E1-585F-FF1B-9772-07477E40AF78}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D37991E1-585F-FF1B-9772-07477E40AF78}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D37991E1-585F-FF1B-9772-07477E40AF78}.Release|Any CPU.Build.0 = Release|Any CPU - {35A06F00-71AB-8A31-7D60-EBF41EA730CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {35A06F00-71AB-8A31-7D60-EBF41EA730CA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {35A06F00-71AB-8A31-7D60-EBF41EA730CA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {35A06F00-71AB-8A31-7D60-EBF41EA730CA}.Release|Any CPU.Build.0 = Release|Any CPU - {56120A54-1D4D-F07B-63B4-B15525C2ADD9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {56120A54-1D4D-F07B-63B4-B15525C2ADD9}.Debug|Any CPU.Build.0 = Debug|Any CPU - {56120A54-1D4D-F07B-63B4-B15525C2ADD9}.Release|Any CPU.ActiveCfg = Release|Any CPU - {56120A54-1D4D-F07B-63B4-B15525C2ADD9}.Release|Any CPU.Build.0 = Release|Any CPU - {BE47FB74-D163-0B1F-5293-0962EA7E8585}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {BE47FB74-D163-0B1F-5293-0962EA7E8585}.Debug|Any CPU.Build.0 = Debug|Any CPU - {BE47FB74-D163-0B1F-5293-0962EA7E8585}.Release|Any CPU.ActiveCfg = Release|Any CPU - {BE47FB74-D163-0B1F-5293-0962EA7E8585}.Release|Any CPU.Build.0 = Release|Any CPU - {9AD932E9-0986-654C-B454-34E654C80697}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9AD932E9-0986-654C-B454-34E654C80697}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9AD932E9-0986-654C-B454-34E654C80697}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9AD932E9-0986-654C-B454-34E654C80697}.Release|Any CPU.Build.0 = Release|Any CPU - {00BE2B68-FC96-23F8-F61D-EE53B7AE06A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {00BE2B68-FC96-23F8-F61D-EE53B7AE06A1}.Debug|Any CPU.Build.0 = Debug|Any CPU - {00BE2B68-FC96-23F8-F61D-EE53B7AE06A1}.Release|Any CPU.ActiveCfg = Release|Any CPU - {00BE2B68-FC96-23F8-F61D-EE53B7AE06A1}.Release|Any CPU.Build.0 = Release|Any CPU - {570BA050-81A7-46EB-3DDD-422027EE2CA2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {570BA050-81A7-46EB-3DDD-422027EE2CA2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {570BA050-81A7-46EB-3DDD-422027EE2CA2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {570BA050-81A7-46EB-3DDD-422027EE2CA2}.Release|Any CPU.Build.0 = Release|Any CPU - {6C43FD78-3478-F245-3EE4-E410D1E7D7C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6C43FD78-3478-F245-3EE4-E410D1E7D7C5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6C43FD78-3478-F245-3EE4-E410D1E7D7C5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6C43FD78-3478-F245-3EE4-E410D1E7D7C5}.Release|Any CPU.Build.0 = Release|Any CPU - {7F0FFA06-EAC8-CC9A-3386-389638F12B59}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7F0FFA06-EAC8-CC9A-3386-389638F12B59}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7F0FFA06-EAC8-CC9A-3386-389638F12B59}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7F0FFA06-EAC8-CC9A-3386-389638F12B59}.Release|Any CPU.Build.0 = Release|Any CPU - {03B9D4BE-348B-9AD6-6FE9-6C40AE22053D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {03B9D4BE-348B-9AD6-6FE9-6C40AE22053D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {03B9D4BE-348B-9AD6-6FE9-6C40AE22053D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {03B9D4BE-348B-9AD6-6FE9-6C40AE22053D}.Release|Any CPU.Build.0 = Release|Any CPU - {35CF4CF2-8A84-378D-32F0-572F4AA900A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {35CF4CF2-8A84-378D-32F0-572F4AA900A3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {35CF4CF2-8A84-378D-32F0-572F4AA900A3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {35CF4CF2-8A84-378D-32F0-572F4AA900A3}.Release|Any CPU.Build.0 = Release|Any CPU - {13E03C69-0634-3330-26D9-DCF7DD136BC5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {13E03C69-0634-3330-26D9-DCF7DD136BC5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {13E03C69-0634-3330-26D9-DCF7DD136BC5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {13E03C69-0634-3330-26D9-DCF7DD136BC5}.Release|Any CPU.Build.0 = Release|Any CPU - {A80D212B-7E80-4251-16C0-60FA3670A5B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A80D212B-7E80-4251-16C0-60FA3670A5B4}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A80D212B-7E80-4251-16C0-60FA3670A5B4}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A80D212B-7E80-4251-16C0-60FA3670A5B4}.Release|Any CPU.Build.0 = Release|Any CPU - {2F4ACEB8-76C7-D5A2-6DD1-2EF713D9A197}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2F4ACEB8-76C7-D5A2-6DD1-2EF713D9A197}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2F4ACEB8-76C7-D5A2-6DD1-2EF713D9A197}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2F4ACEB8-76C7-D5A2-6DD1-2EF713D9A197}.Release|Any CPU.Build.0 = Release|Any CPU - {C146A9AF-6C13-B9DC-F555-37182A54430F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C146A9AF-6C13-B9DC-F555-37182A54430F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C146A9AF-6C13-B9DC-F555-37182A54430F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C146A9AF-6C13-B9DC-F555-37182A54430F}.Release|Any CPU.Build.0 = Release|Any CPU - {E5025FCF-78CB-4B5B-3377-AE008B1BE9D2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E5025FCF-78CB-4B5B-3377-AE008B1BE9D2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E5025FCF-78CB-4B5B-3377-AE008B1BE9D2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E5025FCF-78CB-4B5B-3377-AE008B1BE9D2}.Release|Any CPU.Build.0 = Release|Any CPU - {52698305-D6F8-C13C-0882-48FC37726404}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {52698305-D6F8-C13C-0882-48FC37726404}.Debug|Any CPU.Build.0 = Debug|Any CPU - {52698305-D6F8-C13C-0882-48FC37726404}.Release|Any CPU.ActiveCfg = Release|Any CPU - {52698305-D6F8-C13C-0882-48FC37726404}.Release|Any CPU.Build.0 = Release|Any CPU - {DE10AF97-E790-9D19-2399-70940A9B83A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {DE10AF97-E790-9D19-2399-70940A9B83A7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {DE10AF97-E790-9D19-2399-70940A9B83A7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {DE10AF97-E790-9D19-2399-70940A9B83A7}.Release|Any CPU.Build.0 = Release|Any CPU - {5567139C-0365-B6A0-5DD0-978A09B9F176}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5567139C-0365-B6A0-5DD0-978A09B9F176}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5567139C-0365-B6A0-5DD0-978A09B9F176}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5567139C-0365-B6A0-5DD0-978A09B9F176}.Release|Any CPU.Build.0 = Release|Any CPU - {A56C1F0E-3E18-DBEE-7F97-B5FCBF23D1D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A56C1F0E-3E18-DBEE-7F97-B5FCBF23D1D6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A56C1F0E-3E18-DBEE-7F97-B5FCBF23D1D6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A56C1F0E-3E18-DBEE-7F97-B5FCBF23D1D6}.Release|Any CPU.Build.0 = Release|Any CPU - {256D269B-35EA-F833-2F1D-8E0058908DEE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {256D269B-35EA-F833-2F1D-8E0058908DEE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {256D269B-35EA-F833-2F1D-8E0058908DEE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {256D269B-35EA-F833-2F1D-8E0058908DEE}.Release|Any CPU.Build.0 = Release|Any CPU - {F02B63CD-2C69-61F7-7F96-930122D4D4D7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F02B63CD-2C69-61F7-7F96-930122D4D4D7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F02B63CD-2C69-61F7-7F96-930122D4D4D7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F02B63CD-2C69-61F7-7F96-930122D4D4D7}.Release|Any CPU.Build.0 = Release|Any CPU - {F061C879-063E-99DE-B301-E261DB12156F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F061C879-063E-99DE-B301-E261DB12156F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F061C879-063E-99DE-B301-E261DB12156F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F061C879-063E-99DE-B301-E261DB12156F}.Release|Any CPU.Build.0 = Release|Any CPU - {6E9C9582-67FA-2EB1-C6BA-AD4CD326E276}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6E9C9582-67FA-2EB1-C6BA-AD4CD326E276}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6E9C9582-67FA-2EB1-C6BA-AD4CD326E276}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6E9C9582-67FA-2EB1-C6BA-AD4CD326E276}.Release|Any CPU.Build.0 = Release|Any CPU - {FCF711C2-1090-7204-5E38-4BEFBE265A61}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FCF711C2-1090-7204-5E38-4BEFBE265A61}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FCF711C2-1090-7204-5E38-4BEFBE265A61}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FCF711C2-1090-7204-5E38-4BEFBE265A61}.Release|Any CPU.Build.0 = Release|Any CPU - {3A4AAE04-FA0C-2AF8-6548-AC22E5A41312}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3A4AAE04-FA0C-2AF8-6548-AC22E5A41312}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3A4AAE04-FA0C-2AF8-6548-AC22E5A41312}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3A4AAE04-FA0C-2AF8-6548-AC22E5A41312}.Release|Any CPU.Build.0 = Release|Any CPU - {66F8F288-C387-40E0-5F83-938671335703}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {66F8F288-C387-40E0-5F83-938671335703}.Debug|Any CPU.Build.0 = Debug|Any CPU - {66F8F288-C387-40E0-5F83-938671335703}.Release|Any CPU.ActiveCfg = Release|Any CPU - {66F8F288-C387-40E0-5F83-938671335703}.Release|Any CPU.Build.0 = Release|Any CPU - {7B3BDB83-918F-6760-3853-BDD70CD71B42}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7B3BDB83-918F-6760-3853-BDD70CD71B42}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7B3BDB83-918F-6760-3853-BDD70CD71B42}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7B3BDB83-918F-6760-3853-BDD70CD71B42}.Release|Any CPU.Build.0 = Release|Any CPU - {2669C700-5CFF-0186-F65E-8D26BE06E934}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2669C700-5CFF-0186-F65E-8D26BE06E934}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2669C700-5CFF-0186-F65E-8D26BE06E934}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2669C700-5CFF-0186-F65E-8D26BE06E934}.Release|Any CPU.Build.0 = Release|Any CPU - {0560BD84-CDBC-A79A-C665-55F6D62825EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0560BD84-CDBC-A79A-C665-55F6D62825EA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0560BD84-CDBC-A79A-C665-55F6D62825EA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0560BD84-CDBC-A79A-C665-55F6D62825EA}.Release|Any CPU.Build.0 = Release|Any CPU - {783A67C9-3381-6E4C-3752-423F0FC6F6F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {783A67C9-3381-6E4C-3752-423F0FC6F6F9}.Debug|Any CPU.Build.0 = Debug|Any CPU - {783A67C9-3381-6E4C-3752-423F0FC6F6F9}.Release|Any CPU.ActiveCfg = Release|Any CPU - {783A67C9-3381-6E4C-3752-423F0FC6F6F9}.Release|Any CPU.Build.0 = Release|Any CPU - {F890BD12-6CF5-4F80-9099-B7FE9A908432}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F890BD12-6CF5-4F80-9099-B7FE9A908432}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F890BD12-6CF5-4F80-9099-B7FE9A908432}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F890BD12-6CF5-4F80-9099-B7FE9A908432}.Release|Any CPU.Build.0 = Release|Any CPU - {505C6840-5113-26EC-CEDB-D07EEABEF94B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {505C6840-5113-26EC-CEDB-D07EEABEF94B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {505C6840-5113-26EC-CEDB-D07EEABEF94B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {505C6840-5113-26EC-CEDB-D07EEABEF94B}.Release|Any CPU.Build.0 = Release|Any CPU - {A79CBC0C-5313-4ECF-A24E-27CE236BCF2C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A79CBC0C-5313-4ECF-A24E-27CE236BCF2C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A79CBC0C-5313-4ECF-A24E-27CE236BCF2C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A79CBC0C-5313-4ECF-A24E-27CE236BCF2C}.Release|Any CPU.Build.0 = Release|Any CPU - {0AF13355-173C-3128-5AFC-D32E540DA3EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0AF13355-173C-3128-5AFC-D32E540DA3EF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0AF13355-173C-3128-5AFC-D32E540DA3EF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0AF13355-173C-3128-5AFC-D32E540DA3EF}.Release|Any CPU.Build.0 = Release|Any CPU - {AF043113-CCE3-59C1-DF71-9804155F26A8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AF043113-CCE3-59C1-DF71-9804155F26A8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AF043113-CCE3-59C1-DF71-9804155F26A8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AF043113-CCE3-59C1-DF71-9804155F26A8}.Release|Any CPU.Build.0 = Release|Any CPU - {15602821-2ABA-14BB-738D-1A53E1976E07}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {15602821-2ABA-14BB-738D-1A53E1976E07}.Debug|Any CPU.Build.0 = Debug|Any CPU - {15602821-2ABA-14BB-738D-1A53E1976E07}.Release|Any CPU.ActiveCfg = Release|Any CPU - {15602821-2ABA-14BB-738D-1A53E1976E07}.Release|Any CPU.Build.0 = Release|Any CPU - {1D761F8B-921C-53BF-DCF5-5ABD329EEB0C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1D761F8B-921C-53BF-DCF5-5ABD329EEB0C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1D761F8B-921C-53BF-DCF5-5ABD329EEB0C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1D761F8B-921C-53BF-DCF5-5ABD329EEB0C}.Release|Any CPU.Build.0 = Release|Any CPU - {DA7634C2-9156-9B79-7A1D-90D8E605DC8A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {DA7634C2-9156-9B79-7A1D-90D8E605DC8A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {DA7634C2-9156-9B79-7A1D-90D8E605DC8A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {DA7634C2-9156-9B79-7A1D-90D8E605DC8A}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {1B2C9807-0067-AAD6-69CD-7FD799689BDD} = {EB157E4F-3EA5-5CBF-9694-18E3FA265E53} - {4B16E448-1B2D-28B7-2417-D2D191FE524F} = {EB157E4F-3EA5-5CBF-9694-18E3FA265E53} - {9EAD58C8-4DCD-4933-B9F9-7D211C66D73F} = {EB157E4F-3EA5-5CBF-9694-18E3FA265E53} - {0E5FA5F8-5C99-7868-B1D0-1D38A9624795} = {EB157E4F-3EA5-5CBF-9694-18E3FA265E53} - {F310596E-88BB-9E54-885E-21C61971917E} = {5B52EF8A-3661-DCFF-797D-BC4D6AC60BDA} - {EA6E5683-3A20-2E52-1CE6-AE0D6D36AC4D} = {F310596E-88BB-9E54-885E-21C61971917E} - {D9492ED1-A812-924B-65E4-F518592B49BB} = {F310596E-88BB-9E54-885E-21C61971917E} - {3823DE1E-2ACE-C956-99E1-00DB786D9E1D} = {D9492ED1-A812-924B-65E4-F518592B49BB} - {03DFF14F-7321-1784-D4C7-4E99D4120F48} = {5B52EF8A-3661-DCFF-797D-BC4D6AC60BDA} - {BDD326D6-7616-84F0-B914-74743BFBA520} = {03DFF14F-7321-1784-D4C7-4E99D4120F48} - {EC506DBE-AB6D-492E-786E-8B176021BF2E} = {BDD326D6-7616-84F0-B914-74743BFBA520} - {5AC09D9A-F2A5-9CFA-B3C5-8D25F257651C} = {5B52EF8A-3661-DCFF-797D-BC4D6AC60BDA} - {33B1AE27-692A-1778-48C1-CCEC2B9BC78F} = {5AC09D9A-F2A5-9CFA-B3C5-8D25F257651C} - {018E0E11-1CCE-A2BE-641D-21EE14D2E90D} = {5AC09D9A-F2A5-9CFA-B3C5-8D25F257651C} - {5F27FB4E-CF09-3A6B-F5B4-BF5A709FA609} = {33B1AE27-692A-1778-48C1-CCEC2B9BC78F} - {AB67BDB9-D701-3AC9-9CDF-ECCDCCD8DB6D} = {5AC09D9A-F2A5-9CFA-B3C5-8D25F257651C} - {3F605548-87E2-8A1D-306D-0CE6960B8242} = {AB67BDB9-D701-3AC9-9CDF-ECCDCCD8DB6D} - {45F7FA87-7451-6970-7F6E-F8BAE45E081B} = {AB67BDB9-D701-3AC9-9CDF-ECCDCCD8DB6D} - {C1DCEFBD-12A5-EAAE-632E-8EEB9BE491B6} = {5B52EF8A-3661-DCFF-797D-BC4D6AC60BDA} - {A6928CBA-4D4D-AB2B-CA19-FEE6E73ECE70} = {C1DCEFBD-12A5-EAAE-632E-8EEB9BE491B6} - {F2E6CB0E-DF77-1FAA-582B-62B040DF3848} = {A6928CBA-4D4D-AB2B-CA19-FEE6E73ECE70} - {C494ECBE-DEA5-3576-D2AF-200FF12BC144} = {A6928CBA-4D4D-AB2B-CA19-FEE6E73ECE70} - {7E890DF9-B715-B6DF-2498-FD74DDA87D71} = {A6928CBA-4D4D-AB2B-CA19-FEE6E73ECE70} - {64689413-46D7-8499-68A6-B6367ACBC597} = {A6928CBA-4D4D-AB2B-CA19-FEE6E73ECE70} - {5827F4DE-0AA7-FC85-641D-09E3D890DB27} = {C1DCEFBD-12A5-EAAE-632E-8EEB9BE491B6} - {9BD75659-58CB-06D1-E198-C39007E82C6A} = {5827F4DE-0AA7-FC85-641D-09E3D890DB27} - {7BF13935-F1DD-D23B-8347-DB1550C69D69} = {5827F4DE-0AA7-FC85-641D-09E3D890DB27} - {2949F2E7-0E10-76D0-5672-8B1662588F74} = {5B52EF8A-3661-DCFF-797D-BC4D6AC60BDA} - {3ADE81EE-6F71-CB5C-016B-36E8AC854713} = {2949F2E7-0E10-76D0-5672-8B1662588F74} - {24D60BC6-6A93-C97D-1238-113DDB928700} = {3ADE81EE-6F71-CB5C-016B-36E8AC854713} - {CCF230F8-F75D-A766-7EAE-0C9FEF5AF6C2} = {3ADE81EE-6F71-CB5C-016B-36E8AC854713} - {066DF6C9-826C-F223-47D2-BDF53D59F6C3} = {3ADE81EE-6F71-CB5C-016B-36E8AC854713} - {61C7FDA3-83AA-3EE6-6321-1C1ACD1073DF} = {3ADE81EE-6F71-CB5C-016B-36E8AC854713} - {BF6C9274-4DBD-2FDE-B94C-1B208F6C53BC} = {3ADE81EE-6F71-CB5C-016B-36E8AC854713} - {4EAAC62E-EBD2-DFF1-7B37-7E131C75DEC3} = {3ADE81EE-6F71-CB5C-016B-36E8AC854713} - {157C3671-CA0B-69FA-A7C9-74A1FDA97B99} = {5B52EF8A-3661-DCFF-797D-BC4D6AC60BDA} - {F39E09D6-BF93-B64A-CFE7-2BA92815C0FE} = {157C3671-CA0B-69FA-A7C9-74A1FDA97B99} - {39EFDA5B-F5EE-8212-D5BA-90E1B82013E7} = {F39E09D6-BF93-B64A-CFE7-2BA92815C0FE} - {3B82DBF3-3DAE-EA97-85F4-6DCFA09940DF} = {F39E09D6-BF93-B64A-CFE7-2BA92815C0FE} - {6844B539-C2A3-9D4F-139D-9D533BCABADA} = {F39E09D6-BF93-B64A-CFE7-2BA92815C0FE} - {4263AA71-0335-3F44-9A9B-423C3A3D05E6} = {F39E09D6-BF93-B64A-CFE7-2BA92815C0FE} - {F1B1DB47-D2D7-59CB-679B-23E4928E8328} = {F39E09D6-BF93-B64A-CFE7-2BA92815C0FE} - {BC35DE94-4F04-3436-27A3-F11647FEDD5C} = {F39E09D6-BF93-B64A-CFE7-2BA92815C0FE} - {864C8B80-771A-0C15-30A5-558F99006E0D} = {F39E09D6-BF93-B64A-CFE7-2BA92815C0FE} - {603E7A23-1D6B-D3A9-B0E6-3E332B13ED5C} = {F39E09D6-BF93-B64A-CFE7-2BA92815C0FE} - {D2F7E58B-47D4-5205-D917-144CA1CFF4F1} = {F39E09D6-BF93-B64A-CFE7-2BA92815C0FE} - {1DCF4EBB-DBC4-752C-13D4-D1EECE4E8907} = {F39E09D6-BF93-B64A-CFE7-2BA92815C0FE} - {1B37A859-E733-60CB-4806-1A24B6F10E05} = {F39E09D6-BF93-B64A-CFE7-2BA92815C0FE} - {F2B58F4E-6F28-A25F-5BFB-CDEBAD6B9A3E} = {F39E09D6-BF93-B64A-CFE7-2BA92815C0FE} - {7D49FA52-6EA1-EAC8-4C5A-AC07188D6C57} = {5B52EF8A-3661-DCFF-797D-BC4D6AC60BDA} - {C9CF27FC-12DB-954F-863C-576BA8E309A5} = {7D49FA52-6EA1-EAC8-4C5A-AC07188D6C57} - {6DCAF6F3-717F-27A9-D96C-F2BFA5550347} = {C9CF27FC-12DB-954F-863C-576BA8E309A5} - {C4A90603-BE42-0044-CAB4-3EB910AD51A5} = {5B52EF8A-3661-DCFF-797D-BC4D6AC60BDA} - {054761F9-16D3-B2F8-6F4D-EFC2248805CD} = {C4A90603-BE42-0044-CAB4-3EB910AD51A5} - {B54CE64C-4167-1DD1-B7D6-2FD7A5AEF715} = {C4A90603-BE42-0044-CAB4-3EB910AD51A5} - {D2162FEA-AFA4-2A88-6444-2F6D845260BB} = {5B52EF8A-3661-DCFF-797D-BC4D6AC60BDA} - {63EAEA3B-ADC9-631D-774E-7AA04490EDDD} = {D2162FEA-AFA4-2A88-6444-2F6D845260BB} - {B0F64757-F7A7-1A11-8DEC-BAC72EB5EC29} = {63EAEA3B-ADC9-631D-774E-7AA04490EDDD} - {8E6B774C-CC4E-CE7C-AD4B-8AF7C92889A6} = {5B52EF8A-3661-DCFF-797D-BC4D6AC60BDA} - {BC12ED55-6015-7C8B-8384-B39CE93C76D6} = {8E6B774C-CC4E-CE7C-AD4B-8AF7C92889A6} - {FF70543D-AFF9-1D38-4950-4F8EE18D60BB} = {8E6B774C-CC4E-CE7C-AD4B-8AF7C92889A6} - {831265B0-8896-9C95-3488-E12FD9F6DC53} = {FF70543D-AFF9-1D38-4950-4F8EE18D60BB} - {316BBD0A-04D2-85C9-52EA-7993CC6C8930} = {5B52EF8A-3661-DCFF-797D-BC4D6AC60BDA} - {9D6AB85A-85EA-D85A-5566-A121D34016E6} = {316BBD0A-04D2-85C9-52EA-7993CC6C8930} - {FC018E5B-1E2F-DE19-1E97-0C845058C469} = {5B52EF8A-3661-DCFF-797D-BC4D6AC60BDA} - {1BE5B76C-B486-560B-6CB2-44C6537249AA} = {FC018E5B-1E2F-DE19-1E97-0C845058C469} - {F4F1CBE2-1CDD-CAA4-41F0-266DB4677C05} = {1BE5B76C-B486-560B-6CB2-44C6537249AA} - {3DE1DCDC-C845-4AC7-7B66-34B0A9E8626B} = {1BE5B76C-B486-560B-6CB2-44C6537249AA} - {6FA01E92-606B-0CB8-8583-6F693A903CFC} = {1BE5B76C-B486-560B-6CB2-44C6537249AA} - {A5994E92-7E0E-89FE-5628-DE1A0176B8BA} = {1BE5B76C-B486-560B-6CB2-44C6537249AA} - {54C11B29-4C54-7255-AB44-BEB63AF9BD1F} = {1BE5B76C-B486-560B-6CB2-44C6537249AA} - {AD65DDE7-9FEA-7380-8C10-FA165F745354} = {5B52EF8A-3661-DCFF-797D-BC4D6AC60BDA} - {076B8074-5735-5367-1EEA-CA16A5B8ABD7} = {AD65DDE7-9FEA-7380-8C10-FA165F745354} - {3247EE0D-B3E9-9C11-B0AE-FE719410390B} = {5B52EF8A-3661-DCFF-797D-BC4D6AC60BDA} - {CD7C09DA-FEC8-2CC5-D00C-E525638DFF4A} = {3247EE0D-B3E9-9C11-B0AE-FE719410390B} - {79B10804-91E9-972E-1913-EE0F0B11663E} = {CD7C09DA-FEC8-2CC5-D00C-E525638DFF4A} - {A4E2971A-7DCF-D72F-631D-98564D1D1E5D} = {5B52EF8A-3661-DCFF-797D-BC4D6AC60BDA} - {01B2B5BA-58CD-9920-5A1E-6B801BF36685} = {A4E2971A-7DCF-D72F-631D-98564D1D1E5D} - {3C8A19A6-5578-51E9-E592-6DABBE5739E5} = {01B2B5BA-58CD-9920-5A1E-6B801BF36685} - {8403C362-DC2B-AF41-C9D8-E4F2D892DF8E} = {5B52EF8A-3661-DCFF-797D-BC4D6AC60BDA} - {B75836D4-A244-20B8-4707-64E17C725DAA} = {8403C362-DC2B-AF41-C9D8-E4F2D892DF8E} - {0910C958-24C8-947F-359A-218ED1199AAE} = {B75836D4-A244-20B8-4707-64E17C725DAA} - {1345DD29-BB3A-FB5F-4B3D-E29F6045A27A} = {5B52EF8A-3661-DCFF-797D-BC4D6AC60BDA} - {9C2DD234-FA33-FDB6-86F0-EF9B75A13450} = {1345DD29-BB3A-FB5F-4B3D-E29F6045A27A} - {79E122F4-2325-3E92-438E-5825A307B594} = {1345DD29-BB3A-FB5F-4B3D-E29F6045A27A} - {538E2D98-5325-3F54-BE74-EFE5FC1ECBD8} = {1345DD29-BB3A-FB5F-4B3D-E29F6045A27A} - {66557252-B5C4-664B-D807-07018C627474} = {1345DD29-BB3A-FB5F-4B3D-E29F6045A27A} - {7203223D-FF02-7BEB-2798-D1639ACC01C4} = {1345DD29-BB3A-FB5F-4B3D-E29F6045A27A} - {5AC9EE40-1881-5F8A-46A2-2C303950D3C8} = {1345DD29-BB3A-FB5F-4B3D-E29F6045A27A} - {927E3CD3-4C20-4DE5-A395-D0977152A8D3} = {1345DD29-BB3A-FB5F-4B3D-E29F6045A27A} - {3C69853C-90E3-D889-1960-3B9229882590} = {1345DD29-BB3A-FB5F-4B3D-E29F6045A27A} - {9FB0DDD7-7A77-8DA4-F9E2-A94E60ED8FC7} = {1345DD29-BB3A-FB5F-4B3D-E29F6045A27A} - {643E4D4C-BC96-A37F-E0EC-488127F0B127} = {1345DD29-BB3A-FB5F-4B3D-E29F6045A27A} - {6F2CA7F5-3E7C-C61B-94E6-E7DD1227B5B1} = {1345DD29-BB3A-FB5F-4B3D-E29F6045A27A} - {F04B7DBB-77A5-C978-B2DE-8C189A32AA72} = {1345DD29-BB3A-FB5F-4B3D-E29F6045A27A} - {7C72F22A-20FF-DF5B-9191-6DFD0D497DB2} = {1345DD29-BB3A-FB5F-4B3D-E29F6045A27A} - {C896CC0A-F5E6-9AA4-C582-E691441F8D32} = {1345DD29-BB3A-FB5F-4B3D-E29F6045A27A} - {0AA3A418-AB45-CCA4-46D4-EEBFE011FECA} = {1345DD29-BB3A-FB5F-4B3D-E29F6045A27A} - {225D9926-4AE8-E539-70AD-8698E688F271} = {1345DD29-BB3A-FB5F-4B3D-E29F6045A27A} - {D6E8E69C-F721-BBCB-8C39-9716D53D72AD} = {1345DD29-BB3A-FB5F-4B3D-E29F6045A27A} - {9529EE99-D6A5-B570-EB1F-15BD2D57DFE2} = {1345DD29-BB3A-FB5F-4B3D-E29F6045A27A} - {589A43FD-8213-E9E3-6CFF-9CBA72D53E98} = {1345DD29-BB3A-FB5F-4B3D-E29F6045A27A} - {2BACF7E3-1278-FE99-8343-8221E6FBA9DE} = {1345DD29-BB3A-FB5F-4B3D-E29F6045A27A} - {75E47125-E4D7-8482-F1A4-726564970864} = {1345DD29-BB3A-FB5F-4B3D-E29F6045A27A} - {FCD529E0-DD17-6587-B29C-12D425C0AD0C} = {1345DD29-BB3A-FB5F-4B3D-E29F6045A27A} - {61B23570-4F2D-B060-BE1F-37995682E494} = {1345DD29-BB3A-FB5F-4B3D-E29F6045A27A} - {1182764D-2143-EEF0-9270-3DCE392F5D06} = {1345DD29-BB3A-FB5F-4B3D-E29F6045A27A} - {772B02B5-6280-E1D4-3E2E-248D0455C2FB} = {1345DD29-BB3A-FB5F-4B3D-E29F6045A27A} - {48F90289-938C-CCA7-B60F-D2143E7C9A69} = {1345DD29-BB3A-FB5F-4B3D-E29F6045A27A} - {E69FA1A0-6D1B-A6E4-2DC0-8F4C5F21BF04} = {1345DD29-BB3A-FB5F-4B3D-E29F6045A27A} - {083067CF-CE89-EF39-9BD3-4741919E26F3} = {1345DD29-BB3A-FB5F-4B3D-E29F6045A27A} - {8380A20C-A5B8-EE91-1A58-270323688CB9} = {1345DD29-BB3A-FB5F-4B3D-E29F6045A27A} - {A7542386-71EB-4F34-E1CE-27D399325955} = {1345DD29-BB3A-FB5F-4B3D-E29F6045A27A} - {90659617-4DF7-809A-4E5B-29BB5A98E8E1} = {5B52EF8A-3661-DCFF-797D-BC4D6AC60BDA} - {AB8B269C-5A2A-A4B8-0488-B5F81E55B4D9} = {90659617-4DF7-809A-4E5B-29BB5A98E8E1} - {CEDC2447-F717-3C95-7E08-F214D575A7B7} = {AB8B269C-5A2A-A4B8-0488-B5F81E55B4D9} - {2F4EB1B2-4185-C535-85ED-53EA2D2C0573} = {A5C98087-E847-D2C4-2143-20869479839D} - {79987C33-9903-4F75-1045-284F537155FE} = {A5C98087-E847-D2C4-2143-20869479839D} - {730E1137-BA40-3899-17CB-93922F4DA4CA} = {A5C98087-E847-D2C4-2143-20869479839D} - {EEF284CB-436C-439C-9606-E4DD1FD5F6D3} = {A5C98087-E847-D2C4-2143-20869479839D} - {891DD979-9F3C-981D-FB41-EB66B7C61938} = {A5C98087-E847-D2C4-2143-20869479839D} - {E5B2A8AA-8162-7E98-9D31-3DA40BB8EDC2} = {A5C98087-E847-D2C4-2143-20869479839D} - {29E594C5-9DE1-D06F-4F19-4166C3935CAE} = {A5C98087-E847-D2C4-2143-20869479839D} - {BE6FF48D-FDD8-A0C3-69E0-CAE30D3FC13F} = {A5C98087-E847-D2C4-2143-20869479839D} - {8FF79C15-5E56-BB59-A473-0E42F8395C89} = {A5C98087-E847-D2C4-2143-20869479839D} - {1003D3C6-91C9-D165-8325-20F3A16D9A5B} = {A5C98087-E847-D2C4-2143-20869479839D} - {278C2043-B1CE-CC57-7F08-9C4B69C7A65D} = {A5C98087-E847-D2C4-2143-20869479839D} - {F783A1A9-4991-592A-57C3-E18C41986545} = {A5C98087-E847-D2C4-2143-20869479839D} - {9646F3EA-8DFC-F221-D415-D489C166889B} = {A5C98087-E847-D2C4-2143-20869479839D} - {079CEB57-D0A3-20A1-131E-37D0CA4C0410} = {A5C98087-E847-D2C4-2143-20869479839D} - {49ECEC98-A019-AEB6-1632-D05B0DCB6EEA} = {A5C98087-E847-D2C4-2143-20869479839D} - {7C05D5D7-330C-204C-430C-8B2B27AB9995} = {A5C98087-E847-D2C4-2143-20869479839D} - {AA9E4361-01CE-AC2F-E20D-0CA5C1D438CC} = {A5C98087-E847-D2C4-2143-20869479839D} - {3C0D80C5-B033-CE52-8868-614826F45D72} = {A5C98087-E847-D2C4-2143-20869479839D} - {C28FED05-DEDA-E1BA-F538-4BCAA8DAF2FE} = {A5C98087-E847-D2C4-2143-20869479839D} - {53094E1B-EF1A-C3F8-6EB5-45A43B0B8A9C} = {A5C98087-E847-D2C4-2143-20869479839D} - {9AA450C7-BBED-3722-DB4B-4DA97D885E00} = {A5C98087-E847-D2C4-2143-20869479839D} - {31219219-DCBE-766A-A54F-4975DA7BBB80} = {A5C98087-E847-D2C4-2143-20869479839D} - {0CBB3D0E-CC76-CDE9-4A2D-CE5F73B59420} = {A5C98087-E847-D2C4-2143-20869479839D} - {66B51CD4-BCE3-757B-BEFC-0F2BF96C7D52} = {A5C98087-E847-D2C4-2143-20869479839D} - {3C13055F-5B62-4AF8-F8BB-83CFDDEEDDCF} = {A5C98087-E847-D2C4-2143-20869479839D} - {4DAC6B77-D825-3085-4263-3226D14F61AC} = {A5C98087-E847-D2C4-2143-20869479839D} - {12AF40C6-8379-4786-4DF4-B8CCCC54E2A8} = {A5C98087-E847-D2C4-2143-20869479839D} - {160F000D-9DA4-3AAA-8370-15C6BF460E1B} = {A5C98087-E847-D2C4-2143-20869479839D} - {5F222E3F-1E9A-331A-F5BC-7C22CC4DC27B} = {A5C98087-E847-D2C4-2143-20869479839D} - {7E234D3A-B714-8683-EF17-12E573F8796B} = {A5C98087-E847-D2C4-2143-20869479839D} - {A92B2733-D971-E795-C42E-763B8D28BE6A} = {A5C98087-E847-D2C4-2143-20869479839D} - {56F1436F-34A3-0E62-F76D-7D33B0F6CF9A} = {A5C98087-E847-D2C4-2143-20869479839D} - {A5F6CD9E-7F26-36C4-0785-86FECE0484FE} = {A5C98087-E847-D2C4-2143-20869479839D} - {340ACF7A-5050-5BEF-8E3E-56EFD1EA2CFF} = {A5C98087-E847-D2C4-2143-20869479839D} - {6C13C210-5443-5941-7273-A830E6A0F2FD} = {A5C98087-E847-D2C4-2143-20869479839D} - {7CC450E2-1730-BE2C-9AE2-AF8567982B8E} = {A5C98087-E847-D2C4-2143-20869479839D} - {680B3990-2960-BCCC-7C3B-2C4BAB9B8134} = {A5C98087-E847-D2C4-2143-20869479839D} - {20D1D291-07F1-1389-74B4-F82B49847CED} = {A5C98087-E847-D2C4-2143-20869479839D} - {7A7A0652-CB76-9B73-1FD1-D67F5510DB1F} = {A5C98087-E847-D2C4-2143-20869479839D} - {4BE45036-7A8F-9072-5160-3BB53505A68F} = {A5C98087-E847-D2C4-2143-20869479839D} - {C484B2AA-2582-1395-840E-61DE9FED5313} = {A5C98087-E847-D2C4-2143-20869479839D} - {A81B4B21-F490-7DE9-024B-4959F9172735} = {A5C98087-E847-D2C4-2143-20869479839D} - {68C21EB9-A635-48B1-98E8-A0DD36C545F5} = {A5C98087-E847-D2C4-2143-20869479839D} - {7A608DF5-E8F5-38DA-4E15-2A7A2B6C508F} = {A5C98087-E847-D2C4-2143-20869479839D} - {0D24FEF1-02AE-8265-6524-64DE307CB4CE} = {A5C98087-E847-D2C4-2143-20869479839D} - {7DB7DF6E-FCAA-E569-29D8-C0C4FBFC5625} = {A5C98087-E847-D2C4-2143-20869479839D} - {D655B564-92E5-1E96-7A72-112F368418C4} = {A5C98087-E847-D2C4-2143-20869479839D} - {BAF3E8BB-263A-8E31-3576-D18BE2B46A22} = {A5C98087-E847-D2C4-2143-20869479839D} - {DB888E44-BB55-C4A2-CDFF-7B04F0E2D6E5} = {A5C98087-E847-D2C4-2143-20869479839D} - {56026A9E-B2DA-48EC-60C3-3193857A238C} = {A5C98087-E847-D2C4-2143-20869479839D} - {3B2531C6-790E-BD6E-AE00-3F5F95EFE5F9} = {BB76B5A5-14BA-E317-828D-110B711D71F5} - {75E6454F-DC5F-C6E5-CB1D-47AA2AB72FDF} = {BB76B5A5-14BA-E317-828D-110B711D71F5} - {E49112A1-06DA-43F4-4A8D-5E805A6466FC} = {BB76B5A5-14BA-E317-828D-110B711D71F5} - {446F1BC4-7609-555E-1FA7-901560438951} = {BB76B5A5-14BA-E317-828D-110B711D71F5} - {327615A7-CCA8-CAAA-A279-54A2EB3C8D8C} = {BB76B5A5-14BA-E317-828D-110B711D71F5} - {1006432F-63A0-8569-EEC7-4393A1918480} = {BB76B5A5-14BA-E317-828D-110B711D71F5} - {4B6B949F-4BF5-2817-4818-71E26BF57749} = {BB76B5A5-14BA-E317-828D-110B711D71F5} - {FBBE5FEA-8CD5-8521-DC93-06C239F7876D} = {BB76B5A5-14BA-E317-828D-110B711D71F5} - {1F48E1F9-072B-1F1D-7E58-9C065D0F4ED1} = {BB76B5A5-14BA-E317-828D-110B711D71F5} - {5CC00E13-DAA8-7285-CDB0-D482D750FFBD} = {BB76B5A5-14BA-E317-828D-110B711D71F5} - {D2BC4D25-D54D-1778-798D-6547A38EEB05} = {BB76B5A5-14BA-E317-828D-110B711D71F5} - {7AC29AE8-7BDA-4243-5869-D89EF3921DC3} = {BB76B5A5-14BA-E317-828D-110B711D71F5} - {6E200246-2354-ECCC-0D81-F58C27F7551C} = {BB76B5A5-14BA-E317-828D-110B711D71F5} - {2F13F88C-7820-12E7-1FEF-4DB0DDE34DAB} = {BB76B5A5-14BA-E317-828D-110B711D71F5} - {ED480238-AEBE-3C29-8D80-5C0AB45E13D3} = {BB76B5A5-14BA-E317-828D-110B711D71F5} - {633ED6DB-FFB9-F767-070D-09C342DB4B74} = {BB76B5A5-14BA-E317-828D-110B711D71F5} - {235A45A7-331C-25BE-DB53-C2C328987E22} = {BB76B5A5-14BA-E317-828D-110B711D71F5} - {63E6AEF3-80BE-0FCE-E677-D01F65E1424F} = {BB76B5A5-14BA-E317-828D-110B711D71F5} - {D17D4B09-569F-CD45-70A8-2E4DDDE53E69} = {BB76B5A5-14BA-E317-828D-110B711D71F5} - {C1767CE0-3470-0BE7-DF63-57D1C7455A8A} = {BB76B5A5-14BA-E317-828D-110B711D71F5} - {4E63ED70-BA5F-324B-9E64-B04E64B5C0C7} = {BB76B5A5-14BA-E317-828D-110B711D71F5} - {F381910D-309A-2065-DD77-7494C2FD23BF} = {BB76B5A5-14BA-E317-828D-110B711D71F5} - {51A857B4-DBDA-1B5A-0F80-83580CA223B4} = {BB76B5A5-14BA-E317-828D-110B711D71F5} - {63B739C4-3FFB-AD71-3B39-8EA5128D8127} = {BB76B5A5-14BA-E317-828D-110B711D71F5} - {4DCC3C48-6209-AF26-3864-EAB021E65AD5} = {BB76B5A5-14BA-E317-828D-110B711D71F5} - {7C0B72FC-8CBC-D831-9614-5C4CAE8197D3} = {BB76B5A5-14BA-E317-828D-110B711D71F5} - {1AD4003A-C2C9-E202-16B4-973BE4ECFEB0} = {BB76B5A5-14BA-E317-828D-110B711D71F5} - {A684BCCD-046B-5314-B492-13A65C7AAC2A} = {BB76B5A5-14BA-E317-828D-110B711D71F5} - {7A54278E-939F-447A-17F4-15DDB3F6B403} = {BB76B5A5-14BA-E317-828D-110B711D71F5} - {AF719B05-6089-CA3F-96A7-8F59902FF781} = {BB76B5A5-14BA-E317-828D-110B711D71F5} - {D6EFF950-E1CE-F003-26E6-CCD4CE93C144} = {BB76B5A5-14BA-E317-828D-110B711D71F5} - {44F1D673-6AB1-2635-567A-4512FEA53D32} = {BB76B5A5-14BA-E317-828D-110B711D71F5} - {4B435085-030A-E80A-F018-FAEE801E2FB4} = {BB76B5A5-14BA-E317-828D-110B711D71F5} - {5E698EC5-3B12-9266-8F90-ED9B6C841C6A} = {BB76B5A5-14BA-E317-828D-110B711D71F5} - {D767CC98-7156-89E4-C89E-0B4624D83584} = {BB76B5A5-14BA-E317-828D-110B711D71F5} - {DE5FDAC5-0355-823F-552F-5A37661C9FE5} = {BB76B5A5-14BA-E317-828D-110B711D71F5} - {0BA16F05-266F-4BCC-04A6-25E7D566E769} = {BB76B5A5-14BA-E317-828D-110B711D71F5} - {540911AA-2FA7-DF51-2203-B6109B580346} = {BB76B5A5-14BA-E317-828D-110B711D71F5} - {B4801F0F-50AB-5613-AB78-5225D80E0421} = {BB76B5A5-14BA-E317-828D-110B711D71F5} - {42729022-D163-60E6-B0BA-6FA7E84DA52F} = {BB76B5A5-14BA-E317-828D-110B711D71F5} - {6A2FEC90-D817-C92B-89B8-8B2E2DD18FF1} = {BB76B5A5-14BA-E317-828D-110B711D71F5} - {EA3E79FB-BEC9-7590-EC7C-64D2CF8E8782} = {BB76B5A5-14BA-E317-828D-110B711D71F5} - {D835F664-32BC-4CA8-2ABB-97D30953F05C} = {BB76B5A5-14BA-E317-828D-110B711D71F5} - {BC4C56DA-A389-5A58-7CB0-D7AA2AE44430} = {BB76B5A5-14BA-E317-828D-110B711D71F5} - {AF8D5B15-8ADF-FA1F-342A-73D095FD5B21} = {BB76B5A5-14BA-E317-828D-110B711D71F5} - {A27B1BB8-9415-4C14-CA41-828B66357E0B} = {BB76B5A5-14BA-E317-828D-110B711D71F5} - {69D6403C-9C1F-04C3-BADF-8CFFBD3848E7} = {BB76B5A5-14BA-E317-828D-110B711D71F5} - {C26F680C-684A-ECC6-BB6C-EBD19DC43B4C} = {BB76B5A5-14BA-E317-828D-110B711D71F5} - {22B129C7-C609-3B90-AD56-64C746A1505E} = {EA6E5683-3A20-2E52-1CE6-AE0D6D36AC4D} - {AD31623A-BC43-52C2-D906-AC1D8784A541} = {3823DE1E-2ACE-C956-99E1-00DB786D9E1D} - {776E2142-804F-03B9-C804-D061D64C6092} = {EC506DBE-AB6D-492E-786E-8B176021BF2E} - {5B4DF41E-C8CC-2606-FA2D-967118BD3C59} = {5F27FB4E-CF09-3A6B-F5B4-BF5A709FA609} - {3D8C5A6C-462D-7487-5BD0-A3EF6B657EB6} = {018E0E11-1CCE-A2BE-641D-21EE14D2E90D} - {2609BC1A-6765-29BE-78CC-C0F1D2814F10} = {3F605548-87E2-8A1D-306D-0CE6960B8242} - {C6822231-A4F4-9E69-6CE2-4FDB3E81C728} = {45F7FA87-7451-6970-7F6E-F8BAE45E081B} - {55D9B653-FB76-FCE8-1A3C-67B1BEDEC214} = {F2E6CB0E-DF77-1FAA-582B-62B040DF3848} - {DE5BF139-1E5C-D6EA-4FAA-661EF353A194} = {C494ECBE-DEA5-3576-D2AF-200FF12BC144} - {335E62C0-9E69-A952-680B-753B1B17C6D0} = {9C2DD234-FA33-FDB6-86F0-EF9B75A13450} - {ECA25786-A3A8-92C4-4AA3-D4A73C69FDCA} = {7E890DF9-B715-B6DF-2498-FD74DDA87D71} - {5A6CD890-8142-F920-3734-D67CA3E65F61} = {9BD75659-58CB-06D1-E198-C39007E82C6A} - {A260E14F-DBA4-862E-53CD-18D3B92ADA3D} = {7BF13935-F1DD-D23B-8347-DB1550C69D69} - {97F94029-5419-6187-5A63-5C8FD9232FAE} = {64689413-46D7-8499-68A6-B6367ACBC597} - {03DF5914-2390-A82D-7464-642D0B95E068} = {24D60BC6-6A93-C97D-1238-113DDB928700} - {CF633BDA-9F2E-D0C8-702F-BC9D27363B4B} = {CCF230F8-F75D-A766-7EAE-0C9FEF5AF6C2} - {73DE9C04-CEFE-53BA-A527-3A36D478DEFE} = {066DF6C9-826C-F223-47D2-BDF53D59F6C3} - {B7DC1B0A-EBD8-B1E8-28C8-9D5F19E118AD} = {61C7FDA3-83AA-3EE6-6321-1C1ACD1073DF} - {0B56708E-B56C-E058-DE31-FCDFF30031F7} = {BF6C9274-4DBD-2FDE-B94C-1B208F6C53BC} - {78FAD457-CE1B-D78E-A602-510EAD85E0AF} = {4EAAC62E-EBD2-DFF1-7B37-7E131C75DEC3} - {AF9E7F02-25AD-3540-18D7-F6A4F8BA5A60} = {79E122F4-2325-3E92-438E-5825A307B594} - {AB6AE2B6-8D6B-2D9F-2A88-7C596C59F4FC} = {39EFDA5B-F5EE-8212-D5BA-90E1B82013E7} - {375F5AD0-F7EE-1782-7B34-E181CDB61B9F} = {3B82DBF3-3DAE-EA97-85F4-6DCFA09940DF} - {BA45605A-1CCE-6B0C-489D-C113915B243F} = {6844B539-C2A3-9D4F-139D-9D533BCABADA} - {9D31FC8A-2A69-B78A-D3E5-4F867B16D971} = {4263AA71-0335-3F44-9A9B-423C3A3D05E6} - {92268008-FBB0-C7AD-ECC2-7B75BED9F5E1} = {F1B1DB47-D2D7-59CB-679B-23E4928E8328} - {8DCCAF70-D364-4C8B-4E90-AF65091DE0C5} = {BC35DE94-4F04-3436-27A3-F11647FEDD5C} - {7828C164-DD01-2809-CCB3-364486834F60} = {864C8B80-771A-0C15-30A5-558F99006E0D} - {DE95E7B2-0937-A980-441F-829E023BC43E} = {603E7A23-1D6B-D3A9-B0E6-3E332B13ED5C} - {91D69463-23E2-E2C7-AA7E-A78B13CED620} = {D2F7E58B-47D4-5205-D917-144CA1CFF4F1} - {34EFF636-81A7-8DF6-7CC9-4DA784BAC7F3} = {1DCF4EBB-DBC4-752C-13D4-D1EECE4E8907} - {5DCF16A8-97C6-2CB4-6A63-0370239039EB} = {1B37A859-E733-60CB-4806-1A24B6F10E05} - {EB093C48-CDAC-106B-1196-AE34809B34C0} = {F2B58F4E-6F28-A25F-5BFB-CDEBAD6B9A3E} - {92C62F7B-8028-6EE1-B71B-F45F459B8E97} = {538E2D98-5325-3F54-BE74-EFE5FC1ECBD8} - {F664A948-E352-5808-E780-77A03F19E93E} = {66557252-B5C4-664B-D807-07018C627474} - {FA83F778-5252-0B80-5555-E69F790322EA} = {7203223D-FF02-7BEB-2798-D1639ACC01C4} - {F3A27846-6DE0-3448-222C-25A273E86B2E} = {5AC9EE40-1881-5F8A-46A2-2C303950D3C8} - {166F4DEC-9886-92D5-6496-085664E9F08F} = {927E3CD3-4C20-4DE5-A395-D0977152A8D3} - {C53E0895-879A-D9E6-0A43-24AD17A2F270} = {3C69853C-90E3-D889-1960-3B9229882590} - {246FCC7C-1437-742D-BAE5-E77A24164F08} = {9FB0DDD7-7A77-8DA4-F9E2-A94E60ED8FC7} - {0AED303F-69E6-238F-EF80-81985080EDB7} = {643E4D4C-BC96-A37F-E0EC-488127F0B127} - {2904D288-CE64-A565-2C46-C2E85A96A1EE} = {6F2CA7F5-3E7C-C61B-94E6-E7DD1227B5B1} - {A6667CC3-B77F-023E-3A67-05F99E9FF46A} = {F04B7DBB-77A5-C978-B2DE-8C189A32AA72} - {A26E2816-F787-F76B-1D6C-E086DD3E19CE} = {7C72F22A-20FF-DF5B-9191-6DFD0D497DB2} - {B3DEC619-67AC-1B5A-4F3E-A1F24C3F6877} = {C896CC0A-F5E6-9AA4-C582-E691441F8D32} - {90DB65B4-8F6E-FB8E-0281-505AD8BC6BA6} = {0AA3A418-AB45-CCA4-46D4-EEBFE011FECA} - {059FBB86-DEE6-8207-3F23-2A1A3EC00DEA} = {225D9926-4AE8-E539-70AD-8698E688F271} - {8BBA3159-C4CC-F685-A28C-7FE6CBD3D2A1} = {D6E8E69C-F721-BBCB-8C39-9716D53D72AD} - {EA0974E3-CD2B-5792-EF1E-9B5B7CCBDF00} = {9529EE99-D6A5-B570-EB1F-15BD2D57DFE2} - {632A1F0D-1BA5-C84B-B716-2BE638A92780} = {589A43FD-8213-E9E3-6CFF-9CBA72D53E98} - {9DE7852B-7E2D-257E-B0F1-45D2687854ED} = {2BACF7E3-1278-FE99-8343-8221E6FBA9DE} - {DC2AFC89-C3C8-4E9B-13A7-027EB6386EFA} = {75E47125-E4D7-8482-F1A4-726564970864} - {9151601C-8784-01A6-C2E7-A5C0FAAB0AEF} = {6DCAF6F3-717F-27A9-D96C-F2BFA5550347} - {CB296A20-2732-77C1-7F23-27D5BAEDD0C7} = {054761F9-16D3-B2F8-6F4D-EFC2248805CD} - {0DBEC9BA-FE1D-3898-B2C6-E4357DC23E0F} = {B54CE64C-4167-1DD1-B7D6-2FD7A5AEF715} - {A63897D9-9531-989B-7309-E384BCFC2BB9} = {FCD529E0-DD17-6587-B29C-12D425C0AD0C} - {8C594D82-3463-3367-4F06-900AC707753D} = {61B23570-4F2D-B060-BE1F-37995682E494} - {52F400CD-D473-7A1F-7986-89011CD2A887} = {CEDC2447-F717-3C95-7E08-F214D575A7B7} - {9588FBF9-C37E-D16E-2E8F-CFA226EAC01D} = {1182764D-2143-EEF0-9270-3DCE392F5D06} - {97998C88-E6E1-D5E2-B632-537B58E00CBF} = {F4F1CBE2-1CDD-CAA4-41F0-266DB4677C05} - {BAD08D96-A80A-D27F-5D9C-656AEEB3D568} = {3DE1DCDC-C845-4AC7-7B66-34B0A9E8626B} - {F63694F1-B56D-6E72-3F5D-5D38B1541F0F} = {6FA01E92-606B-0CB8-8583-6F693A903CFC} - {20D1569C-2A47-38B8-075E-47225B674394} = {B0F64757-F7A7-1A11-8DEC-BAC72EB5EC29} - {38A9EE9B-6FC8-93BC-0D43-2A906E678D66} = {772B02B5-6280-E1D4-3E2E-248D0455C2FB} - {19868E2D-7163-2108-1094-F13887C4F070} = {831265B0-8896-9C95-3488-E12FD9F6DC53} - {CC319FC5-F4B1-C3DD-7310-4DAD343E0125} = {BC12ED55-6015-7C8B-8384-B39CE93C76D6} - {84F711C2-C210-28D2-F0D9-B13733FEE23D} = {48F90289-938C-CCA7-B60F-D2143E7C9A69} - {CBB14B90-27F9-8DD6-DFC4-3507DBD1FBC6} = {E69FA1A0-6D1B-A6E4-2DC0-8F4C5F21BF04} - {A78EBC0F-C62C-8F56-95C0-330E376242A2} = {9D6AB85A-85EA-D85A-5566-A121D34016E6} - {6D26FB21-7E48-024B-E5D4-E3F0F31976BB} = {083067CF-CE89-EF39-9BD3-4741919E26F3} - {79104479-B087-E5D0-5523-F1803282A246} = {A5994E92-7E0E-89FE-5628-DE1A0176B8BA} - {F17A6F0B-3120-2BA9-84D8-5F8BA0B9705D} = {54C11B29-4C54-7255-AB44-BEB63AF9BD1F} - {FBD908D6-AF93-CC62-C09D-F0BB3E0CEA7F} = {2F4EB1B2-4185-C535-85ED-53EA2D2C0573} - {37F9B25E-81CF-95C5-0311-EA6DA191E415} = {3B2531C6-790E-BD6E-AE00-3F5F95EFE5F9} - {28D91816-206C-576E-1A83-FD98E08C2E3C} = {79987C33-9903-4F75-1045-284F537155FE} - {5EFEC79C-A9F1-96A4-692C-733566107170} = {730E1137-BA40-3899-17CB-93922F4DA4CA} - {F4E7E32B-D78B-5A08-F7B5-E8D4C7ED20D3} = {75E6454F-DC5F-C6E5-CB1D-47AA2AB72FDF} - {3A1CFB24-6EAA-9A87-7783-BFC56B0E5394} = {EEF284CB-436C-439C-9606-E4DD1FD5F6D3} - {B1969736-DE03-ADEB-2659-55B2B82B38A8} = {1B2C9807-0067-AAD6-69CD-7FD799689BDD} - {D166FCF0-F220-A013-133A-620521740411} = {E49112A1-06DA-43F4-4A8D-5E805A6466FC} - {F638D731-2DB2-2278-D9F8-019418A264F2} = {891DD979-9F3C-981D-FB41-EB66B7C61938} - {CAEB1FEB-B3F1-4B9D-7FEC-1983BCA60D81} = {446F1BC4-7609-555E-1FA7-901560438951} - {B07074FE-3D4E-5957-5F81-B75B5D25BD1B} = {E5B2A8AA-8162-7E98-9D31-3DA40BB8EDC2} - {91B8E22B-C90B-AEBD-707E-57BBD549BA32} = {327615A7-CCA8-CAAA-A279-54A2EB3C8D8C} - {B7B5D764-C3A0-1743-0739-29966F993626} = {29E594C5-9DE1-D06F-4F19-4166C3935CAE} - {E9039B92-DAFC-F20B-22D6-78F8E4EB7CF1} = {1006432F-63A0-8569-EEC7-4393A1918480} - {C4EDBBAF-875C-4839-05A8-F6F12A5ED52D} = {BE6FF48D-FDD8-A0C3-69E0-CAE30D3FC13F} - {04444789-CEE4-3F3A-6EFA-18416E620B2A} = {4B6B949F-4BF5-2817-4818-71E26BF57749} - {AD1B6448-2DC9-2F9A-D143-F09BBDF6F01F} = {FBBE5FEA-8CD5-8521-DC93-06C239F7876D} - {0EAC8F64-9588-1EF0-C33A-67590CF27590} = {8FF79C15-5E56-BB59-A473-0E42F8395C89} - {761CAD6D-98CB-1936-9065-BF1A756671FF} = {4B16E448-1B2D-28B7-2417-D2D191FE524F} - {7974C4F0-BC89-2775-8943-2DF909F3B08B} = {1F48E1F9-072B-1F1D-7E58-9C065D0F4ED1} - {B1B31937-CCC8-D97A-F66D-1849734B780B} = {1003D3C6-91C9-D165-8325-20F3A16D9A5B} - {9A566B3A-E281-09AF-EC1B-BD4FDB3248CE} = {5CC00E13-DAA8-7285-CDB0-D482D750FFBD} - {A345E5AC-BDDB-A817-3C92-08C8865D1EF9} = {278C2043-B1CE-CC57-7F08-9C4B69C7A65D} - {905DD8ED-3D10-7C2B-B199-B98E85267BB8} = {D2BC4D25-D54D-1778-798D-6547A38EEB05} - {C2D3B3C7-E556-9B72-024A-FF5EDEAF4CB5} = {F783A1A9-4991-592A-57C3-E18C41986545} - {31AC6B88-D6C8-E2EF-39AF-1B21AFD76C89} = {9EAD58C8-4DCD-4933-B9F9-7D211C66D73F} - {90B84537-F992-234C-C998-91C6AD65AB12} = {7AC29AE8-7BDA-4243-5869-D89EF3921DC3} - {F22333B6-7E27-679B-8475-B4B9AB1CB186} = {9646F3EA-8DFC-F221-D415-D489C166889B} - {CE042F3A-6851-FAAB-9E9C-AD905B4AAC8D} = {83CDC626-3D1B-02BE-1DE9-82175D01430B} - {D6B56A54-4057-9F76-BC7E-56E896E5D276} = {6E200246-2354-ECCC-0D81-F58C27F7551C} - {9258E4F2-762C-C780-F118-2CABD0281CC9} = {079CEB57-D0A3-20A1-131E-37D0CA4C0410} - {D6752A7E-0FD2-6626-80E3-CE4D4816D2B0} = {49ECEC98-A019-AEB6-1632-D05B0DCB6EEA} - {AF85AC87-521A-2F0E-5F10-836E416EC716} = {7C05D5D7-330C-204C-430C-8B2B27AB9995} - {FB946C57-55B3-08C6-18AE-1672D46C5308} = {AA9E4361-01CE-AC2F-E20D-0CA5C1D438CC} - {99A47EAA-44B8-8E06-DA0E-05B225009FDF} = {2F13F88C-7820-12E7-1FEF-4DB0DDE34DAB} - {4F0EF830-4308-347B-A31D-270A9812D15E} = {3C0D80C5-B033-CE52-8868-614826F45D72} - {B7EE2F70-A634-8B4D-93F4-EA1EEFD9E5E8} = {ED480238-AEBE-3C29-8D80-5C0AB45E13D3} - {A5298720-984E-6574-D41B-CFE7CA408182} = {C28FED05-DEDA-E1BA-F538-4BCAA8DAF2FE} - {CB033CB6-F90B-E201-BA86-C867544E7247} = {633ED6DB-FFB9-F767-070D-09C342DB4B74} - {E9F5BFF2-0D0E-7B41-9AF0-83384F4B8825} = {53094E1B-EF1A-C3F8-6EB5-45A43B0B8A9C} - {668466AC-CD66-BAA0-0322-148549E373CB} = {235A45A7-331C-25BE-DB53-C2C328987E22} - {07EBBFA6-798E-76A3-CAF0-67828B00B58E} = {9AA450C7-BBED-3722-DB4B-4DA97D885E00} - {181ED0FE-FE20-069F-7CCF-86FF5449D7F5} = {63E6AEF3-80BE-0FCE-E677-D01F65E1424F} - {5E683B7C-B584-0E56-C8D6-D29050DE70FB} = {31219219-DCBE-766A-A54F-4975DA7BBB80} - {4163E755-1563-6A72-60E7-BB2B69F5ABA2} = {D17D4B09-569F-CD45-70A8-2E4DDDE53E69} - {AE6F3DA7-2993-6926-323E-A29295D55C36} = {0CBB3D0E-CC76-CDE9-4A2D-CE5F73B59420} - {D013641A-8457-6215-05A1-74BB57B58409} = {C1767CE0-3470-0BE7-DF63-57D1C7455A8A} - {4FC29140-9C5F-8DD5-B405-6982BD1DA5A3} = {66B51CD4-BCE3-757B-BEFC-0F2BF96C7D52} - {B9C9A1E4-3BB8-C8BE-7819-660A582D2952} = {3C13055F-5B62-4AF8-F8BB-83CFDDEEDDCF} - {2BBAB3B4-2E18-F945-F7AB-6207D7F72714} = {4E63ED70-BA5F-324B-9E64-B04E64B5C0C7} - {BA492274-A505-BCD5-3DA5-EE0C94DD5748} = {4DAC6B77-D825-3085-4263-3226D14F61AC} - {029F8300-57F5-9CCD-505E-708937686679} = {F381910D-309A-2065-DD77-7494C2FD23BF} - {A5D2DB78-8045-29AC-E4B1-66E72F2C7FF0} = {12AF40C6-8379-4786-4DF4-B8CCCC54E2A8} - {294792C0-DC28-3C5D-2D59-33DC99CD6C61} = {51A857B4-DBDA-1B5A-0F80-83580CA223B4} - {58D8630F-C0F4-B772-8572-BCC98FF0F0D8} = {160F000D-9DA4-3AAA-8370-15C6BF460E1B} - {2B1B4954-1241-8F2E-75B6-2146D15D037B} = {63B739C4-3FFB-AD71-3B39-8EA5128D8127} - {97A9C869-F385-6711-6B76-F3859C86DCAC} = {5F222E3F-1E9A-331A-F5BC-7C22CC4DC27B} - {201CE292-0186-2A38-55D7-69890B5817DF} = {4DCC3C48-6209-AF26-3864-EAB021E65AD5} - {17A00031-9FF7-4F73-5319-23FA5817625F} = {7E234D3A-B714-8683-EF17-12E573F8796B} - {11E0E129-091F-BEEB-A1B2-9BAEF5BE9FBC} = {7C0B72FC-8CBC-D831-9614-5C4CAE8197D3} - {AEF63403-4889-5396-CDEA-3B713CEF2ED7} = {1AD4003A-C2C9-E202-16B4-973BE4ECFEB0} - {D24E7862-3930-A4F6-1DFA-DA88C759546C} = {A92B2733-D971-E795-C42E-763B8D28BE6A} - {6DC62619-949E-92E6-F4F1-5A0320959929} = {A684BCCD-046B-5314-B492-13A65C7AAC2A} - {37F1D83D-073C-C165-4C53-664AD87628E6} = {56F1436F-34A3-0E62-F76D-7D33B0F6CF9A} - {CDC236E8-6881-46C4-EE95-3C386AF009D0} = {7A54278E-939F-447A-17F4-15DDB3F6B403} - {ACC2785F-F4B9-13E4-EED2-C5D067242175} = {A5F6CD9E-7F26-36C4-0785-86FECE0484FE} - {7F4A3CA3-C3DA-A698-5CBC-54F28D1C7DFB} = {AF719B05-6089-CA3F-96A7-8F59902FF781} - {DAA1F516-DEC3-7FF2-3A63-78DD97BA062C} = {D6EFF950-E1CE-F003-26E6-CCD4CE93C144} - {11EF0DE9-2648-F711-6194-70B5C40B3F3F} = {340ACF7A-5050-5BEF-8E3E-56EFD1EA2CFF} - {01A21B47-07C5-6039-1B48-C5EACA3DBA2D} = {6C13C210-5443-5941-7273-A830E6A0F2FD} - {7CB7FEA8-8A12-A5D6-0057-AA65DB328617} = {7CC450E2-1730-BE2C-9AE2-AF8567982B8E} - {0484DB46-3E40-1A10-131C-524AF1233EA7} = {44F1D673-6AB1-2635-567A-4512FEA53D32} - {64E1D9B1-B944-8AA3-799F-02E7DD33FB78} = {680B3990-2960-BCCC-7C3B-2C4BAB9B8134} - {D37991E1-585F-FF1B-9772-07477E40AF78} = {4B435085-030A-E80A-F018-FAEE801E2FB4} - {35A06F00-71AB-8A31-7D60-EBF41EA730CA} = {20D1D291-07F1-1389-74B4-F82B49847CED} - {56120A54-1D4D-F07B-63B4-B15525C2ADD9} = {5E698EC5-3B12-9266-8F90-ED9B6C841C6A} - {BE47FB74-D163-0B1F-5293-0962EA7E8585} = {D767CC98-7156-89E4-C89E-0B4624D83584} - {9AD932E9-0986-654C-B454-34E654C80697} = {7A7A0652-CB76-9B73-1FD1-D67F5510DB1F} - {00BE2B68-FC96-23F8-F61D-EE53B7AE06A1} = {DE5FDAC5-0355-823F-552F-5A37661C9FE5} - {570BA050-81A7-46EB-3DDD-422027EE2CA2} = {513870EC-4723-146F-A8F5-D6A7981C32B5} - {6C43FD78-3478-F245-3EE4-E410D1E7D7C5} = {0BA16F05-266F-4BCC-04A6-25E7D566E769} - {7F0FFA06-EAC8-CC9A-3386-389638F12B59} = {4BE45036-7A8F-9072-5160-3BB53505A68F} - {03B9D4BE-348B-9AD6-6FE9-6C40AE22053D} = {540911AA-2FA7-DF51-2203-B6109B580346} - {35CF4CF2-8A84-378D-32F0-572F4AA900A3} = {C484B2AA-2582-1395-840E-61DE9FED5313} - {13E03C69-0634-3330-26D9-DCF7DD136BC5} = {0E5FA5F8-5C99-7868-B1D0-1D38A9624795} - {A80D212B-7E80-4251-16C0-60FA3670A5B4} = {A81B4B21-F490-7DE9-024B-4959F9172735} - {2F4ACEB8-76C7-D5A2-6DD1-2EF713D9A197} = {B4801F0F-50AB-5613-AB78-5225D80E0421} - {C146A9AF-6C13-B9DC-F555-37182A54430F} = {42729022-D163-60E6-B0BA-6FA7E84DA52F} - {E5025FCF-78CB-4B5B-3377-AE008B1BE9D2} = {68C21EB9-A635-48B1-98E8-A0DD36C545F5} - {52698305-D6F8-C13C-0882-48FC37726404} = {7A608DF5-E8F5-38DA-4E15-2A7A2B6C508F} - {DE10AF97-E790-9D19-2399-70940A9B83A7} = {6A2FEC90-D817-C92B-89B8-8B2E2DD18FF1} - {5567139C-0365-B6A0-5DD0-978A09B9F176} = {0D24FEF1-02AE-8265-6524-64DE307CB4CE} - {A56C1F0E-3E18-DBEE-7F97-B5FCBF23D1D6} = {EA3E79FB-BEC9-7590-EC7C-64D2CF8E8782} - {256D269B-35EA-F833-2F1D-8E0058908DEE} = {7DB7DF6E-FCAA-E569-29D8-C0C4FBFC5625} - {F02B63CD-2C69-61F7-7F96-930122D4D4D7} = {D835F664-32BC-4CA8-2ABB-97D30953F05C} - {F061C879-063E-99DE-B301-E261DB12156F} = {BC4C56DA-A389-5A58-7CB0-D7AA2AE44430} - {6E9C9582-67FA-2EB1-C6BA-AD4CD326E276} = {D655B564-92E5-1E96-7A72-112F368418C4} - {FCF711C2-1090-7204-5E38-4BEFBE265A61} = {AF8D5B15-8ADF-FA1F-342A-73D095FD5B21} - {3A4AAE04-FA0C-2AF8-6548-AC22E5A41312} = {BAF3E8BB-263A-8E31-3576-D18BE2B46A22} - {66F8F288-C387-40E0-5F83-938671335703} = {A27B1BB8-9415-4C14-CA41-828B66357E0B} - {7B3BDB83-918F-6760-3853-BDD70CD71B42} = {DB888E44-BB55-C4A2-CDFF-7B04F0E2D6E5} - {2669C700-5CFF-0186-F65E-8D26BE06E934} = {56026A9E-B2DA-48EC-60C3-3193857A238C} - {0560BD84-CDBC-A79A-C665-55F6D62825EA} = {BE1D16DA-78A9-22D8-F49D-94719ECB5132} - {783A67C9-3381-6E4C-3752-423F0FC6F6F9} = {69D6403C-9C1F-04C3-BADF-8CFFBD3848E7} - {F890BD12-6CF5-4F80-9099-B7FE9A908432} = {C35AAD13-5F80-85D6-1702-E3E0C55D6A99} - {505C6840-5113-26EC-CEDB-D07EEABEF94B} = {C26F680C-684A-ECC6-BB6C-EBD19DC43B4C} - {A79CBC0C-5313-4ECF-A24E-27CE236BCF2C} = {076B8074-5735-5367-1EEA-CA16A5B8ABD7} - {0AF13355-173C-3128-5AFC-D32E540DA3EF} = {79B10804-91E9-972E-1913-EE0F0B11663E} - {AF043113-CCE3-59C1-DF71-9804155F26A8} = {8380A20C-A5B8-EE91-1A58-270323688CB9} - {15602821-2ABA-14BB-738D-1A53E1976E07} = {3C8A19A6-5578-51E9-E592-6DABBE5739E5} - {1D761F8B-921C-53BF-DCF5-5ABD329EEB0C} = {A7542386-71EB-4F34-E1CE-27D399325955} - {DA7634C2-9156-9B79-7A1D-90D8E605DC8A} = {0910C958-24C8-947F-359A-218ED1199AAE} - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {C9C08EA6-E174-0E6C-3FFC-FC856E9A6EC2} - EndGlobalSection -EndGlobal + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.Native", "StellaOps.Scanner.Analyzers.Native", "{83CDC626-3D1B-02BE-1DE9-82175D01430B}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Sbomer.BuildXPlugin", "StellaOps.Scanner.Sbomer.BuildXPlugin", "{513870EC-4723-146F-A8F5-D6A7981C32B5}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.WebService", "StellaOps.Scanner.WebService", "{BE1D16DA-78A9-22D8-F49D-94719ECB5132}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Worker", "StellaOps.Scanner.Worker", "{C35AAD13-5F80-85D6-1702-E3E0C55D6A99}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "__Benchmarks", "__Benchmarks", "{EB157E4F-3EA5-5CBF-9694-18E3FA265E53}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.Lang.Deno.Benchmarks", "StellaOps.Scanner.Analyzers.Lang.Deno.Benchmarks", "{1B2C9807-0067-AAD6-69CD-7FD799689BDD}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.Lang.Php.Benchmarks", "StellaOps.Scanner.Analyzers.Lang.Php.Benchmarks", "{4B16E448-1B2D-28B7-2417-D2D191FE524F}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.Lang.Rust.Benchmarks", "StellaOps.Scanner.Analyzers.Lang.Rust.Benchmarks", "{9EAD58C8-4DCD-4933-B9F9-7D211C66D73F}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Storage.Epss.Perf", "StellaOps.Scanner.Storage.Epss.Perf", "{0E5FA5F8-5C99-7868-B1D0-1D38A9624795}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "__External", "__External", "{5B52EF8A-3661-DCFF-797D-BC4D6AC60BDA}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AirGap", "AirGap", "{F310596E-88BB-9E54-885E-21C61971917E}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.AirGap.Importer", "StellaOps.AirGap.Importer", "{EA6E5683-3A20-2E52-1CE6-AE0D6D36AC4D}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.AirGap.Policy", "StellaOps.AirGap.Policy", "{D9492ED1-A812-924B-65E4-F518592B49BB}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.AirGap.Policy", "StellaOps.AirGap.Policy", "{3823DE1E-2ACE-C956-99E1-00DB786D9E1D}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Aoc", "Aoc", "{03DFF14F-7321-1784-D4C7-4E99D4120F48}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "__Libraries", "__Libraries", "{BDD326D6-7616-84F0-B914-74743BFBA520}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Aoc", "StellaOps.Aoc", "{EC506DBE-AB6D-492E-786E-8B176021BF2E}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Attestor", "Attestor", "{5AC09D9A-F2A5-9CFA-B3C5-8D25F257651C}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Attestor", "StellaOps.Attestor", "{33B1AE27-692A-1778-48C1-CCEC2B9BC78F}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Attestor.Envelope", "StellaOps.Attestor.Envelope", "{018E0E11-1CCE-A2BE-641D-21EE14D2E90D}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Attestor.Core", "StellaOps.Attestor.Core", "{5F27FB4E-CF09-3A6B-F5B4-BF5A709FA609}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "__Libraries", "__Libraries", "{AB67BDB9-D701-3AC9-9CDF-ECCDCCD8DB6D}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Attestor.GraphRoot", "StellaOps.Attestor.GraphRoot", "{3F605548-87E2-8A1D-306D-0CE6960B8242}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Attestor.ProofChain", "StellaOps.Attestor.ProofChain", "{45F7FA87-7451-6970-7F6E-F8BAE45E081B}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Authority", "Authority", "{C1DCEFBD-12A5-EAAE-632E-8EEB9BE491B6}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Authority", "StellaOps.Authority", "{A6928CBA-4D4D-AB2B-CA19-FEE6E73ECE70}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Auth.Abstractions", "StellaOps.Auth.Abstractions", "{F2E6CB0E-DF77-1FAA-582B-62B040DF3848}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Auth.Client", "StellaOps.Auth.Client", "{C494ECBE-DEA5-3576-D2AF-200FF12BC144}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Auth.ServerIntegration", "StellaOps.Auth.ServerIntegration", "{7E890DF9-B715-B6DF-2498-FD74DDA87D71}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Authority.Plugins.Abstractions", "StellaOps.Authority.Plugins.Abstractions", "{64689413-46D7-8499-68A6-B6367ACBC597}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "__Libraries", "__Libraries", "{5827F4DE-0AA7-FC85-641D-09E3D890DB27}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Authority.Core", "StellaOps.Authority.Core", "{9BD75659-58CB-06D1-E198-C39007E82C6A}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Authority.Persistence", "StellaOps.Authority.Persistence", "{7BF13935-F1DD-D23B-8347-DB1550C69D69}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "BinaryIndex", "BinaryIndex", "{2949F2E7-0E10-76D0-5672-8B1662588F74}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "__Libraries", "__Libraries", "{3ADE81EE-6F71-CB5C-016B-36E8AC854713}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.BinaryIndex.Contracts", "StellaOps.BinaryIndex.Contracts", "{24D60BC6-6A93-C97D-1238-113DDB928700}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.BinaryIndex.Core", "StellaOps.BinaryIndex.Core", "{CCF230F8-F75D-A766-7EAE-0C9FEF5AF6C2}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.BinaryIndex.Corpus", "StellaOps.BinaryIndex.Corpus", "{066DF6C9-826C-F223-47D2-BDF53D59F6C3}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.BinaryIndex.Fingerprints", "StellaOps.BinaryIndex.Fingerprints", "{61C7FDA3-83AA-3EE6-6321-1C1ACD1073DF}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.BinaryIndex.FixIndex", "StellaOps.BinaryIndex.FixIndex", "{BF6C9274-4DBD-2FDE-B94C-1B208F6C53BC}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.BinaryIndex.Persistence", "StellaOps.BinaryIndex.Persistence", "{4EAAC62E-EBD2-DFF1-7B37-7E131C75DEC3}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Concelier", "Concelier", "{157C3671-CA0B-69FA-A7C9-74A1FDA97B99}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "__Libraries", "__Libraries", "{F39E09D6-BF93-B64A-CFE7-2BA92815C0FE}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Concelier.Cache.Valkey", "StellaOps.Concelier.Cache.Valkey", "{39EFDA5B-F5EE-8212-D5BA-90E1B82013E7}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Concelier.Connector.Common", "StellaOps.Concelier.Connector.Common", "{3B82DBF3-3DAE-EA97-85F4-6DCFA09940DF}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Concelier.Core", "StellaOps.Concelier.Core", "{6844B539-C2A3-9D4F-139D-9D533BCABADA}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Concelier.Interest", "StellaOps.Concelier.Interest", "{4263AA71-0335-3F44-9A9B-423C3A3D05E6}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Concelier.Merge", "StellaOps.Concelier.Merge", "{F1B1DB47-D2D7-59CB-679B-23E4928E8328}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Concelier.Models", "StellaOps.Concelier.Models", "{BC35DE94-4F04-3436-27A3-F11647FEDD5C}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Concelier.Normalization", "StellaOps.Concelier.Normalization", "{864C8B80-771A-0C15-30A5-558F99006E0D}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Concelier.Persistence", "StellaOps.Concelier.Persistence", "{603E7A23-1D6B-D3A9-B0E6-3E332B13ED5C}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Concelier.ProofService", "StellaOps.Concelier.ProofService", "{D2F7E58B-47D4-5205-D917-144CA1CFF4F1}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Concelier.RawModels", "StellaOps.Concelier.RawModels", "{1DCF4EBB-DBC4-752C-13D4-D1EECE4E8907}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Concelier.SbomIntegration", "StellaOps.Concelier.SbomIntegration", "{1B37A859-E733-60CB-4806-1A24B6F10E05}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Concelier.SourceIntel", "StellaOps.Concelier.SourceIntel", "{F2B58F4E-6F28-A25F-5BFB-CDEBAD6B9A3E}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Excititor", "Excititor", "{7D49FA52-6EA1-EAC8-4C5A-AC07188D6C57}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "__Libraries", "__Libraries", "{C9CF27FC-12DB-954F-863C-576BA8E309A5}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Excititor.Core", "StellaOps.Excititor.Core", "{6DCAF6F3-717F-27A9-D96C-F2BFA5550347}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Feedser", "Feedser", "{C4A90603-BE42-0044-CAB4-3EB910AD51A5}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Feedser.BinaryAnalysis", "StellaOps.Feedser.BinaryAnalysis", "{054761F9-16D3-B2F8-6F4D-EFC2248805CD}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Feedser.Core", "StellaOps.Feedser.Core", "{B54CE64C-4167-1DD1-B7D6-2FD7A5AEF715}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Notify", "Notify", "{D2162FEA-AFA4-2A88-6444-2F6D845260BB}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "__Libraries", "__Libraries", "{63EAEA3B-ADC9-631D-774E-7AA04490EDDD}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Notify.Models", "StellaOps.Notify.Models", "{B0F64757-F7A7-1A11-8DEC-BAC72EB5EC29}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Policy", "Policy", "{8E6B774C-CC4E-CE7C-AD4B-8AF7C92889A6}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Policy.RiskProfile", "StellaOps.Policy.RiskProfile", "{BC12ED55-6015-7C8B-8384-B39CE93C76D6}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "__Libraries", "__Libraries", "{FF70543D-AFF9-1D38-4950-4F8EE18D60BB}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Policy", "StellaOps.Policy", "{831265B0-8896-9C95-3488-E12FD9F6DC53}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Provenance", "Provenance", "{316BBD0A-04D2-85C9-52EA-7993CC6C8930}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Provenance.Attestation", "StellaOps.Provenance.Attestation", "{9D6AB85A-85EA-D85A-5566-A121D34016E6}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Router", "Router", "{FC018E5B-1E2F-DE19-1E97-0C845058C469}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "__Libraries", "__Libraries", "{1BE5B76C-B486-560B-6CB2-44C6537249AA}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Messaging", "StellaOps.Messaging", "{F4F1CBE2-1CDD-CAA4-41F0-266DB4677C05}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Microservice", "StellaOps.Microservice", "{3DE1DCDC-C845-4AC7-7B66-34B0A9E8626B}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Microservice.AspNetCore", "StellaOps.Microservice.AspNetCore", "{6FA01E92-606B-0CB8-8583-6F693A903CFC}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Router.AspNet", "StellaOps.Router.AspNet", "{A5994E92-7E0E-89FE-5628-DE1A0176B8BA}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Router.Common", "StellaOps.Router.Common", "{54C11B29-4C54-7255-AB44-BEB63AF9BD1F}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Signals", "Signals", "{AD65DDE7-9FEA-7380-8C10-FA165F745354}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Signals", "StellaOps.Signals", "{076B8074-5735-5367-1EEA-CA16A5B8ABD7}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Signer", "Signer", "{3247EE0D-B3E9-9C11-B0AE-FE719410390B}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Signer", "StellaOps.Signer", "{CD7C09DA-FEC8-2CC5-D00C-E525638DFF4A}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Signer.Core", "StellaOps.Signer.Core", "{79B10804-91E9-972E-1913-EE0F0B11663E}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Unknowns", "Unknowns", "{A4E2971A-7DCF-D72F-631D-98564D1D1E5D}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "__Libraries", "__Libraries", "{01B2B5BA-58CD-9920-5A1E-6B801BF36685}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Unknowns.Core", "StellaOps.Unknowns.Core", "{3C8A19A6-5578-51E9-E592-6DABBE5739E5}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Zastava", "Zastava", "{8403C362-DC2B-AF41-C9D8-E4F2D892DF8E}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "__Libraries", "__Libraries", "{B75836D4-A244-20B8-4707-64E17C725DAA}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Zastava.Core", "StellaOps.Zastava.Core", "{0910C958-24C8-947F-359A-218ED1199AAE}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "__Libraries", "__Libraries", "{1345DD29-BB3A-FB5F-4B3D-E29F6045A27A}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Auth.Security", "StellaOps.Auth.Security", "{9C2DD234-FA33-FDB6-86F0-EF9B75A13450}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Canonical.Json", "StellaOps.Canonical.Json", "{79E122F4-2325-3E92-438E-5825A307B594}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Configuration", "StellaOps.Configuration", "{538E2D98-5325-3F54-BE74-EFE5FC1ECBD8}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Cryptography", "StellaOps.Cryptography", "{66557252-B5C4-664B-D807-07018C627474}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Cryptography.DependencyInjection", "StellaOps.Cryptography.DependencyInjection", "{7203223D-FF02-7BEB-2798-D1639ACC01C4}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Cryptography.Kms", "StellaOps.Cryptography.Kms", "{5AC9EE40-1881-5F8A-46A2-2C303950D3C8}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Cryptography.Plugin.BouncyCastle", "StellaOps.Cryptography.Plugin.BouncyCastle", "{927E3CD3-4C20-4DE5-A395-D0977152A8D3}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Cryptography.Plugin.CryptoPro", "StellaOps.Cryptography.Plugin.CryptoPro", "{3C69853C-90E3-D889-1960-3B9229882590}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Cryptography.Plugin.OfflineVerification", "StellaOps.Cryptography.Plugin.OfflineVerification", "{9FB0DDD7-7A77-8DA4-F9E2-A94E60ED8FC7}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Cryptography.Plugin.OpenSslGost", "StellaOps.Cryptography.Plugin.OpenSslGost", "{643E4D4C-BC96-A37F-E0EC-488127F0B127}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Cryptography.Plugin.Pkcs11Gost", "StellaOps.Cryptography.Plugin.Pkcs11Gost", "{6F2CA7F5-3E7C-C61B-94E6-E7DD1227B5B1}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Cryptography.Plugin.PqSoft", "StellaOps.Cryptography.Plugin.PqSoft", "{F04B7DBB-77A5-C978-B2DE-8C189A32AA72}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Cryptography.Plugin.SimRemote", "StellaOps.Cryptography.Plugin.SimRemote", "{7C72F22A-20FF-DF5B-9191-6DFD0D497DB2}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Cryptography.Plugin.SmRemote", "StellaOps.Cryptography.Plugin.SmRemote", "{C896CC0A-F5E6-9AA4-C582-E691441F8D32}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Cryptography.Plugin.SmSoft", "StellaOps.Cryptography.Plugin.SmSoft", "{0AA3A418-AB45-CCA4-46D4-EEBFE011FECA}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Cryptography.Plugin.WineCsp", "StellaOps.Cryptography.Plugin.WineCsp", "{225D9926-4AE8-E539-70AD-8698E688F271}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Cryptography.PluginLoader", "StellaOps.Cryptography.PluginLoader", "{D6E8E69C-F721-BBCB-8C39-9716D53D72AD}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.DeltaVerdict", "StellaOps.DeltaVerdict", "{9529EE99-D6A5-B570-EB1F-15BD2D57DFE2}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.DependencyInjection", "StellaOps.DependencyInjection", "{589A43FD-8213-E9E3-6CFF-9CBA72D53E98}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Evidence.Bundle", "StellaOps.Evidence.Bundle", "{2BACF7E3-1278-FE99-8343-8221E6FBA9DE}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Evidence.Core", "StellaOps.Evidence.Core", "{75E47125-E4D7-8482-F1A4-726564970864}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Infrastructure.EfCore", "StellaOps.Infrastructure.EfCore", "{FCD529E0-DD17-6587-B29C-12D425C0AD0C}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Infrastructure.Postgres", "StellaOps.Infrastructure.Postgres", "{61B23570-4F2D-B060-BE1F-37995682E494}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Ingestion.Telemetry", "StellaOps.Ingestion.Telemetry", "{1182764D-2143-EEF0-9270-3DCE392F5D06}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Plugin", "StellaOps.Plugin", "{772B02B5-6280-E1D4-3E2E-248D0455C2FB}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Provcache", "StellaOps.Provcache", "{48F90289-938C-CCA7-B60F-D2143E7C9A69}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Provenance", "StellaOps.Provenance", "{E69FA1A0-6D1B-A6E4-2DC0-8F4C5F21BF04}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Replay.Core", "StellaOps.Replay.Core", "{083067CF-CE89-EF39-9BD3-4741919E26F3}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.TestKit", "StellaOps.TestKit", "{8380A20C-A5B8-EE91-1A58-270323688CB9}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.VersionComparison", "StellaOps.VersionComparison", "{A7542386-71EB-4F34-E1CE-27D399325955}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "__Tests", "__Tests", "{90659617-4DF7-809A-4E5B-29BB5A98E8E1}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "__Libraries", "__Libraries", "{AB8B269C-5A2A-A4B8-0488-B5F81E55B4D9}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Infrastructure.Postgres.Testing", "StellaOps.Infrastructure.Postgres.Testing", "{CEDC2447-F717-3C95-7E08-F214D575A7B7}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "__Libraries", "__Libraries", "{A5C98087-E847-D2C4-2143-20869479839D}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Advisory", "StellaOps.Scanner.Advisory", "{2F4EB1B2-4185-C535-85ED-53EA2D2C0573}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.Lang", "StellaOps.Scanner.Analyzers.Lang", "{79987C33-9903-4F75-1045-284F537155FE}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.Lang.Bun", "StellaOps.Scanner.Analyzers.Lang.Bun", "{730E1137-BA40-3899-17CB-93922F4DA4CA}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.Lang.Deno", "StellaOps.Scanner.Analyzers.Lang.Deno", "{EEF284CB-436C-439C-9606-E4DD1FD5F6D3}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.Lang.DotNet", "StellaOps.Scanner.Analyzers.Lang.DotNet", "{891DD979-9F3C-981D-FB41-EB66B7C61938}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.Lang.Go", "StellaOps.Scanner.Analyzers.Lang.Go", "{E5B2A8AA-8162-7E98-9D31-3DA40BB8EDC2}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.Lang.Java", "StellaOps.Scanner.Analyzers.Lang.Java", "{29E594C5-9DE1-D06F-4F19-4166C3935CAE}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.Lang.Node", "StellaOps.Scanner.Analyzers.Lang.Node", "{BE6FF48D-FDD8-A0C3-69E0-CAE30D3FC13F}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.Lang.Php", "StellaOps.Scanner.Analyzers.Lang.Php", "{8FF79C15-5E56-BB59-A473-0E42F8395C89}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.Lang.Python", "StellaOps.Scanner.Analyzers.Lang.Python", "{1003D3C6-91C9-D165-8325-20F3A16D9A5B}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.Lang.Ruby", "StellaOps.Scanner.Analyzers.Lang.Ruby", "{278C2043-B1CE-CC57-7F08-9C4B69C7A65D}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.Lang.Rust", "StellaOps.Scanner.Analyzers.Lang.Rust", "{F783A1A9-4991-592A-57C3-E18C41986545}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.Native", "StellaOps.Scanner.Analyzers.Native", "{9646F3EA-8DFC-F221-D415-D489C166889B}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.OS", "StellaOps.Scanner.Analyzers.OS", "{079CEB57-D0A3-20A1-131E-37D0CA4C0410}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.OS.Apk", "StellaOps.Scanner.Analyzers.OS.Apk", "{49ECEC98-A019-AEB6-1632-D05B0DCB6EEA}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.OS.Dpkg", "StellaOps.Scanner.Analyzers.OS.Dpkg", "{7C05D5D7-330C-204C-430C-8B2B27AB9995}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.OS.Homebrew", "StellaOps.Scanner.Analyzers.OS.Homebrew", "{AA9E4361-01CE-AC2F-E20D-0CA5C1D438CC}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.OS.MacOsBundle", "StellaOps.Scanner.Analyzers.OS.MacOsBundle", "{3C0D80C5-B033-CE52-8868-614826F45D72}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.OS.Pkgutil", "StellaOps.Scanner.Analyzers.OS.Pkgutil", "{C28FED05-DEDA-E1BA-F538-4BCAA8DAF2FE}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.OS.Rpm", "StellaOps.Scanner.Analyzers.OS.Rpm", "{53094E1B-EF1A-C3F8-6EB5-45A43B0B8A9C}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.OS.Windows.Chocolatey", "StellaOps.Scanner.Analyzers.OS.Windows.Chocolatey", "{9AA450C7-BBED-3722-DB4B-4DA97D885E00}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.OS.Windows.Msi", "StellaOps.Scanner.Analyzers.OS.Windows.Msi", "{31219219-DCBE-766A-A54F-4975DA7BBB80}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.OS.Windows.WinSxS", "StellaOps.Scanner.Analyzers.OS.Windows.WinSxS", "{0CBB3D0E-CC76-CDE9-4A2D-CE5F73B59420}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Benchmark", "StellaOps.Scanner.Benchmark", "{66B51CD4-BCE3-757B-BEFC-0F2BF96C7D52}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Benchmarks", "StellaOps.Scanner.Benchmarks", "{3C13055F-5B62-4AF8-F8BB-83CFDDEEDDCF}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Cache", "StellaOps.Scanner.Cache", "{4DAC6B77-D825-3085-4263-3226D14F61AC}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.CallGraph", "StellaOps.Scanner.CallGraph", "{12AF40C6-8379-4786-4DF4-B8CCCC54E2A8}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Core", "StellaOps.Scanner.Core", "{160F000D-9DA4-3AAA-8370-15C6BF460E1B}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Diff", "StellaOps.Scanner.Diff", "{5F222E3F-1E9A-331A-F5BC-7C22CC4DC27B}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Emit", "StellaOps.Scanner.Emit", "{7E234D3A-B714-8683-EF17-12E573F8796B}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.EntryTrace", "StellaOps.Scanner.EntryTrace", "{A92B2733-D971-E795-C42E-763B8D28BE6A}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Evidence", "StellaOps.Scanner.Evidence", "{56F1436F-34A3-0E62-F76D-7D33B0F6CF9A}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Explainability", "StellaOps.Scanner.Explainability", "{A5F6CD9E-7F26-36C4-0785-86FECE0484FE}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Orchestration", "StellaOps.Scanner.Orchestration", "{340ACF7A-5050-5BEF-8E3E-56EFD1EA2CFF}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.ProofIntegration", "StellaOps.Scanner.ProofIntegration", "{6C13C210-5443-5941-7273-A830E6A0F2FD}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.ProofSpine", "StellaOps.Scanner.ProofSpine", "{7CC450E2-1730-BE2C-9AE2-AF8567982B8E}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Queue", "StellaOps.Scanner.Queue", "{680B3990-2960-BCCC-7C3B-2C4BAB9B8134}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Reachability", "StellaOps.Scanner.Reachability", "{20D1D291-07F1-1389-74B4-F82B49847CED}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.ReachabilityDrift", "StellaOps.Scanner.ReachabilityDrift", "{7A7A0652-CB76-9B73-1FD1-D67F5510DB1F}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.SmartDiff", "StellaOps.Scanner.SmartDiff", "{4BE45036-7A8F-9072-5160-3BB53505A68F}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Storage", "StellaOps.Scanner.Storage", "{C484B2AA-2582-1395-840E-61DE9FED5313}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Storage.Oci", "StellaOps.Scanner.Storage.Oci", "{A81B4B21-F490-7DE9-024B-4959F9172735}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Surface", "StellaOps.Scanner.Surface", "{68C21EB9-A635-48B1-98E8-A0DD36C545F5}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Surface.Env", "StellaOps.Scanner.Surface.Env", "{7A608DF5-E8F5-38DA-4E15-2A7A2B6C508F}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Surface.FS", "StellaOps.Scanner.Surface.FS", "{0D24FEF1-02AE-8265-6524-64DE307CB4CE}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Surface.Secrets", "StellaOps.Scanner.Surface.Secrets", "{7DB7DF6E-FCAA-E569-29D8-C0C4FBFC5625}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Surface.Validation", "StellaOps.Scanner.Surface.Validation", "{D655B564-92E5-1E96-7A72-112F368418C4}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Triage", "StellaOps.Scanner.Triage", "{BAF3E8BB-263A-8E31-3576-D18BE2B46A22}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.VulnSurfaces", "StellaOps.Scanner.VulnSurfaces", "{DB888E44-BB55-C4A2-CDFF-7B04F0E2D6E5}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.VulnSurfaces.Tests", "StellaOps.Scanner.VulnSurfaces.Tests", "{56026A9E-B2DA-48EC-60C3-3193857A238C}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "__Tests", "__Tests", "{BB76B5A5-14BA-E317-828D-110B711D71F5}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Advisory.Tests", "StellaOps.Scanner.Advisory.Tests", "{3B2531C6-790E-BD6E-AE00-3F5F95EFE5F9}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.Lang.Bun.Tests", "StellaOps.Scanner.Analyzers.Lang.Bun.Tests", "{75E6454F-DC5F-C6E5-CB1D-47AA2AB72FDF}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.Lang.Deno.Tests", "StellaOps.Scanner.Analyzers.Lang.Deno.Tests", "{E49112A1-06DA-43F4-4A8D-5E805A6466FC}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.Lang.DotNet.Tests", "StellaOps.Scanner.Analyzers.Lang.DotNet.Tests", "{446F1BC4-7609-555E-1FA7-901560438951}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.Lang.Go.Tests", "StellaOps.Scanner.Analyzers.Lang.Go.Tests", "{327615A7-CCA8-CAAA-A279-54A2EB3C8D8C}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.Lang.Java.Tests", "StellaOps.Scanner.Analyzers.Lang.Java.Tests", "{1006432F-63A0-8569-EEC7-4393A1918480}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.Lang.Node.SmokeTests", "StellaOps.Scanner.Analyzers.Lang.Node.SmokeTests", "{4B6B949F-4BF5-2817-4818-71E26BF57749}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.Lang.Node.Tests", "StellaOps.Scanner.Analyzers.Lang.Node.Tests", "{FBBE5FEA-8CD5-8521-DC93-06C239F7876D}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.Lang.Php.Tests", "StellaOps.Scanner.Analyzers.Lang.Php.Tests", "{1F48E1F9-072B-1F1D-7E58-9C065D0F4ED1}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.Lang.Python.Tests", "StellaOps.Scanner.Analyzers.Lang.Python.Tests", "{5CC00E13-DAA8-7285-CDB0-D482D750FFBD}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.Lang.Ruby.Tests", "StellaOps.Scanner.Analyzers.Lang.Ruby.Tests", "{D2BC4D25-D54D-1778-798D-6547A38EEB05}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.Lang.Tests", "StellaOps.Scanner.Analyzers.Lang.Tests", "{7AC29AE8-7BDA-4243-5869-D89EF3921DC3}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.Native.Tests", "StellaOps.Scanner.Analyzers.Native.Tests", "{6E200246-2354-ECCC-0D81-F58C27F7551C}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.OS.Homebrew.Tests", "StellaOps.Scanner.Analyzers.OS.Homebrew.Tests", "{2F13F88C-7820-12E7-1FEF-4DB0DDE34DAB}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.OS.MacOsBundle.Tests", "StellaOps.Scanner.Analyzers.OS.MacOsBundle.Tests", "{ED480238-AEBE-3C29-8D80-5C0AB45E13D3}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.OS.Pkgutil.Tests", "StellaOps.Scanner.Analyzers.OS.Pkgutil.Tests", "{633ED6DB-FFB9-F767-070D-09C342DB4B74}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.OS.Tests", "StellaOps.Scanner.Analyzers.OS.Tests", "{235A45A7-331C-25BE-DB53-C2C328987E22}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.OS.Windows.Chocolatey.Tests", "StellaOps.Scanner.Analyzers.OS.Windows.Chocolatey.Tests", "{63E6AEF3-80BE-0FCE-E677-D01F65E1424F}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.OS.Windows.Msi.Tests", "StellaOps.Scanner.Analyzers.OS.Windows.Msi.Tests", "{D17D4B09-569F-CD45-70A8-2E4DDDE53E69}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Analyzers.OS.Windows.WinSxS.Tests", "StellaOps.Scanner.Analyzers.OS.Windows.WinSxS.Tests", "{C1767CE0-3470-0BE7-DF63-57D1C7455A8A}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Benchmarks.Tests", "StellaOps.Scanner.Benchmarks.Tests", "{4E63ED70-BA5F-324B-9E64-B04E64B5C0C7}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Cache.Tests", "StellaOps.Scanner.Cache.Tests", "{F381910D-309A-2065-DD77-7494C2FD23BF}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.CallGraph.Tests", "StellaOps.Scanner.CallGraph.Tests", "{51A857B4-DBDA-1B5A-0F80-83580CA223B4}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Core.Tests", "StellaOps.Scanner.Core.Tests", "{63B739C4-3FFB-AD71-3B39-8EA5128D8127}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Diff.Tests", "StellaOps.Scanner.Diff.Tests", "{4DCC3C48-6209-AF26-3864-EAB021E65AD5}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Emit.Lineage.Tests", "StellaOps.Scanner.Emit.Lineage.Tests", "{7C0B72FC-8CBC-D831-9614-5C4CAE8197D3}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Emit.Tests", "StellaOps.Scanner.Emit.Tests", "{1AD4003A-C2C9-E202-16B4-973BE4ECFEB0}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.EntryTrace.Tests", "StellaOps.Scanner.EntryTrace.Tests", "{A684BCCD-046B-5314-B492-13A65C7AAC2A}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Evidence.Tests", "StellaOps.Scanner.Evidence.Tests", "{7A54278E-939F-447A-17F4-15DDB3F6B403}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Explainability.Tests", "StellaOps.Scanner.Explainability.Tests", "{AF719B05-6089-CA3F-96A7-8F59902FF781}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Integration.Tests", "StellaOps.Scanner.Integration.Tests", "{D6EFF950-E1CE-F003-26E6-CCD4CE93C144}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.ProofSpine.Tests", "StellaOps.Scanner.ProofSpine.Tests", "{44F1D673-6AB1-2635-567A-4512FEA53D32}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Queue.Tests", "StellaOps.Scanner.Queue.Tests", "{4B435085-030A-E80A-F018-FAEE801E2FB4}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Reachability.Stack.Tests", "StellaOps.Scanner.Reachability.Stack.Tests", "{5E698EC5-3B12-9266-8F90-ED9B6C841C6A}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Reachability.Tests", "StellaOps.Scanner.Reachability.Tests", "{D767CC98-7156-89E4-C89E-0B4624D83584}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.ReachabilityDrift.Tests", "StellaOps.Scanner.ReachabilityDrift.Tests", "{DE5FDAC5-0355-823F-552F-5A37661C9FE5}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Sbomer.BuildXPlugin.Tests", "StellaOps.Scanner.Sbomer.BuildXPlugin.Tests", "{0BA16F05-266F-4BCC-04A6-25E7D566E769}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.SmartDiff.Tests", "StellaOps.Scanner.SmartDiff.Tests", "{540911AA-2FA7-DF51-2203-B6109B580346}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Storage.Oci.Tests", "StellaOps.Scanner.Storage.Oci.Tests", "{B4801F0F-50AB-5613-AB78-5225D80E0421}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Storage.Tests", "StellaOps.Scanner.Storage.Tests", "{42729022-D163-60E6-B0BA-6FA7E84DA52F}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Surface.Env.Tests", "StellaOps.Scanner.Surface.Env.Tests", "{6A2FEC90-D817-C92B-89B8-8B2E2DD18FF1}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Surface.FS.Tests", "StellaOps.Scanner.Surface.FS.Tests", "{EA3E79FB-BEC9-7590-EC7C-64D2CF8E8782}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Surface.Secrets.Tests", "StellaOps.Scanner.Surface.Secrets.Tests", "{D835F664-32BC-4CA8-2ABB-97D30953F05C}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Surface.Tests", "StellaOps.Scanner.Surface.Tests", "{BC4C56DA-A389-5A58-7CB0-D7AA2AE44430}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Surface.Validation.Tests", "StellaOps.Scanner.Surface.Validation.Tests", "{AF8D5B15-8ADF-FA1F-342A-73D095FD5B21}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Triage.Tests", "StellaOps.Scanner.Triage.Tests", "{A27B1BB8-9415-4C14-CA41-828B66357E0B}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.WebService.Tests", "StellaOps.Scanner.WebService.Tests", "{69D6403C-9C1F-04C3-BADF-8CFFBD3848E7}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StellaOps.Scanner.Worker.Tests", "StellaOps.Scanner.Worker.Tests", "{C26F680C-684A-ECC6-BB6C-EBD19DC43B4C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.AirGap.Importer", "E:\dev\git.stella-ops.org\src\AirGap\StellaOps.AirGap.Importer\StellaOps.AirGap.Importer.csproj", "{22B129C7-C609-3B90-AD56-64C746A1505E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.AirGap.Policy", "E:\dev\git.stella-ops.org\src\AirGap\StellaOps.AirGap.Policy\StellaOps.AirGap.Policy\StellaOps.AirGap.Policy.csproj", "{AD31623A-BC43-52C2-D906-AC1D8784A541}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Aoc", "E:\dev\git.stella-ops.org\src\Aoc\__Libraries\StellaOps.Aoc\StellaOps.Aoc.csproj", "{776E2142-804F-03B9-C804-D061D64C6092}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Attestor.Core", "E:\dev\git.stella-ops.org\src\Attestor\StellaOps.Attestor\StellaOps.Attestor.Core\StellaOps.Attestor.Core.csproj", "{5B4DF41E-C8CC-2606-FA2D-967118BD3C59}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Attestor.Envelope", "E:\dev\git.stella-ops.org\src\Attestor\StellaOps.Attestor.Envelope\StellaOps.Attestor.Envelope.csproj", "{3D8C5A6C-462D-7487-5BD0-A3EF6B657EB6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Attestor.GraphRoot", "E:\dev\git.stella-ops.org\src\Attestor\__Libraries\StellaOps.Attestor.GraphRoot\StellaOps.Attestor.GraphRoot.csproj", "{2609BC1A-6765-29BE-78CC-C0F1D2814F10}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Attestor.ProofChain", "E:\dev\git.stella-ops.org\src\Attestor\__Libraries\StellaOps.Attestor.ProofChain\StellaOps.Attestor.ProofChain.csproj", "{C6822231-A4F4-9E69-6CE2-4FDB3E81C728}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Auth.Abstractions", "E:\dev\git.stella-ops.org\src\Authority\StellaOps.Authority\StellaOps.Auth.Abstractions\StellaOps.Auth.Abstractions.csproj", "{55D9B653-FB76-FCE8-1A3C-67B1BEDEC214}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Auth.Client", "E:\dev\git.stella-ops.org\src\Authority\StellaOps.Authority\StellaOps.Auth.Client\StellaOps.Auth.Client.csproj", "{DE5BF139-1E5C-D6EA-4FAA-661EF353A194}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Auth.Security", "E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.Auth.Security\StellaOps.Auth.Security.csproj", "{335E62C0-9E69-A952-680B-753B1B17C6D0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Auth.ServerIntegration", "E:\dev\git.stella-ops.org\src\Authority\StellaOps.Authority\StellaOps.Auth.ServerIntegration\StellaOps.Auth.ServerIntegration.csproj", "{ECA25786-A3A8-92C4-4AA3-D4A73C69FDCA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Authority.Core", "E:\dev\git.stella-ops.org\src\Authority\__Libraries\StellaOps.Authority.Core\StellaOps.Authority.Core.csproj", "{5A6CD890-8142-F920-3734-D67CA3E65F61}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Authority.Persistence", "E:\dev\git.stella-ops.org\src\Authority\__Libraries\StellaOps.Authority.Persistence\StellaOps.Authority.Persistence.csproj", "{A260E14F-DBA4-862E-53CD-18D3B92ADA3D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Authority.Plugins.Abstractions", "E:\dev\git.stella-ops.org\src\Authority\StellaOps.Authority\StellaOps.Authority.Plugins.Abstractions\StellaOps.Authority.Plugins.Abstractions.csproj", "{97F94029-5419-6187-5A63-5C8FD9232FAE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.BinaryIndex.Contracts", "E:\dev\git.stella-ops.org\src\BinaryIndex\__Libraries\StellaOps.BinaryIndex.Contracts\StellaOps.BinaryIndex.Contracts.csproj", "{03DF5914-2390-A82D-7464-642D0B95E068}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.BinaryIndex.Core", "E:\dev\git.stella-ops.org\src\BinaryIndex\__Libraries\StellaOps.BinaryIndex.Core\StellaOps.BinaryIndex.Core.csproj", "{CF633BDA-9F2E-D0C8-702F-BC9D27363B4B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.BinaryIndex.Corpus", "E:\dev\git.stella-ops.org\src\BinaryIndex\__Libraries\StellaOps.BinaryIndex.Corpus\StellaOps.BinaryIndex.Corpus.csproj", "{73DE9C04-CEFE-53BA-A527-3A36D478DEFE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.BinaryIndex.Fingerprints", "E:\dev\git.stella-ops.org\src\BinaryIndex\__Libraries\StellaOps.BinaryIndex.Fingerprints\StellaOps.BinaryIndex.Fingerprints.csproj", "{B7DC1B0A-EBD8-B1E8-28C8-9D5F19E118AD}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.BinaryIndex.FixIndex", "E:\dev\git.stella-ops.org\src\BinaryIndex\__Libraries\StellaOps.BinaryIndex.FixIndex\StellaOps.BinaryIndex.FixIndex.csproj", "{0B56708E-B56C-E058-DE31-FCDFF30031F7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.BinaryIndex.Persistence", "E:\dev\git.stella-ops.org\src\BinaryIndex\__Libraries\StellaOps.BinaryIndex.Persistence\StellaOps.BinaryIndex.Persistence.csproj", "{78FAD457-CE1B-D78E-A602-510EAD85E0AF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Canonical.Json", "E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.Canonical.Json\StellaOps.Canonical.Json.csproj", "{AF9E7F02-25AD-3540-18D7-F6A4F8BA5A60}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Cache.Valkey", "E:\dev\git.stella-ops.org\src\Concelier\__Libraries\StellaOps.Concelier.Cache.Valkey\StellaOps.Concelier.Cache.Valkey.csproj", "{AB6AE2B6-8D6B-2D9F-2A88-7C596C59F4FC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Common", "E:\dev\git.stella-ops.org\src\Concelier\__Libraries\StellaOps.Concelier.Connector.Common\StellaOps.Concelier.Connector.Common.csproj", "{375F5AD0-F7EE-1782-7B34-E181CDB61B9F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Core", "E:\dev\git.stella-ops.org\src\Concelier\__Libraries\StellaOps.Concelier.Core\StellaOps.Concelier.Core.csproj", "{BA45605A-1CCE-6B0C-489D-C113915B243F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Interest", "E:\dev\git.stella-ops.org\src\Concelier\__Libraries\StellaOps.Concelier.Interest\StellaOps.Concelier.Interest.csproj", "{9D31FC8A-2A69-B78A-D3E5-4F867B16D971}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Merge", "E:\dev\git.stella-ops.org\src\Concelier\__Libraries\StellaOps.Concelier.Merge\StellaOps.Concelier.Merge.csproj", "{92268008-FBB0-C7AD-ECC2-7B75BED9F5E1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Models", "E:\dev\git.stella-ops.org\src\Concelier\__Libraries\StellaOps.Concelier.Models\StellaOps.Concelier.Models.csproj", "{8DCCAF70-D364-4C8B-4E90-AF65091DE0C5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Normalization", "E:\dev\git.stella-ops.org\src\Concelier\__Libraries\StellaOps.Concelier.Normalization\StellaOps.Concelier.Normalization.csproj", "{7828C164-DD01-2809-CCB3-364486834F60}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Persistence", "E:\dev\git.stella-ops.org\src\Concelier\__Libraries\StellaOps.Concelier.Persistence\StellaOps.Concelier.Persistence.csproj", "{DE95E7B2-0937-A980-441F-829E023BC43E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.ProofService", "E:\dev\git.stella-ops.org\src\Concelier\__Libraries\StellaOps.Concelier.ProofService\StellaOps.Concelier.ProofService.csproj", "{91D69463-23E2-E2C7-AA7E-A78B13CED620}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.RawModels", "E:\dev\git.stella-ops.org\src\Concelier\__Libraries\StellaOps.Concelier.RawModels\StellaOps.Concelier.RawModels.csproj", "{34EFF636-81A7-8DF6-7CC9-4DA784BAC7F3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.SbomIntegration", "E:\dev\git.stella-ops.org\src\Concelier\__Libraries\StellaOps.Concelier.SbomIntegration\StellaOps.Concelier.SbomIntegration.csproj", "{5DCF16A8-97C6-2CB4-6A63-0370239039EB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.SourceIntel", "E:\dev\git.stella-ops.org\src\Concelier\__Libraries\StellaOps.Concelier.SourceIntel\StellaOps.Concelier.SourceIntel.csproj", "{EB093C48-CDAC-106B-1196-AE34809B34C0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Configuration", "E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.Configuration\StellaOps.Configuration.csproj", "{92C62F7B-8028-6EE1-B71B-F45F459B8E97}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Cryptography", "E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.Cryptography\StellaOps.Cryptography.csproj", "{F664A948-E352-5808-E780-77A03F19E93E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Cryptography.DependencyInjection", "E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.Cryptography.DependencyInjection\StellaOps.Cryptography.DependencyInjection.csproj", "{FA83F778-5252-0B80-5555-E69F790322EA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Cryptography.Kms", "E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.Cryptography.Kms\StellaOps.Cryptography.Kms.csproj", "{F3A27846-6DE0-3448-222C-25A273E86B2E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Cryptography.Plugin.BouncyCastle", "E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.Cryptography.Plugin.BouncyCastle\StellaOps.Cryptography.Plugin.BouncyCastle.csproj", "{166F4DEC-9886-92D5-6496-085664E9F08F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Cryptography.Plugin.CryptoPro", "E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.Cryptography.Plugin.CryptoPro\StellaOps.Cryptography.Plugin.CryptoPro.csproj", "{C53E0895-879A-D9E6-0A43-24AD17A2F270}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Cryptography.Plugin.OfflineVerification", "E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.Cryptography.Plugin.OfflineVerification\StellaOps.Cryptography.Plugin.OfflineVerification.csproj", "{246FCC7C-1437-742D-BAE5-E77A24164F08}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Cryptography.Plugin.OpenSslGost", "E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.Cryptography.Plugin.OpenSslGost\StellaOps.Cryptography.Plugin.OpenSslGost.csproj", "{0AED303F-69E6-238F-EF80-81985080EDB7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Cryptography.Plugin.Pkcs11Gost", "E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.Cryptography.Plugin.Pkcs11Gost\StellaOps.Cryptography.Plugin.Pkcs11Gost.csproj", "{2904D288-CE64-A565-2C46-C2E85A96A1EE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Cryptography.Plugin.PqSoft", "E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.Cryptography.Plugin.PqSoft\StellaOps.Cryptography.Plugin.PqSoft.csproj", "{A6667CC3-B77F-023E-3A67-05F99E9FF46A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Cryptography.Plugin.SimRemote", "E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.Cryptography.Plugin.SimRemote\StellaOps.Cryptography.Plugin.SimRemote.csproj", "{A26E2816-F787-F76B-1D6C-E086DD3E19CE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Cryptography.Plugin.SmRemote", "E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.Cryptography.Plugin.SmRemote\StellaOps.Cryptography.Plugin.SmRemote.csproj", "{B3DEC619-67AC-1B5A-4F3E-A1F24C3F6877}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Cryptography.Plugin.SmSoft", "E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.Cryptography.Plugin.SmSoft\StellaOps.Cryptography.Plugin.SmSoft.csproj", "{90DB65B4-8F6E-FB8E-0281-505AD8BC6BA6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Cryptography.Plugin.WineCsp", "E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.Cryptography.Plugin.WineCsp\StellaOps.Cryptography.Plugin.WineCsp.csproj", "{059FBB86-DEE6-8207-3F23-2A1A3EC00DEA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Cryptography.PluginLoader", "E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.Cryptography.PluginLoader\StellaOps.Cryptography.PluginLoader.csproj", "{8BBA3159-C4CC-F685-A28C-7FE6CBD3D2A1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.DeltaVerdict", "E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.DeltaVerdict\StellaOps.DeltaVerdict.csproj", "{EA0974E3-CD2B-5792-EF1E-9B5B7CCBDF00}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.DependencyInjection", "E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.DependencyInjection\StellaOps.DependencyInjection.csproj", "{632A1F0D-1BA5-C84B-B716-2BE638A92780}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Evidence.Bundle", "E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.Evidence.Bundle\StellaOps.Evidence.Bundle.csproj", "{9DE7852B-7E2D-257E-B0F1-45D2687854ED}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Evidence.Core", "E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.Evidence.Core\StellaOps.Evidence.Core.csproj", "{DC2AFC89-C3C8-4E9B-13A7-027EB6386EFA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Excititor.Core", "E:\dev\git.stella-ops.org\src\Excititor\__Libraries\StellaOps.Excititor.Core\StellaOps.Excititor.Core.csproj", "{9151601C-8784-01A6-C2E7-A5C0FAAB0AEF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Feedser.BinaryAnalysis", "E:\dev\git.stella-ops.org\src\Feedser\StellaOps.Feedser.BinaryAnalysis\StellaOps.Feedser.BinaryAnalysis.csproj", "{CB296A20-2732-77C1-7F23-27D5BAEDD0C7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Feedser.Core", "E:\dev\git.stella-ops.org\src\Feedser\StellaOps.Feedser.Core\StellaOps.Feedser.Core.csproj", "{0DBEC9BA-FE1D-3898-B2C6-E4357DC23E0F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Infrastructure.EfCore", "E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.Infrastructure.EfCore\StellaOps.Infrastructure.EfCore.csproj", "{A63897D9-9531-989B-7309-E384BCFC2BB9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Infrastructure.Postgres", "E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.Infrastructure.Postgres\StellaOps.Infrastructure.Postgres.csproj", "{8C594D82-3463-3367-4F06-900AC707753D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Infrastructure.Postgres.Testing", "E:\dev\git.stella-ops.org\src\__Tests\__Libraries\StellaOps.Infrastructure.Postgres.Testing\StellaOps.Infrastructure.Postgres.Testing.csproj", "{52F400CD-D473-7A1F-7986-89011CD2A887}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Ingestion.Telemetry", "E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.Ingestion.Telemetry\StellaOps.Ingestion.Telemetry.csproj", "{9588FBF9-C37E-D16E-2E8F-CFA226EAC01D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Messaging", "E:\dev\git.stella-ops.org\src\Router\__Libraries\StellaOps.Messaging\StellaOps.Messaging.csproj", "{97998C88-E6E1-D5E2-B632-537B58E00CBF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Microservice", "E:\dev\git.stella-ops.org\src\Router\__Libraries\StellaOps.Microservice\StellaOps.Microservice.csproj", "{BAD08D96-A80A-D27F-5D9C-656AEEB3D568}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Microservice.AspNetCore", "E:\dev\git.stella-ops.org\src\Router\__Libraries\StellaOps.Microservice.AspNetCore\StellaOps.Microservice.AspNetCore.csproj", "{F63694F1-B56D-6E72-3F5D-5D38B1541F0F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Notify.Models", "E:\dev\git.stella-ops.org\src\Notify\__Libraries\StellaOps.Notify.Models\StellaOps.Notify.Models.csproj", "{20D1569C-2A47-38B8-075E-47225B674394}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Plugin", "E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.Plugin\StellaOps.Plugin.csproj", "{38A9EE9B-6FC8-93BC-0D43-2A906E678D66}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Policy", "E:\dev\git.stella-ops.org\src\Policy\__Libraries\StellaOps.Policy\StellaOps.Policy.csproj", "{19868E2D-7163-2108-1094-F13887C4F070}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Policy.RiskProfile", "E:\dev\git.stella-ops.org\src\Policy\StellaOps.Policy.RiskProfile\StellaOps.Policy.RiskProfile.csproj", "{CC319FC5-F4B1-C3DD-7310-4DAD343E0125}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Provcache", "E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.Provcache\StellaOps.Provcache.csproj", "{84F711C2-C210-28D2-F0D9-B13733FEE23D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Provenance", "E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.Provenance\StellaOps.Provenance.csproj", "{CBB14B90-27F9-8DD6-DFC4-3507DBD1FBC6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Provenance.Attestation", "E:\dev\git.stella-ops.org\src\Provenance\StellaOps.Provenance.Attestation\StellaOps.Provenance.Attestation.csproj", "{A78EBC0F-C62C-8F56-95C0-330E376242A2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Replay.Core", "E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.Replay.Core\StellaOps.Replay.Core.csproj", "{6D26FB21-7E48-024B-E5D4-E3F0F31976BB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Router.AspNet", "E:\dev\git.stella-ops.org\src\Router\__Libraries\StellaOps.Router.AspNet\StellaOps.Router.AspNet.csproj", "{79104479-B087-E5D0-5523-F1803282A246}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Router.Common", "E:\dev\git.stella-ops.org\src\Router\__Libraries\StellaOps.Router.Common\StellaOps.Router.Common.csproj", "{F17A6F0B-3120-2BA9-84D8-5F8BA0B9705D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Advisory", "__Libraries\StellaOps.Scanner.Advisory\StellaOps.Scanner.Advisory.csproj", "{FBD908D6-AF93-CC62-C09D-F0BB3E0CEA7F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Advisory.Tests", "__Tests\StellaOps.Scanner.Advisory.Tests\StellaOps.Scanner.Advisory.Tests.csproj", "{37F9B25E-81CF-95C5-0311-EA6DA191E415}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Lang", "__Libraries\StellaOps.Scanner.Analyzers.Lang\StellaOps.Scanner.Analyzers.Lang.csproj", "{28D91816-206C-576E-1A83-FD98E08C2E3C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Lang.Bun", "__Libraries\StellaOps.Scanner.Analyzers.Lang.Bun\StellaOps.Scanner.Analyzers.Lang.Bun.csproj", "{5EFEC79C-A9F1-96A4-692C-733566107170}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Lang.Bun.Tests", "__Tests\StellaOps.Scanner.Analyzers.Lang.Bun.Tests\StellaOps.Scanner.Analyzers.Lang.Bun.Tests.csproj", "{F4E7E32B-D78B-5A08-F7B5-E8D4C7ED20D3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Lang.Deno", "__Libraries\StellaOps.Scanner.Analyzers.Lang.Deno\StellaOps.Scanner.Analyzers.Lang.Deno.csproj", "{3A1CFB24-6EAA-9A87-7783-BFC56B0E5394}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Lang.Deno.Benchmarks", "__Benchmarks\StellaOps.Scanner.Analyzers.Lang.Deno.Benchmarks\StellaOps.Scanner.Analyzers.Lang.Deno.Benchmarks.csproj", "{B1969736-DE03-ADEB-2659-55B2B82B38A8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Lang.Deno.Tests", "__Tests\StellaOps.Scanner.Analyzers.Lang.Deno.Tests\StellaOps.Scanner.Analyzers.Lang.Deno.Tests.csproj", "{D166FCF0-F220-A013-133A-620521740411}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Lang.DotNet", "__Libraries\StellaOps.Scanner.Analyzers.Lang.DotNet\StellaOps.Scanner.Analyzers.Lang.DotNet.csproj", "{F638D731-2DB2-2278-D9F8-019418A264F2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Lang.DotNet.Tests", "__Tests\StellaOps.Scanner.Analyzers.Lang.DotNet.Tests\StellaOps.Scanner.Analyzers.Lang.DotNet.Tests.csproj", "{CAEB1FEB-B3F1-4B9D-7FEC-1983BCA60D81}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Lang.Go", "__Libraries\StellaOps.Scanner.Analyzers.Lang.Go\StellaOps.Scanner.Analyzers.Lang.Go.csproj", "{B07074FE-3D4E-5957-5F81-B75B5D25BD1B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Lang.Go.Tests", "__Tests\StellaOps.Scanner.Analyzers.Lang.Go.Tests\StellaOps.Scanner.Analyzers.Lang.Go.Tests.csproj", "{91B8E22B-C90B-AEBD-707E-57BBD549BA32}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Lang.Java", "__Libraries\StellaOps.Scanner.Analyzers.Lang.Java\StellaOps.Scanner.Analyzers.Lang.Java.csproj", "{B7B5D764-C3A0-1743-0739-29966F993626}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Lang.Java.Tests", "__Tests\StellaOps.Scanner.Analyzers.Lang.Java.Tests\StellaOps.Scanner.Analyzers.Lang.Java.Tests.csproj", "{E9039B92-DAFC-F20B-22D6-78F8E4EB7CF1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Lang.Node", "__Libraries\StellaOps.Scanner.Analyzers.Lang.Node\StellaOps.Scanner.Analyzers.Lang.Node.csproj", "{C4EDBBAF-875C-4839-05A8-F6F12A5ED52D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Lang.Node.SmokeTests", "__Tests\StellaOps.Scanner.Analyzers.Lang.Node.SmokeTests\StellaOps.Scanner.Analyzers.Lang.Node.SmokeTests.csproj", "{04444789-CEE4-3F3A-6EFA-18416E620B2A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Lang.Node.Tests", "__Tests\StellaOps.Scanner.Analyzers.Lang.Node.Tests\StellaOps.Scanner.Analyzers.Lang.Node.Tests.csproj", "{AD1B6448-2DC9-2F9A-D143-F09BBDF6F01F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Lang.Php", "__Libraries\StellaOps.Scanner.Analyzers.Lang.Php\StellaOps.Scanner.Analyzers.Lang.Php.csproj", "{0EAC8F64-9588-1EF0-C33A-67590CF27590}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Lang.Php.Benchmarks", "__Benchmarks\StellaOps.Scanner.Analyzers.Lang.Php.Benchmarks\StellaOps.Scanner.Analyzers.Lang.Php.Benchmarks.csproj", "{761CAD6D-98CB-1936-9065-BF1A756671FF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Lang.Php.Tests", "__Tests\StellaOps.Scanner.Analyzers.Lang.Php.Tests\StellaOps.Scanner.Analyzers.Lang.Php.Tests.csproj", "{7974C4F0-BC89-2775-8943-2DF909F3B08B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Lang.Python", "__Libraries\StellaOps.Scanner.Analyzers.Lang.Python\StellaOps.Scanner.Analyzers.Lang.Python.csproj", "{B1B31937-CCC8-D97A-F66D-1849734B780B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Lang.Python.Tests", "__Tests\StellaOps.Scanner.Analyzers.Lang.Python.Tests\StellaOps.Scanner.Analyzers.Lang.Python.Tests.csproj", "{9A566B3A-E281-09AF-EC1B-BD4FDB3248CE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Lang.Ruby", "__Libraries\StellaOps.Scanner.Analyzers.Lang.Ruby\StellaOps.Scanner.Analyzers.Lang.Ruby.csproj", "{A345E5AC-BDDB-A817-3C92-08C8865D1EF9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Lang.Ruby.Tests", "__Tests\StellaOps.Scanner.Analyzers.Lang.Ruby.Tests\StellaOps.Scanner.Analyzers.Lang.Ruby.Tests.csproj", "{905DD8ED-3D10-7C2B-B199-B98E85267BB8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Lang.Rust", "__Libraries\StellaOps.Scanner.Analyzers.Lang.Rust\StellaOps.Scanner.Analyzers.Lang.Rust.csproj", "{C2D3B3C7-E556-9B72-024A-FF5EDEAF4CB5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Lang.Rust.Benchmarks", "__Benchmarks\StellaOps.Scanner.Analyzers.Lang.Rust.Benchmarks\StellaOps.Scanner.Analyzers.Lang.Rust.Benchmarks.csproj", "{31AC6B88-D6C8-E2EF-39AF-1B21AFD76C89}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Lang.Tests", "__Tests\StellaOps.Scanner.Analyzers.Lang.Tests\StellaOps.Scanner.Analyzers.Lang.Tests.csproj", "{90B84537-F992-234C-C998-91C6AD65AB12}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Native", "__Libraries\StellaOps.Scanner.Analyzers.Native\StellaOps.Scanner.Analyzers.Native.csproj", "{F22333B6-7E27-679B-8475-B4B9AB1CB186}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Native", "StellaOps.Scanner.Analyzers.Native\StellaOps.Scanner.Analyzers.Native.csproj", "{CE042F3A-6851-FAAB-9E9C-AD905B4AAC8D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Native.Tests", "__Tests\StellaOps.Scanner.Analyzers.Native.Tests\StellaOps.Scanner.Analyzers.Native.Tests.csproj", "{D6B56A54-4057-9F76-BC7E-56E896E5D276}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.OS", "__Libraries\StellaOps.Scanner.Analyzers.OS\StellaOps.Scanner.Analyzers.OS.csproj", "{9258E4F2-762C-C780-F118-2CABD0281CC9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.OS.Apk", "__Libraries\StellaOps.Scanner.Analyzers.OS.Apk\StellaOps.Scanner.Analyzers.OS.Apk.csproj", "{D6752A7E-0FD2-6626-80E3-CE4D4816D2B0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.OS.Dpkg", "__Libraries\StellaOps.Scanner.Analyzers.OS.Dpkg\StellaOps.Scanner.Analyzers.OS.Dpkg.csproj", "{AF85AC87-521A-2F0E-5F10-836E416EC716}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.OS.Homebrew", "__Libraries\StellaOps.Scanner.Analyzers.OS.Homebrew\StellaOps.Scanner.Analyzers.OS.Homebrew.csproj", "{FB946C57-55B3-08C6-18AE-1672D46C5308}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.OS.Homebrew.Tests", "__Tests\StellaOps.Scanner.Analyzers.OS.Homebrew.Tests\StellaOps.Scanner.Analyzers.OS.Homebrew.Tests.csproj", "{99A47EAA-44B8-8E06-DA0E-05B225009FDF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.OS.MacOsBundle", "__Libraries\StellaOps.Scanner.Analyzers.OS.MacOsBundle\StellaOps.Scanner.Analyzers.OS.MacOsBundle.csproj", "{4F0EF830-4308-347B-A31D-270A9812D15E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.OS.MacOsBundle.Tests", "__Tests\StellaOps.Scanner.Analyzers.OS.MacOsBundle.Tests\StellaOps.Scanner.Analyzers.OS.MacOsBundle.Tests.csproj", "{B7EE2F70-A634-8B4D-93F4-EA1EEFD9E5E8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.OS.Pkgutil", "__Libraries\StellaOps.Scanner.Analyzers.OS.Pkgutil\StellaOps.Scanner.Analyzers.OS.Pkgutil.csproj", "{A5298720-984E-6574-D41B-CFE7CA408182}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.OS.Pkgutil.Tests", "__Tests\StellaOps.Scanner.Analyzers.OS.Pkgutil.Tests\StellaOps.Scanner.Analyzers.OS.Pkgutil.Tests.csproj", "{CB033CB6-F90B-E201-BA86-C867544E7247}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.OS.Rpm", "__Libraries\StellaOps.Scanner.Analyzers.OS.Rpm\StellaOps.Scanner.Analyzers.OS.Rpm.csproj", "{E9F5BFF2-0D0E-7B41-9AF0-83384F4B8825}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.OS.Tests", "__Tests\StellaOps.Scanner.Analyzers.OS.Tests\StellaOps.Scanner.Analyzers.OS.Tests.csproj", "{668466AC-CD66-BAA0-0322-148549E373CB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.OS.Windows.Chocolatey", "__Libraries\StellaOps.Scanner.Analyzers.OS.Windows.Chocolatey\StellaOps.Scanner.Analyzers.OS.Windows.Chocolatey.csproj", "{07EBBFA6-798E-76A3-CAF0-67828B00B58E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.OS.Windows.Chocolatey.Tests", "__Tests\StellaOps.Scanner.Analyzers.OS.Windows.Chocolatey.Tests\StellaOps.Scanner.Analyzers.OS.Windows.Chocolatey.Tests.csproj", "{181ED0FE-FE20-069F-7CCF-86FF5449D7F5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.OS.Windows.Msi", "__Libraries\StellaOps.Scanner.Analyzers.OS.Windows.Msi\StellaOps.Scanner.Analyzers.OS.Windows.Msi.csproj", "{5E683B7C-B584-0E56-C8D6-D29050DE70FB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.OS.Windows.Msi.Tests", "__Tests\StellaOps.Scanner.Analyzers.OS.Windows.Msi.Tests\StellaOps.Scanner.Analyzers.OS.Windows.Msi.Tests.csproj", "{4163E755-1563-6A72-60E7-BB2B69F5ABA2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.OS.Windows.WinSxS", "__Libraries\StellaOps.Scanner.Analyzers.OS.Windows.WinSxS\StellaOps.Scanner.Analyzers.OS.Windows.WinSxS.csproj", "{AE6F3DA7-2993-6926-323E-A29295D55C36}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.OS.Windows.WinSxS.Tests", "__Tests\StellaOps.Scanner.Analyzers.OS.Windows.WinSxS.Tests\StellaOps.Scanner.Analyzers.OS.Windows.WinSxS.Tests.csproj", "{D013641A-8457-6215-05A1-74BB57B58409}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Benchmark", "__Libraries\StellaOps.Scanner.Benchmark\StellaOps.Scanner.Benchmark.csproj", "{4FC29140-9C5F-8DD5-B405-6982BD1DA5A3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Benchmarks", "__Libraries\StellaOps.Scanner.Benchmarks\StellaOps.Scanner.Benchmarks.csproj", "{B9C9A1E4-3BB8-C8BE-7819-660A582D2952}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Benchmarks.Tests", "__Tests\StellaOps.Scanner.Benchmarks.Tests\StellaOps.Scanner.Benchmarks.Tests.csproj", "{2BBAB3B4-2E18-F945-F7AB-6207D7F72714}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Cache", "__Libraries\StellaOps.Scanner.Cache\StellaOps.Scanner.Cache.csproj", "{BA492274-A505-BCD5-3DA5-EE0C94DD5748}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Cache.Tests", "__Tests\StellaOps.Scanner.Cache.Tests\StellaOps.Scanner.Cache.Tests.csproj", "{029F8300-57F5-9CCD-505E-708937686679}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.CallGraph", "__Libraries\StellaOps.Scanner.CallGraph\StellaOps.Scanner.CallGraph.csproj", "{A5D2DB78-8045-29AC-E4B1-66E72F2C7FF0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.CallGraph.Tests", "__Tests\StellaOps.Scanner.CallGraph.Tests\StellaOps.Scanner.CallGraph.Tests.csproj", "{294792C0-DC28-3C5D-2D59-33DC99CD6C61}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Core", "__Libraries\StellaOps.Scanner.Core\StellaOps.Scanner.Core.csproj", "{58D8630F-C0F4-B772-8572-BCC98FF0F0D8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Core.Tests", "__Tests\StellaOps.Scanner.Core.Tests\StellaOps.Scanner.Core.Tests.csproj", "{2B1B4954-1241-8F2E-75B6-2146D15D037B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Diff", "__Libraries\StellaOps.Scanner.Diff\StellaOps.Scanner.Diff.csproj", "{97A9C869-F385-6711-6B76-F3859C86DCAC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Diff.Tests", "__Tests\StellaOps.Scanner.Diff.Tests\StellaOps.Scanner.Diff.Tests.csproj", "{201CE292-0186-2A38-55D7-69890B5817DF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Emit", "__Libraries\StellaOps.Scanner.Emit\StellaOps.Scanner.Emit.csproj", "{17A00031-9FF7-4F73-5319-23FA5817625F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Emit.Lineage.Tests", "__Tests\StellaOps.Scanner.Emit.Lineage.Tests\StellaOps.Scanner.Emit.Lineage.Tests.csproj", "{11E0E129-091F-BEEB-A1B2-9BAEF5BE9FBC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Emit.Tests", "__Tests\StellaOps.Scanner.Emit.Tests\StellaOps.Scanner.Emit.Tests.csproj", "{AEF63403-4889-5396-CDEA-3B713CEF2ED7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.EntryTrace", "__Libraries\StellaOps.Scanner.EntryTrace\StellaOps.Scanner.EntryTrace.csproj", "{D24E7862-3930-A4F6-1DFA-DA88C759546C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.EntryTrace.Tests", "__Tests\StellaOps.Scanner.EntryTrace.Tests\StellaOps.Scanner.EntryTrace.Tests.csproj", "{6DC62619-949E-92E6-F4F1-5A0320959929}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Evidence", "__Libraries\StellaOps.Scanner.Evidence\StellaOps.Scanner.Evidence.csproj", "{37F1D83D-073C-C165-4C53-664AD87628E6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Evidence.Tests", "__Tests\StellaOps.Scanner.Evidence.Tests\StellaOps.Scanner.Evidence.Tests.csproj", "{CDC236E8-6881-46C4-EE95-3C386AF009D0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Explainability", "__Libraries\StellaOps.Scanner.Explainability\StellaOps.Scanner.Explainability.csproj", "{ACC2785F-F4B9-13E4-EED2-C5D067242175}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Explainability.Tests", "__Tests\StellaOps.Scanner.Explainability.Tests\StellaOps.Scanner.Explainability.Tests.csproj", "{7F4A3CA3-C3DA-A698-5CBC-54F28D1C7DFB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Integration.Tests", "__Tests\StellaOps.Scanner.Integration.Tests\StellaOps.Scanner.Integration.Tests.csproj", "{DAA1F516-DEC3-7FF2-3A63-78DD97BA062C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Orchestration", "__Libraries\StellaOps.Scanner.Orchestration\StellaOps.Scanner.Orchestration.csproj", "{11EF0DE9-2648-F711-6194-70B5C40B3F3F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.ProofIntegration", "__Libraries\StellaOps.Scanner.ProofIntegration\StellaOps.Scanner.ProofIntegration.csproj", "{01A21B47-07C5-6039-1B48-C5EACA3DBA2D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.ProofSpine", "__Libraries\StellaOps.Scanner.ProofSpine\StellaOps.Scanner.ProofSpine.csproj", "{7CB7FEA8-8A12-A5D6-0057-AA65DB328617}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.ProofSpine.Tests", "__Tests\StellaOps.Scanner.ProofSpine.Tests\StellaOps.Scanner.ProofSpine.Tests.csproj", "{0484DB46-3E40-1A10-131C-524AF1233EA7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Queue", "__Libraries\StellaOps.Scanner.Queue\StellaOps.Scanner.Queue.csproj", "{64E1D9B1-B944-8AA3-799F-02E7DD33FB78}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Queue.Tests", "__Tests\StellaOps.Scanner.Queue.Tests\StellaOps.Scanner.Queue.Tests.csproj", "{D37991E1-585F-FF1B-9772-07477E40AF78}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Reachability", "__Libraries\StellaOps.Scanner.Reachability\StellaOps.Scanner.Reachability.csproj", "{35A06F00-71AB-8A31-7D60-EBF41EA730CA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Reachability.Stack.Tests", "__Tests\StellaOps.Scanner.Reachability.Stack.Tests\StellaOps.Scanner.Reachability.Stack.Tests.csproj", "{56120A54-1D4D-F07B-63B4-B15525C2ADD9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Reachability.Tests", "__Tests\StellaOps.Scanner.Reachability.Tests\StellaOps.Scanner.Reachability.Tests.csproj", "{BE47FB74-D163-0B1F-5293-0962EA7E8585}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.ReachabilityDrift", "__Libraries\StellaOps.Scanner.ReachabilityDrift\StellaOps.Scanner.ReachabilityDrift.csproj", "{9AD932E9-0986-654C-B454-34E654C80697}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.ReachabilityDrift.Tests", "__Tests\StellaOps.Scanner.ReachabilityDrift.Tests\StellaOps.Scanner.ReachabilityDrift.Tests.csproj", "{00BE2B68-FC96-23F8-F61D-EE53B7AE06A1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Sbomer.BuildXPlugin", "StellaOps.Scanner.Sbomer.BuildXPlugin\StellaOps.Scanner.Sbomer.BuildXPlugin.csproj", "{570BA050-81A7-46EB-3DDD-422027EE2CA2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Sbomer.BuildXPlugin.Tests", "__Tests\StellaOps.Scanner.Sbomer.BuildXPlugin.Tests\StellaOps.Scanner.Sbomer.BuildXPlugin.Tests.csproj", "{6C43FD78-3478-F245-3EE4-E410D1E7D7C5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.SmartDiff", "__Libraries\StellaOps.Scanner.SmartDiff\StellaOps.Scanner.SmartDiff.csproj", "{7F0FFA06-EAC8-CC9A-3386-389638F12B59}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.SmartDiff.Tests", "__Tests\StellaOps.Scanner.SmartDiff.Tests\StellaOps.Scanner.SmartDiff.Tests.csproj", "{03B9D4BE-348B-9AD6-6FE9-6C40AE22053D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Storage", "__Libraries\StellaOps.Scanner.Storage\StellaOps.Scanner.Storage.csproj", "{35CF4CF2-8A84-378D-32F0-572F4AA900A3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Storage.Epss.Perf", "__Benchmarks\StellaOps.Scanner.Storage.Epss.Perf\StellaOps.Scanner.Storage.Epss.Perf.csproj", "{13E03C69-0634-3330-26D9-DCF7DD136BC5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Storage.Oci", "__Libraries\StellaOps.Scanner.Storage.Oci\StellaOps.Scanner.Storage.Oci.csproj", "{A80D212B-7E80-4251-16C0-60FA3670A5B4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Storage.Oci.Tests", "__Tests\StellaOps.Scanner.Storage.Oci.Tests\StellaOps.Scanner.Storage.Oci.Tests.csproj", "{2F4ACEB8-76C7-D5A2-6DD1-2EF713D9A197}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Storage.Tests", "__Tests\StellaOps.Scanner.Storage.Tests\StellaOps.Scanner.Storage.Tests.csproj", "{C146A9AF-6C13-B9DC-F555-37182A54430F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Surface", "__Libraries\StellaOps.Scanner.Surface\StellaOps.Scanner.Surface.csproj", "{E5025FCF-78CB-4B5B-3377-AE008B1BE9D2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Surface.Env", "__Libraries\StellaOps.Scanner.Surface.Env\StellaOps.Scanner.Surface.Env.csproj", "{52698305-D6F8-C13C-0882-48FC37726404}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Surface.Env.Tests", "__Tests\StellaOps.Scanner.Surface.Env.Tests\StellaOps.Scanner.Surface.Env.Tests.csproj", "{DE10AF97-E790-9D19-2399-70940A9B83A7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Surface.FS", "__Libraries\StellaOps.Scanner.Surface.FS\StellaOps.Scanner.Surface.FS.csproj", "{5567139C-0365-B6A0-5DD0-978A09B9F176}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Surface.FS.Tests", "__Tests\StellaOps.Scanner.Surface.FS.Tests\StellaOps.Scanner.Surface.FS.Tests.csproj", "{A56C1F0E-3E18-DBEE-7F97-B5FCBF23D1D6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Surface.Secrets", "__Libraries\StellaOps.Scanner.Surface.Secrets\StellaOps.Scanner.Surface.Secrets.csproj", "{256D269B-35EA-F833-2F1D-8E0058908DEE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Surface.Secrets.Tests", "__Tests\StellaOps.Scanner.Surface.Secrets.Tests\StellaOps.Scanner.Surface.Secrets.Tests.csproj", "{F02B63CD-2C69-61F7-7F96-930122D4D4D7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Surface.Tests", "__Tests\StellaOps.Scanner.Surface.Tests\StellaOps.Scanner.Surface.Tests.csproj", "{F061C879-063E-99DE-B301-E261DB12156F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Surface.Validation", "__Libraries\StellaOps.Scanner.Surface.Validation\StellaOps.Scanner.Surface.Validation.csproj", "{6E9C9582-67FA-2EB1-C6BA-AD4CD326E276}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Surface.Validation.Tests", "__Tests\StellaOps.Scanner.Surface.Validation.Tests\StellaOps.Scanner.Surface.Validation.Tests.csproj", "{FCF711C2-1090-7204-5E38-4BEFBE265A61}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Triage", "__Libraries\StellaOps.Scanner.Triage\StellaOps.Scanner.Triage.csproj", "{3A4AAE04-FA0C-2AF8-6548-AC22E5A41312}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Triage.Tests", "__Tests\StellaOps.Scanner.Triage.Tests\StellaOps.Scanner.Triage.Tests.csproj", "{66F8F288-C387-40E0-5F83-938671335703}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.VulnSurfaces", "__Libraries\StellaOps.Scanner.VulnSurfaces\StellaOps.Scanner.VulnSurfaces.csproj", "{7B3BDB83-918F-6760-3853-BDD70CD71B42}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.VulnSurfaces.Tests", "__Libraries\StellaOps.Scanner.VulnSurfaces.Tests\StellaOps.Scanner.VulnSurfaces.Tests.csproj", "{2669C700-5CFF-0186-F65E-8D26BE06E934}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.WebService", "StellaOps.Scanner.WebService\StellaOps.Scanner.WebService.csproj", "{0560BD84-CDBC-A79A-C665-55F6D62825EA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.WebService.Tests", "__Tests\StellaOps.Scanner.WebService.Tests\StellaOps.Scanner.WebService.Tests.csproj", "{783A67C9-3381-6E4C-3752-423F0FC6F6F9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Worker", "StellaOps.Scanner.Worker\StellaOps.Scanner.Worker.csproj", "{F890BD12-6CF5-4F80-9099-B7FE9A908432}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Worker.Tests", "__Tests\StellaOps.Scanner.Worker.Tests\StellaOps.Scanner.Worker.Tests.csproj", "{505C6840-5113-26EC-CEDB-D07EEABEF94B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Signals", "E:\dev\git.stella-ops.org\src\Signals\StellaOps.Signals\StellaOps.Signals.csproj", "{A79CBC0C-5313-4ECF-A24E-27CE236BCF2C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Signer.Core", "E:\dev\git.stella-ops.org\src\Signer\StellaOps.Signer\StellaOps.Signer.Core\StellaOps.Signer.Core.csproj", "{0AF13355-173C-3128-5AFC-D32E540DA3EF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.TestKit", "E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.TestKit\StellaOps.TestKit.csproj", "{AF043113-CCE3-59C1-DF71-9804155F26A8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Unknowns.Core", "E:\dev\git.stella-ops.org\src\Unknowns\__Libraries\StellaOps.Unknowns.Core\StellaOps.Unknowns.Core.csproj", "{15602821-2ABA-14BB-738D-1A53E1976E07}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.VersionComparison", "E:\dev\git.stella-ops.org\src\__Libraries\StellaOps.VersionComparison\StellaOps.VersionComparison.csproj", "{1D761F8B-921C-53BF-DCF5-5ABD329EEB0C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Zastava.Core", "E:\dev\git.stella-ops.org\src\Zastava\__Libraries\StellaOps.Zastava.Core\StellaOps.Zastava.Core.csproj", "{DA7634C2-9156-9B79-7A1D-90D8E605DC8A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Native.Library.Tests", "__Tests\StellaOps.Scanner.Analyzers.Native.Library.Tests\StellaOps.Scanner.Analyzers.Native.Library.Tests.csproj", "{5C4EF841-B039-4899-BF6F-32DC4FDB7AE5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Testing", "..\__Tests\__Libraries\StellaOps.Concelier.Testing\StellaOps.Concelier.Testing.csproj", "{188D45EF-BD46-40F6-BC4A-1B708E0C7169}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Common", "..\Concelier\__Libraries\StellaOps.Concelier.Connector.Common\StellaOps.Concelier.Connector.Common.csproj", "{4D73CAD5-0D7A-4FCA-AE0B-06D07BFA1851}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Normalization", "..\Concelier\__Libraries\StellaOps.Concelier.Normalization\StellaOps.Concelier.Normalization.csproj", "{4297C94F-8AB6-46BB-AAF0-27BE050F2FA7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Models", "..\Concelier\__Libraries\StellaOps.Concelier.Models\StellaOps.Concelier.Models.csproj", "{0EB3E7C7-4DBA-48E2-BC3C-E98F30799A0C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.RawModels", "..\Concelier\__Libraries\StellaOps.Concelier.RawModels\StellaOps.Concelier.RawModels.csproj", "{AB8B0F8E-D92D-410E-8EB7-A766522DF520}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Plugin", "..\__Libraries\StellaOps.Plugin\StellaOps.Plugin.csproj", "{46C131C6-64D9-4C72-9C11-AABCAD340DE3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.DependencyInjection", "..\__Libraries\StellaOps.DependencyInjection\StellaOps.DependencyInjection.csproj", "{E0016C1D-F312-4EB5-8EFE-96DE87D97B43}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Cryptography", "..\__Libraries\StellaOps.Cryptography\StellaOps.Cryptography.csproj", "{8B84E879-EB68-435A-8FAF-4539D8E2B359}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Core", "..\Concelier\__Libraries\StellaOps.Concelier.Core\StellaOps.Concelier.Core.csproj", "{EEB5F0FD-BCAC-47C6-8895-B414B8656070}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Ingestion.Telemetry", "..\__Libraries\StellaOps.Ingestion.Telemetry\StellaOps.Ingestion.Telemetry.csproj", "{A9AE973D-8734-4C6E-9DD2-552478F6D5D5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Provenance", "..\__Libraries\StellaOps.Provenance\StellaOps.Provenance.csproj", "{9C74824C-0CED-4976-A910-CBAE529223D9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Aoc", "..\Aoc\__Libraries\StellaOps.Aoc\StellaOps.Aoc.csproj", "{0608FA93-FE0B-410F-810E-C250CEB81AB9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Persistence", "..\Concelier\__Libraries\StellaOps.Concelier.Persistence\StellaOps.Concelier.Persistence.csproj", "{444A9AEC-4B40-4B6D-916A-15B9D195DF26}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Determinism.Abstractions", "..\__Libraries\StellaOps.Determinism.Abstractions\StellaOps.Determinism.Abstractions.csproj", "{3A24D8FE-4A6F-4E03-85C9-17E46B647516}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Infrastructure.Postgres", "..\__Libraries\StellaOps.Infrastructure.Postgres\StellaOps.Infrastructure.Postgres.csproj", "{C09B550A-314A-4719-88CF-DFBD549E0CA1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Infrastructure.EfCore", "..\__Libraries\StellaOps.Infrastructure.EfCore\StellaOps.Infrastructure.EfCore.csproj", "{6D3C9124-C29A-4BD9-B14D-B285E5F2481D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Interest", "..\Concelier\__Libraries\StellaOps.Concelier.Interest\StellaOps.Concelier.Interest.csproj", "{08377B69-0AB0-471E-A743-A9C92436DC51}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Cache.Valkey", "..\Concelier\__Libraries\StellaOps.Concelier.Cache.Valkey\StellaOps.Concelier.Cache.Valkey.csproj", "{1E4548D0-9272-41BD-9529-A40BE308A1BC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.SbomIntegration", "..\Concelier\__Libraries\StellaOps.Concelier.SbomIntegration\StellaOps.Concelier.SbomIntegration.csproj", "{4EC447F0-DE52-49E1-A798-4EA1CFF08036}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Messaging", "..\Router\__Libraries\StellaOps.Messaging\StellaOps.Messaging.csproj", "{3AC27AD7-CBAC-422A-818F-669D0B63983B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Merge", "..\Concelier\__Libraries\StellaOps.Concelier.Merge\StellaOps.Concelier.Merge.csproj", "{C3DDE3A6-217A-4DE6-B5E9-1E31AC4E0B35}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.ProofService", "..\Concelier\__Libraries\StellaOps.Concelier.ProofService\StellaOps.Concelier.ProofService.csproj", "{E9BE6A86-1AF1-4291-A42E-21BDD384CBDA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.SourceIntel", "..\Concelier\__Libraries\StellaOps.Concelier.SourceIntel\StellaOps.Concelier.SourceIntel.csproj", "{31544218-76AF-4ADA-B779-9C793E9686D8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Feedser.Core", "..\Feedser\StellaOps.Feedser.Core\StellaOps.Feedser.Core.csproj", "{CF2A7FD7-E5F7-4810-A5E3-0D40269F8E1B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Feedser.BinaryAnalysis", "..\Feedser\StellaOps.Feedser.BinaryAnalysis\StellaOps.Feedser.BinaryAnalysis.csproj", "{F19C3D33-FACE-4217-AC9B-519BE901CDF0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Attestor.ProofChain", "..\Attestor\__Libraries\StellaOps.Attestor.ProofChain\StellaOps.Attestor.ProofChain.csproj", "{8D384B62-F15F-4BDF-BE33-17BDE81B3599}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Canonical.Json", "..\__Libraries\StellaOps.Canonical.Json\StellaOps.Canonical.Json.csproj", "{3FDB9AC2-4CFC-48C7-99A5-8C24050886F7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Attestor.Envelope", "..\Attestor\StellaOps.Attestor.Envelope\StellaOps.Attestor.Envelope.csproj", "{CF3DD636-CCC5-448C-A083-F2961B2FB3F6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.ChangeTrace", "__Libraries\StellaOps.Scanner.ChangeTrace\StellaOps.Scanner.ChangeTrace.csproj", "{44A3DE13-CC1A-4331-8551-30F52E67510C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Provcache", "..\__Libraries\StellaOps.Provcache\StellaOps.Provcache.csproj", "{162D0F7E-3313-40B1-97AC-16198CB0F6BA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Provenance.Attestation", "..\Provenance\StellaOps.Provenance.Attestation\StellaOps.Provenance.Attestation.csproj", "{7492C8D3-B033-45F8-A826-560B925EAFD9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.VersionComparison", "..\__Libraries\StellaOps.VersionComparison\StellaOps.VersionComparison.csproj", "{41E9DD28-3F40-4288-B4CA-D2395BFA3B9E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Infrastructure.Postgres.Testing", "..\__Tests\__Libraries\StellaOps.Infrastructure.Postgres.Testing\StellaOps.Infrastructure.Postgres.Testing.csproj", "{C6087B8C-3C57-4593-A340-A4D7BDCD8259}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {22B129C7-C609-3B90-AD56-64C746A1505E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {22B129C7-C609-3B90-AD56-64C746A1505E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {22B129C7-C609-3B90-AD56-64C746A1505E}.Debug|x64.ActiveCfg = Debug|Any CPU + {22B129C7-C609-3B90-AD56-64C746A1505E}.Debug|x64.Build.0 = Debug|Any CPU + {22B129C7-C609-3B90-AD56-64C746A1505E}.Debug|x86.ActiveCfg = Debug|Any CPU + {22B129C7-C609-3B90-AD56-64C746A1505E}.Debug|x86.Build.0 = Debug|Any CPU + {22B129C7-C609-3B90-AD56-64C746A1505E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {22B129C7-C609-3B90-AD56-64C746A1505E}.Release|Any CPU.Build.0 = Release|Any CPU + {22B129C7-C609-3B90-AD56-64C746A1505E}.Release|x64.ActiveCfg = Release|Any CPU + {22B129C7-C609-3B90-AD56-64C746A1505E}.Release|x64.Build.0 = Release|Any CPU + {22B129C7-C609-3B90-AD56-64C746A1505E}.Release|x86.ActiveCfg = Release|Any CPU + {22B129C7-C609-3B90-AD56-64C746A1505E}.Release|x86.Build.0 = Release|Any CPU + {AD31623A-BC43-52C2-D906-AC1D8784A541}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AD31623A-BC43-52C2-D906-AC1D8784A541}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AD31623A-BC43-52C2-D906-AC1D8784A541}.Debug|x64.ActiveCfg = Debug|Any CPU + {AD31623A-BC43-52C2-D906-AC1D8784A541}.Debug|x64.Build.0 = Debug|Any CPU + {AD31623A-BC43-52C2-D906-AC1D8784A541}.Debug|x86.ActiveCfg = Debug|Any CPU + {AD31623A-BC43-52C2-D906-AC1D8784A541}.Debug|x86.Build.0 = Debug|Any CPU + {AD31623A-BC43-52C2-D906-AC1D8784A541}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AD31623A-BC43-52C2-D906-AC1D8784A541}.Release|Any CPU.Build.0 = Release|Any CPU + {AD31623A-BC43-52C2-D906-AC1D8784A541}.Release|x64.ActiveCfg = Release|Any CPU + {AD31623A-BC43-52C2-D906-AC1D8784A541}.Release|x64.Build.0 = Release|Any CPU + {AD31623A-BC43-52C2-D906-AC1D8784A541}.Release|x86.ActiveCfg = Release|Any CPU + {AD31623A-BC43-52C2-D906-AC1D8784A541}.Release|x86.Build.0 = Release|Any CPU + {776E2142-804F-03B9-C804-D061D64C6092}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {776E2142-804F-03B9-C804-D061D64C6092}.Debug|Any CPU.Build.0 = Debug|Any CPU + {776E2142-804F-03B9-C804-D061D64C6092}.Debug|x64.ActiveCfg = Debug|Any CPU + {776E2142-804F-03B9-C804-D061D64C6092}.Debug|x64.Build.0 = Debug|Any CPU + {776E2142-804F-03B9-C804-D061D64C6092}.Debug|x86.ActiveCfg = Debug|Any CPU + {776E2142-804F-03B9-C804-D061D64C6092}.Debug|x86.Build.0 = Debug|Any CPU + {776E2142-804F-03B9-C804-D061D64C6092}.Release|Any CPU.ActiveCfg = Release|Any CPU + {776E2142-804F-03B9-C804-D061D64C6092}.Release|Any CPU.Build.0 = Release|Any CPU + {776E2142-804F-03B9-C804-D061D64C6092}.Release|x64.ActiveCfg = Release|Any CPU + {776E2142-804F-03B9-C804-D061D64C6092}.Release|x64.Build.0 = Release|Any CPU + {776E2142-804F-03B9-C804-D061D64C6092}.Release|x86.ActiveCfg = Release|Any CPU + {776E2142-804F-03B9-C804-D061D64C6092}.Release|x86.Build.0 = Release|Any CPU + {5B4DF41E-C8CC-2606-FA2D-967118BD3C59}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5B4DF41E-C8CC-2606-FA2D-967118BD3C59}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5B4DF41E-C8CC-2606-FA2D-967118BD3C59}.Debug|x64.ActiveCfg = Debug|Any CPU + {5B4DF41E-C8CC-2606-FA2D-967118BD3C59}.Debug|x64.Build.0 = Debug|Any CPU + {5B4DF41E-C8CC-2606-FA2D-967118BD3C59}.Debug|x86.ActiveCfg = Debug|Any CPU + {5B4DF41E-C8CC-2606-FA2D-967118BD3C59}.Debug|x86.Build.0 = Debug|Any CPU + {5B4DF41E-C8CC-2606-FA2D-967118BD3C59}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5B4DF41E-C8CC-2606-FA2D-967118BD3C59}.Release|Any CPU.Build.0 = Release|Any CPU + {5B4DF41E-C8CC-2606-FA2D-967118BD3C59}.Release|x64.ActiveCfg = Release|Any CPU + {5B4DF41E-C8CC-2606-FA2D-967118BD3C59}.Release|x64.Build.0 = Release|Any CPU + {5B4DF41E-C8CC-2606-FA2D-967118BD3C59}.Release|x86.ActiveCfg = Release|Any CPU + {5B4DF41E-C8CC-2606-FA2D-967118BD3C59}.Release|x86.Build.0 = Release|Any CPU + {3D8C5A6C-462D-7487-5BD0-A3EF6B657EB6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3D8C5A6C-462D-7487-5BD0-A3EF6B657EB6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3D8C5A6C-462D-7487-5BD0-A3EF6B657EB6}.Debug|x64.ActiveCfg = Debug|Any CPU + {3D8C5A6C-462D-7487-5BD0-A3EF6B657EB6}.Debug|x64.Build.0 = Debug|Any CPU + {3D8C5A6C-462D-7487-5BD0-A3EF6B657EB6}.Debug|x86.ActiveCfg = Debug|Any CPU + {3D8C5A6C-462D-7487-5BD0-A3EF6B657EB6}.Debug|x86.Build.0 = Debug|Any CPU + {3D8C5A6C-462D-7487-5BD0-A3EF6B657EB6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3D8C5A6C-462D-7487-5BD0-A3EF6B657EB6}.Release|Any CPU.Build.0 = Release|Any CPU + {3D8C5A6C-462D-7487-5BD0-A3EF6B657EB6}.Release|x64.ActiveCfg = Release|Any CPU + {3D8C5A6C-462D-7487-5BD0-A3EF6B657EB6}.Release|x64.Build.0 = Release|Any CPU + {3D8C5A6C-462D-7487-5BD0-A3EF6B657EB6}.Release|x86.ActiveCfg = Release|Any CPU + {3D8C5A6C-462D-7487-5BD0-A3EF6B657EB6}.Release|x86.Build.0 = Release|Any CPU + {2609BC1A-6765-29BE-78CC-C0F1D2814F10}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2609BC1A-6765-29BE-78CC-C0F1D2814F10}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2609BC1A-6765-29BE-78CC-C0F1D2814F10}.Debug|x64.ActiveCfg = Debug|Any CPU + {2609BC1A-6765-29BE-78CC-C0F1D2814F10}.Debug|x64.Build.0 = Debug|Any CPU + {2609BC1A-6765-29BE-78CC-C0F1D2814F10}.Debug|x86.ActiveCfg = Debug|Any CPU + {2609BC1A-6765-29BE-78CC-C0F1D2814F10}.Debug|x86.Build.0 = Debug|Any CPU + {2609BC1A-6765-29BE-78CC-C0F1D2814F10}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2609BC1A-6765-29BE-78CC-C0F1D2814F10}.Release|Any CPU.Build.0 = Release|Any CPU + {2609BC1A-6765-29BE-78CC-C0F1D2814F10}.Release|x64.ActiveCfg = Release|Any CPU + {2609BC1A-6765-29BE-78CC-C0F1D2814F10}.Release|x64.Build.0 = Release|Any CPU + {2609BC1A-6765-29BE-78CC-C0F1D2814F10}.Release|x86.ActiveCfg = Release|Any CPU + {2609BC1A-6765-29BE-78CC-C0F1D2814F10}.Release|x86.Build.0 = Release|Any CPU + {C6822231-A4F4-9E69-6CE2-4FDB3E81C728}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C6822231-A4F4-9E69-6CE2-4FDB3E81C728}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C6822231-A4F4-9E69-6CE2-4FDB3E81C728}.Debug|x64.ActiveCfg = Debug|Any CPU + {C6822231-A4F4-9E69-6CE2-4FDB3E81C728}.Debug|x64.Build.0 = Debug|Any CPU + {C6822231-A4F4-9E69-6CE2-4FDB3E81C728}.Debug|x86.ActiveCfg = Debug|Any CPU + {C6822231-A4F4-9E69-6CE2-4FDB3E81C728}.Debug|x86.Build.0 = Debug|Any CPU + {C6822231-A4F4-9E69-6CE2-4FDB3E81C728}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C6822231-A4F4-9E69-6CE2-4FDB3E81C728}.Release|Any CPU.Build.0 = Release|Any CPU + {C6822231-A4F4-9E69-6CE2-4FDB3E81C728}.Release|x64.ActiveCfg = Release|Any CPU + {C6822231-A4F4-9E69-6CE2-4FDB3E81C728}.Release|x64.Build.0 = Release|Any CPU + {C6822231-A4F4-9E69-6CE2-4FDB3E81C728}.Release|x86.ActiveCfg = Release|Any CPU + {C6822231-A4F4-9E69-6CE2-4FDB3E81C728}.Release|x86.Build.0 = Release|Any CPU + {55D9B653-FB76-FCE8-1A3C-67B1BEDEC214}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {55D9B653-FB76-FCE8-1A3C-67B1BEDEC214}.Debug|Any CPU.Build.0 = Debug|Any CPU + {55D9B653-FB76-FCE8-1A3C-67B1BEDEC214}.Debug|x64.ActiveCfg = Debug|Any CPU + {55D9B653-FB76-FCE8-1A3C-67B1BEDEC214}.Debug|x64.Build.0 = Debug|Any CPU + {55D9B653-FB76-FCE8-1A3C-67B1BEDEC214}.Debug|x86.ActiveCfg = Debug|Any CPU + {55D9B653-FB76-FCE8-1A3C-67B1BEDEC214}.Debug|x86.Build.0 = Debug|Any CPU + {55D9B653-FB76-FCE8-1A3C-67B1BEDEC214}.Release|Any CPU.ActiveCfg = Release|Any CPU + {55D9B653-FB76-FCE8-1A3C-67B1BEDEC214}.Release|Any CPU.Build.0 = Release|Any CPU + {55D9B653-FB76-FCE8-1A3C-67B1BEDEC214}.Release|x64.ActiveCfg = Release|Any CPU + {55D9B653-FB76-FCE8-1A3C-67B1BEDEC214}.Release|x64.Build.0 = Release|Any CPU + {55D9B653-FB76-FCE8-1A3C-67B1BEDEC214}.Release|x86.ActiveCfg = Release|Any CPU + {55D9B653-FB76-FCE8-1A3C-67B1BEDEC214}.Release|x86.Build.0 = Release|Any CPU + {DE5BF139-1E5C-D6EA-4FAA-661EF353A194}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DE5BF139-1E5C-D6EA-4FAA-661EF353A194}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DE5BF139-1E5C-D6EA-4FAA-661EF353A194}.Debug|x64.ActiveCfg = Debug|Any CPU + {DE5BF139-1E5C-D6EA-4FAA-661EF353A194}.Debug|x64.Build.0 = Debug|Any CPU + {DE5BF139-1E5C-D6EA-4FAA-661EF353A194}.Debug|x86.ActiveCfg = Debug|Any CPU + {DE5BF139-1E5C-D6EA-4FAA-661EF353A194}.Debug|x86.Build.0 = Debug|Any CPU + {DE5BF139-1E5C-D6EA-4FAA-661EF353A194}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DE5BF139-1E5C-D6EA-4FAA-661EF353A194}.Release|Any CPU.Build.0 = Release|Any CPU + {DE5BF139-1E5C-D6EA-4FAA-661EF353A194}.Release|x64.ActiveCfg = Release|Any CPU + {DE5BF139-1E5C-D6EA-4FAA-661EF353A194}.Release|x64.Build.0 = Release|Any CPU + {DE5BF139-1E5C-D6EA-4FAA-661EF353A194}.Release|x86.ActiveCfg = Release|Any CPU + {DE5BF139-1E5C-D6EA-4FAA-661EF353A194}.Release|x86.Build.0 = Release|Any CPU + {335E62C0-9E69-A952-680B-753B1B17C6D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {335E62C0-9E69-A952-680B-753B1B17C6D0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {335E62C0-9E69-A952-680B-753B1B17C6D0}.Debug|x64.ActiveCfg = Debug|Any CPU + {335E62C0-9E69-A952-680B-753B1B17C6D0}.Debug|x64.Build.0 = Debug|Any CPU + {335E62C0-9E69-A952-680B-753B1B17C6D0}.Debug|x86.ActiveCfg = Debug|Any CPU + {335E62C0-9E69-A952-680B-753B1B17C6D0}.Debug|x86.Build.0 = Debug|Any CPU + {335E62C0-9E69-A952-680B-753B1B17C6D0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {335E62C0-9E69-A952-680B-753B1B17C6D0}.Release|Any CPU.Build.0 = Release|Any CPU + {335E62C0-9E69-A952-680B-753B1B17C6D0}.Release|x64.ActiveCfg = Release|Any CPU + {335E62C0-9E69-A952-680B-753B1B17C6D0}.Release|x64.Build.0 = Release|Any CPU + {335E62C0-9E69-A952-680B-753B1B17C6D0}.Release|x86.ActiveCfg = Release|Any CPU + {335E62C0-9E69-A952-680B-753B1B17C6D0}.Release|x86.Build.0 = Release|Any CPU + {ECA25786-A3A8-92C4-4AA3-D4A73C69FDCA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {ECA25786-A3A8-92C4-4AA3-D4A73C69FDCA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ECA25786-A3A8-92C4-4AA3-D4A73C69FDCA}.Debug|x64.ActiveCfg = Debug|Any CPU + {ECA25786-A3A8-92C4-4AA3-D4A73C69FDCA}.Debug|x64.Build.0 = Debug|Any CPU + {ECA25786-A3A8-92C4-4AA3-D4A73C69FDCA}.Debug|x86.ActiveCfg = Debug|Any CPU + {ECA25786-A3A8-92C4-4AA3-D4A73C69FDCA}.Debug|x86.Build.0 = Debug|Any CPU + {ECA25786-A3A8-92C4-4AA3-D4A73C69FDCA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {ECA25786-A3A8-92C4-4AA3-D4A73C69FDCA}.Release|Any CPU.Build.0 = Release|Any CPU + {ECA25786-A3A8-92C4-4AA3-D4A73C69FDCA}.Release|x64.ActiveCfg = Release|Any CPU + {ECA25786-A3A8-92C4-4AA3-D4A73C69FDCA}.Release|x64.Build.0 = Release|Any CPU + {ECA25786-A3A8-92C4-4AA3-D4A73C69FDCA}.Release|x86.ActiveCfg = Release|Any CPU + {ECA25786-A3A8-92C4-4AA3-D4A73C69FDCA}.Release|x86.Build.0 = Release|Any CPU + {5A6CD890-8142-F920-3734-D67CA3E65F61}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5A6CD890-8142-F920-3734-D67CA3E65F61}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5A6CD890-8142-F920-3734-D67CA3E65F61}.Debug|x64.ActiveCfg = Debug|Any CPU + {5A6CD890-8142-F920-3734-D67CA3E65F61}.Debug|x64.Build.0 = Debug|Any CPU + {5A6CD890-8142-F920-3734-D67CA3E65F61}.Debug|x86.ActiveCfg = Debug|Any CPU + {5A6CD890-8142-F920-3734-D67CA3E65F61}.Debug|x86.Build.0 = Debug|Any CPU + {5A6CD890-8142-F920-3734-D67CA3E65F61}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5A6CD890-8142-F920-3734-D67CA3E65F61}.Release|Any CPU.Build.0 = Release|Any CPU + {5A6CD890-8142-F920-3734-D67CA3E65F61}.Release|x64.ActiveCfg = Release|Any CPU + {5A6CD890-8142-F920-3734-D67CA3E65F61}.Release|x64.Build.0 = Release|Any CPU + {5A6CD890-8142-F920-3734-D67CA3E65F61}.Release|x86.ActiveCfg = Release|Any CPU + {5A6CD890-8142-F920-3734-D67CA3E65F61}.Release|x86.Build.0 = Release|Any CPU + {A260E14F-DBA4-862E-53CD-18D3B92ADA3D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A260E14F-DBA4-862E-53CD-18D3B92ADA3D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A260E14F-DBA4-862E-53CD-18D3B92ADA3D}.Debug|x64.ActiveCfg = Debug|Any CPU + {A260E14F-DBA4-862E-53CD-18D3B92ADA3D}.Debug|x64.Build.0 = Debug|Any CPU + {A260E14F-DBA4-862E-53CD-18D3B92ADA3D}.Debug|x86.ActiveCfg = Debug|Any CPU + {A260E14F-DBA4-862E-53CD-18D3B92ADA3D}.Debug|x86.Build.0 = Debug|Any CPU + {A260E14F-DBA4-862E-53CD-18D3B92ADA3D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A260E14F-DBA4-862E-53CD-18D3B92ADA3D}.Release|Any CPU.Build.0 = Release|Any CPU + {A260E14F-DBA4-862E-53CD-18D3B92ADA3D}.Release|x64.ActiveCfg = Release|Any CPU + {A260E14F-DBA4-862E-53CD-18D3B92ADA3D}.Release|x64.Build.0 = Release|Any CPU + {A260E14F-DBA4-862E-53CD-18D3B92ADA3D}.Release|x86.ActiveCfg = Release|Any CPU + {A260E14F-DBA4-862E-53CD-18D3B92ADA3D}.Release|x86.Build.0 = Release|Any CPU + {97F94029-5419-6187-5A63-5C8FD9232FAE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {97F94029-5419-6187-5A63-5C8FD9232FAE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {97F94029-5419-6187-5A63-5C8FD9232FAE}.Debug|x64.ActiveCfg = Debug|Any CPU + {97F94029-5419-6187-5A63-5C8FD9232FAE}.Debug|x64.Build.0 = Debug|Any CPU + {97F94029-5419-6187-5A63-5C8FD9232FAE}.Debug|x86.ActiveCfg = Debug|Any CPU + {97F94029-5419-6187-5A63-5C8FD9232FAE}.Debug|x86.Build.0 = Debug|Any CPU + {97F94029-5419-6187-5A63-5C8FD9232FAE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {97F94029-5419-6187-5A63-5C8FD9232FAE}.Release|Any CPU.Build.0 = Release|Any CPU + {97F94029-5419-6187-5A63-5C8FD9232FAE}.Release|x64.ActiveCfg = Release|Any CPU + {97F94029-5419-6187-5A63-5C8FD9232FAE}.Release|x64.Build.0 = Release|Any CPU + {97F94029-5419-6187-5A63-5C8FD9232FAE}.Release|x86.ActiveCfg = Release|Any CPU + {97F94029-5419-6187-5A63-5C8FD9232FAE}.Release|x86.Build.0 = Release|Any CPU + {03DF5914-2390-A82D-7464-642D0B95E068}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {03DF5914-2390-A82D-7464-642D0B95E068}.Debug|Any CPU.Build.0 = Debug|Any CPU + {03DF5914-2390-A82D-7464-642D0B95E068}.Debug|x64.ActiveCfg = Debug|Any CPU + {03DF5914-2390-A82D-7464-642D0B95E068}.Debug|x64.Build.0 = Debug|Any CPU + {03DF5914-2390-A82D-7464-642D0B95E068}.Debug|x86.ActiveCfg = Debug|Any CPU + {03DF5914-2390-A82D-7464-642D0B95E068}.Debug|x86.Build.0 = Debug|Any CPU + {03DF5914-2390-A82D-7464-642D0B95E068}.Release|Any CPU.ActiveCfg = Release|Any CPU + {03DF5914-2390-A82D-7464-642D0B95E068}.Release|Any CPU.Build.0 = Release|Any CPU + {03DF5914-2390-A82D-7464-642D0B95E068}.Release|x64.ActiveCfg = Release|Any CPU + {03DF5914-2390-A82D-7464-642D0B95E068}.Release|x64.Build.0 = Release|Any CPU + {03DF5914-2390-A82D-7464-642D0B95E068}.Release|x86.ActiveCfg = Release|Any CPU + {03DF5914-2390-A82D-7464-642D0B95E068}.Release|x86.Build.0 = Release|Any CPU + {CF633BDA-9F2E-D0C8-702F-BC9D27363B4B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CF633BDA-9F2E-D0C8-702F-BC9D27363B4B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CF633BDA-9F2E-D0C8-702F-BC9D27363B4B}.Debug|x64.ActiveCfg = Debug|Any CPU + {CF633BDA-9F2E-D0C8-702F-BC9D27363B4B}.Debug|x64.Build.0 = Debug|Any CPU + {CF633BDA-9F2E-D0C8-702F-BC9D27363B4B}.Debug|x86.ActiveCfg = Debug|Any CPU + {CF633BDA-9F2E-D0C8-702F-BC9D27363B4B}.Debug|x86.Build.0 = Debug|Any CPU + {CF633BDA-9F2E-D0C8-702F-BC9D27363B4B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CF633BDA-9F2E-D0C8-702F-BC9D27363B4B}.Release|Any CPU.Build.0 = Release|Any CPU + {CF633BDA-9F2E-D0C8-702F-BC9D27363B4B}.Release|x64.ActiveCfg = Release|Any CPU + {CF633BDA-9F2E-D0C8-702F-BC9D27363B4B}.Release|x64.Build.0 = Release|Any CPU + {CF633BDA-9F2E-D0C8-702F-BC9D27363B4B}.Release|x86.ActiveCfg = Release|Any CPU + {CF633BDA-9F2E-D0C8-702F-BC9D27363B4B}.Release|x86.Build.0 = Release|Any CPU + {73DE9C04-CEFE-53BA-A527-3A36D478DEFE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {73DE9C04-CEFE-53BA-A527-3A36D478DEFE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {73DE9C04-CEFE-53BA-A527-3A36D478DEFE}.Debug|x64.ActiveCfg = Debug|Any CPU + {73DE9C04-CEFE-53BA-A527-3A36D478DEFE}.Debug|x64.Build.0 = Debug|Any CPU + {73DE9C04-CEFE-53BA-A527-3A36D478DEFE}.Debug|x86.ActiveCfg = Debug|Any CPU + {73DE9C04-CEFE-53BA-A527-3A36D478DEFE}.Debug|x86.Build.0 = Debug|Any CPU + {73DE9C04-CEFE-53BA-A527-3A36D478DEFE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {73DE9C04-CEFE-53BA-A527-3A36D478DEFE}.Release|Any CPU.Build.0 = Release|Any CPU + {73DE9C04-CEFE-53BA-A527-3A36D478DEFE}.Release|x64.ActiveCfg = Release|Any CPU + {73DE9C04-CEFE-53BA-A527-3A36D478DEFE}.Release|x64.Build.0 = Release|Any CPU + {73DE9C04-CEFE-53BA-A527-3A36D478DEFE}.Release|x86.ActiveCfg = Release|Any CPU + {73DE9C04-CEFE-53BA-A527-3A36D478DEFE}.Release|x86.Build.0 = Release|Any CPU + {B7DC1B0A-EBD8-B1E8-28C8-9D5F19E118AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B7DC1B0A-EBD8-B1E8-28C8-9D5F19E118AD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B7DC1B0A-EBD8-B1E8-28C8-9D5F19E118AD}.Debug|x64.ActiveCfg = Debug|Any CPU + {B7DC1B0A-EBD8-B1E8-28C8-9D5F19E118AD}.Debug|x64.Build.0 = Debug|Any CPU + {B7DC1B0A-EBD8-B1E8-28C8-9D5F19E118AD}.Debug|x86.ActiveCfg = Debug|Any CPU + {B7DC1B0A-EBD8-B1E8-28C8-9D5F19E118AD}.Debug|x86.Build.0 = Debug|Any CPU + {B7DC1B0A-EBD8-B1E8-28C8-9D5F19E118AD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B7DC1B0A-EBD8-B1E8-28C8-9D5F19E118AD}.Release|Any CPU.Build.0 = Release|Any CPU + {B7DC1B0A-EBD8-B1E8-28C8-9D5F19E118AD}.Release|x64.ActiveCfg = Release|Any CPU + {B7DC1B0A-EBD8-B1E8-28C8-9D5F19E118AD}.Release|x64.Build.0 = Release|Any CPU + {B7DC1B0A-EBD8-B1E8-28C8-9D5F19E118AD}.Release|x86.ActiveCfg = Release|Any CPU + {B7DC1B0A-EBD8-B1E8-28C8-9D5F19E118AD}.Release|x86.Build.0 = Release|Any CPU + {0B56708E-B56C-E058-DE31-FCDFF30031F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0B56708E-B56C-E058-DE31-FCDFF30031F7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0B56708E-B56C-E058-DE31-FCDFF30031F7}.Debug|x64.ActiveCfg = Debug|Any CPU + {0B56708E-B56C-E058-DE31-FCDFF30031F7}.Debug|x64.Build.0 = Debug|Any CPU + {0B56708E-B56C-E058-DE31-FCDFF30031F7}.Debug|x86.ActiveCfg = Debug|Any CPU + {0B56708E-B56C-E058-DE31-FCDFF30031F7}.Debug|x86.Build.0 = Debug|Any CPU + {0B56708E-B56C-E058-DE31-FCDFF30031F7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0B56708E-B56C-E058-DE31-FCDFF30031F7}.Release|Any CPU.Build.0 = Release|Any CPU + {0B56708E-B56C-E058-DE31-FCDFF30031F7}.Release|x64.ActiveCfg = Release|Any CPU + {0B56708E-B56C-E058-DE31-FCDFF30031F7}.Release|x64.Build.0 = Release|Any CPU + {0B56708E-B56C-E058-DE31-FCDFF30031F7}.Release|x86.ActiveCfg = Release|Any CPU + {0B56708E-B56C-E058-DE31-FCDFF30031F7}.Release|x86.Build.0 = Release|Any CPU + {78FAD457-CE1B-D78E-A602-510EAD85E0AF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {78FAD457-CE1B-D78E-A602-510EAD85E0AF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {78FAD457-CE1B-D78E-A602-510EAD85E0AF}.Debug|x64.ActiveCfg = Debug|Any CPU + {78FAD457-CE1B-D78E-A602-510EAD85E0AF}.Debug|x64.Build.0 = Debug|Any CPU + {78FAD457-CE1B-D78E-A602-510EAD85E0AF}.Debug|x86.ActiveCfg = Debug|Any CPU + {78FAD457-CE1B-D78E-A602-510EAD85E0AF}.Debug|x86.Build.0 = Debug|Any CPU + {78FAD457-CE1B-D78E-A602-510EAD85E0AF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {78FAD457-CE1B-D78E-A602-510EAD85E0AF}.Release|Any CPU.Build.0 = Release|Any CPU + {78FAD457-CE1B-D78E-A602-510EAD85E0AF}.Release|x64.ActiveCfg = Release|Any CPU + {78FAD457-CE1B-D78E-A602-510EAD85E0AF}.Release|x64.Build.0 = Release|Any CPU + {78FAD457-CE1B-D78E-A602-510EAD85E0AF}.Release|x86.ActiveCfg = Release|Any CPU + {78FAD457-CE1B-D78E-A602-510EAD85E0AF}.Release|x86.Build.0 = Release|Any CPU + {AF9E7F02-25AD-3540-18D7-F6A4F8BA5A60}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AF9E7F02-25AD-3540-18D7-F6A4F8BA5A60}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AF9E7F02-25AD-3540-18D7-F6A4F8BA5A60}.Debug|x64.ActiveCfg = Debug|Any CPU + {AF9E7F02-25AD-3540-18D7-F6A4F8BA5A60}.Debug|x64.Build.0 = Debug|Any CPU + {AF9E7F02-25AD-3540-18D7-F6A4F8BA5A60}.Debug|x86.ActiveCfg = Debug|Any CPU + {AF9E7F02-25AD-3540-18D7-F6A4F8BA5A60}.Debug|x86.Build.0 = Debug|Any CPU + {AF9E7F02-25AD-3540-18D7-F6A4F8BA5A60}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AF9E7F02-25AD-3540-18D7-F6A4F8BA5A60}.Release|Any CPU.Build.0 = Release|Any CPU + {AF9E7F02-25AD-3540-18D7-F6A4F8BA5A60}.Release|x64.ActiveCfg = Release|Any CPU + {AF9E7F02-25AD-3540-18D7-F6A4F8BA5A60}.Release|x64.Build.0 = Release|Any CPU + {AF9E7F02-25AD-3540-18D7-F6A4F8BA5A60}.Release|x86.ActiveCfg = Release|Any CPU + {AF9E7F02-25AD-3540-18D7-F6A4F8BA5A60}.Release|x86.Build.0 = Release|Any CPU + {AB6AE2B6-8D6B-2D9F-2A88-7C596C59F4FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AB6AE2B6-8D6B-2D9F-2A88-7C596C59F4FC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AB6AE2B6-8D6B-2D9F-2A88-7C596C59F4FC}.Debug|x64.ActiveCfg = Debug|Any CPU + {AB6AE2B6-8D6B-2D9F-2A88-7C596C59F4FC}.Debug|x64.Build.0 = Debug|Any CPU + {AB6AE2B6-8D6B-2D9F-2A88-7C596C59F4FC}.Debug|x86.ActiveCfg = Debug|Any CPU + {AB6AE2B6-8D6B-2D9F-2A88-7C596C59F4FC}.Debug|x86.Build.0 = Debug|Any CPU + {AB6AE2B6-8D6B-2D9F-2A88-7C596C59F4FC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AB6AE2B6-8D6B-2D9F-2A88-7C596C59F4FC}.Release|Any CPU.Build.0 = Release|Any CPU + {AB6AE2B6-8D6B-2D9F-2A88-7C596C59F4FC}.Release|x64.ActiveCfg = Release|Any CPU + {AB6AE2B6-8D6B-2D9F-2A88-7C596C59F4FC}.Release|x64.Build.0 = Release|Any CPU + {AB6AE2B6-8D6B-2D9F-2A88-7C596C59F4FC}.Release|x86.ActiveCfg = Release|Any CPU + {AB6AE2B6-8D6B-2D9F-2A88-7C596C59F4FC}.Release|x86.Build.0 = Release|Any CPU + {375F5AD0-F7EE-1782-7B34-E181CDB61B9F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {375F5AD0-F7EE-1782-7B34-E181CDB61B9F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {375F5AD0-F7EE-1782-7B34-E181CDB61B9F}.Debug|x64.ActiveCfg = Debug|Any CPU + {375F5AD0-F7EE-1782-7B34-E181CDB61B9F}.Debug|x64.Build.0 = Debug|Any CPU + {375F5AD0-F7EE-1782-7B34-E181CDB61B9F}.Debug|x86.ActiveCfg = Debug|Any CPU + {375F5AD0-F7EE-1782-7B34-E181CDB61B9F}.Debug|x86.Build.0 = Debug|Any CPU + {375F5AD0-F7EE-1782-7B34-E181CDB61B9F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {375F5AD0-F7EE-1782-7B34-E181CDB61B9F}.Release|Any CPU.Build.0 = Release|Any CPU + {375F5AD0-F7EE-1782-7B34-E181CDB61B9F}.Release|x64.ActiveCfg = Release|Any CPU + {375F5AD0-F7EE-1782-7B34-E181CDB61B9F}.Release|x64.Build.0 = Release|Any CPU + {375F5AD0-F7EE-1782-7B34-E181CDB61B9F}.Release|x86.ActiveCfg = Release|Any CPU + {375F5AD0-F7EE-1782-7B34-E181CDB61B9F}.Release|x86.Build.0 = Release|Any CPU + {BA45605A-1CCE-6B0C-489D-C113915B243F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BA45605A-1CCE-6B0C-489D-C113915B243F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BA45605A-1CCE-6B0C-489D-C113915B243F}.Debug|x64.ActiveCfg = Debug|Any CPU + {BA45605A-1CCE-6B0C-489D-C113915B243F}.Debug|x64.Build.0 = Debug|Any CPU + {BA45605A-1CCE-6B0C-489D-C113915B243F}.Debug|x86.ActiveCfg = Debug|Any CPU + {BA45605A-1CCE-6B0C-489D-C113915B243F}.Debug|x86.Build.0 = Debug|Any CPU + {BA45605A-1CCE-6B0C-489D-C113915B243F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BA45605A-1CCE-6B0C-489D-C113915B243F}.Release|Any CPU.Build.0 = Release|Any CPU + {BA45605A-1CCE-6B0C-489D-C113915B243F}.Release|x64.ActiveCfg = Release|Any CPU + {BA45605A-1CCE-6B0C-489D-C113915B243F}.Release|x64.Build.0 = Release|Any CPU + {BA45605A-1CCE-6B0C-489D-C113915B243F}.Release|x86.ActiveCfg = Release|Any CPU + {BA45605A-1CCE-6B0C-489D-C113915B243F}.Release|x86.Build.0 = Release|Any CPU + {9D31FC8A-2A69-B78A-D3E5-4F867B16D971}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9D31FC8A-2A69-B78A-D3E5-4F867B16D971}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9D31FC8A-2A69-B78A-D3E5-4F867B16D971}.Debug|x64.ActiveCfg = Debug|Any CPU + {9D31FC8A-2A69-B78A-D3E5-4F867B16D971}.Debug|x64.Build.0 = Debug|Any CPU + {9D31FC8A-2A69-B78A-D3E5-4F867B16D971}.Debug|x86.ActiveCfg = Debug|Any CPU + {9D31FC8A-2A69-B78A-D3E5-4F867B16D971}.Debug|x86.Build.0 = Debug|Any CPU + {9D31FC8A-2A69-B78A-D3E5-4F867B16D971}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9D31FC8A-2A69-B78A-D3E5-4F867B16D971}.Release|Any CPU.Build.0 = Release|Any CPU + {9D31FC8A-2A69-B78A-D3E5-4F867B16D971}.Release|x64.ActiveCfg = Release|Any CPU + {9D31FC8A-2A69-B78A-D3E5-4F867B16D971}.Release|x64.Build.0 = Release|Any CPU + {9D31FC8A-2A69-B78A-D3E5-4F867B16D971}.Release|x86.ActiveCfg = Release|Any CPU + {9D31FC8A-2A69-B78A-D3E5-4F867B16D971}.Release|x86.Build.0 = Release|Any CPU + {92268008-FBB0-C7AD-ECC2-7B75BED9F5E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {92268008-FBB0-C7AD-ECC2-7B75BED9F5E1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {92268008-FBB0-C7AD-ECC2-7B75BED9F5E1}.Debug|x64.ActiveCfg = Debug|Any CPU + {92268008-FBB0-C7AD-ECC2-7B75BED9F5E1}.Debug|x64.Build.0 = Debug|Any CPU + {92268008-FBB0-C7AD-ECC2-7B75BED9F5E1}.Debug|x86.ActiveCfg = Debug|Any CPU + {92268008-FBB0-C7AD-ECC2-7B75BED9F5E1}.Debug|x86.Build.0 = Debug|Any CPU + {92268008-FBB0-C7AD-ECC2-7B75BED9F5E1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {92268008-FBB0-C7AD-ECC2-7B75BED9F5E1}.Release|Any CPU.Build.0 = Release|Any CPU + {92268008-FBB0-C7AD-ECC2-7B75BED9F5E1}.Release|x64.ActiveCfg = Release|Any CPU + {92268008-FBB0-C7AD-ECC2-7B75BED9F5E1}.Release|x64.Build.0 = Release|Any CPU + {92268008-FBB0-C7AD-ECC2-7B75BED9F5E1}.Release|x86.ActiveCfg = Release|Any CPU + {92268008-FBB0-C7AD-ECC2-7B75BED9F5E1}.Release|x86.Build.0 = Release|Any CPU + {8DCCAF70-D364-4C8B-4E90-AF65091DE0C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8DCCAF70-D364-4C8B-4E90-AF65091DE0C5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8DCCAF70-D364-4C8B-4E90-AF65091DE0C5}.Debug|x64.ActiveCfg = Debug|Any CPU + {8DCCAF70-D364-4C8B-4E90-AF65091DE0C5}.Debug|x64.Build.0 = Debug|Any CPU + {8DCCAF70-D364-4C8B-4E90-AF65091DE0C5}.Debug|x86.ActiveCfg = Debug|Any CPU + {8DCCAF70-D364-4C8B-4E90-AF65091DE0C5}.Debug|x86.Build.0 = Debug|Any CPU + {8DCCAF70-D364-4C8B-4E90-AF65091DE0C5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8DCCAF70-D364-4C8B-4E90-AF65091DE0C5}.Release|Any CPU.Build.0 = Release|Any CPU + {8DCCAF70-D364-4C8B-4E90-AF65091DE0C5}.Release|x64.ActiveCfg = Release|Any CPU + {8DCCAF70-D364-4C8B-4E90-AF65091DE0C5}.Release|x64.Build.0 = Release|Any CPU + {8DCCAF70-D364-4C8B-4E90-AF65091DE0C5}.Release|x86.ActiveCfg = Release|Any CPU + {8DCCAF70-D364-4C8B-4E90-AF65091DE0C5}.Release|x86.Build.0 = Release|Any CPU + {7828C164-DD01-2809-CCB3-364486834F60}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7828C164-DD01-2809-CCB3-364486834F60}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7828C164-DD01-2809-CCB3-364486834F60}.Debug|x64.ActiveCfg = Debug|Any CPU + {7828C164-DD01-2809-CCB3-364486834F60}.Debug|x64.Build.0 = Debug|Any CPU + {7828C164-DD01-2809-CCB3-364486834F60}.Debug|x86.ActiveCfg = Debug|Any CPU + {7828C164-DD01-2809-CCB3-364486834F60}.Debug|x86.Build.0 = Debug|Any CPU + {7828C164-DD01-2809-CCB3-364486834F60}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7828C164-DD01-2809-CCB3-364486834F60}.Release|Any CPU.Build.0 = Release|Any CPU + {7828C164-DD01-2809-CCB3-364486834F60}.Release|x64.ActiveCfg = Release|Any CPU + {7828C164-DD01-2809-CCB3-364486834F60}.Release|x64.Build.0 = Release|Any CPU + {7828C164-DD01-2809-CCB3-364486834F60}.Release|x86.ActiveCfg = Release|Any CPU + {7828C164-DD01-2809-CCB3-364486834F60}.Release|x86.Build.0 = Release|Any CPU + {DE95E7B2-0937-A980-441F-829E023BC43E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DE95E7B2-0937-A980-441F-829E023BC43E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DE95E7B2-0937-A980-441F-829E023BC43E}.Debug|x64.ActiveCfg = Debug|Any CPU + {DE95E7B2-0937-A980-441F-829E023BC43E}.Debug|x64.Build.0 = Debug|Any CPU + {DE95E7B2-0937-A980-441F-829E023BC43E}.Debug|x86.ActiveCfg = Debug|Any CPU + {DE95E7B2-0937-A980-441F-829E023BC43E}.Debug|x86.Build.0 = Debug|Any CPU + {DE95E7B2-0937-A980-441F-829E023BC43E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DE95E7B2-0937-A980-441F-829E023BC43E}.Release|Any CPU.Build.0 = Release|Any CPU + {DE95E7B2-0937-A980-441F-829E023BC43E}.Release|x64.ActiveCfg = Release|Any CPU + {DE95E7B2-0937-A980-441F-829E023BC43E}.Release|x64.Build.0 = Release|Any CPU + {DE95E7B2-0937-A980-441F-829E023BC43E}.Release|x86.ActiveCfg = Release|Any CPU + {DE95E7B2-0937-A980-441F-829E023BC43E}.Release|x86.Build.0 = Release|Any CPU + {91D69463-23E2-E2C7-AA7E-A78B13CED620}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {91D69463-23E2-E2C7-AA7E-A78B13CED620}.Debug|Any CPU.Build.0 = Debug|Any CPU + {91D69463-23E2-E2C7-AA7E-A78B13CED620}.Debug|x64.ActiveCfg = Debug|Any CPU + {91D69463-23E2-E2C7-AA7E-A78B13CED620}.Debug|x64.Build.0 = Debug|Any CPU + {91D69463-23E2-E2C7-AA7E-A78B13CED620}.Debug|x86.ActiveCfg = Debug|Any CPU + {91D69463-23E2-E2C7-AA7E-A78B13CED620}.Debug|x86.Build.0 = Debug|Any CPU + {91D69463-23E2-E2C7-AA7E-A78B13CED620}.Release|Any CPU.ActiveCfg = Release|Any CPU + {91D69463-23E2-E2C7-AA7E-A78B13CED620}.Release|Any CPU.Build.0 = Release|Any CPU + {91D69463-23E2-E2C7-AA7E-A78B13CED620}.Release|x64.ActiveCfg = Release|Any CPU + {91D69463-23E2-E2C7-AA7E-A78B13CED620}.Release|x64.Build.0 = Release|Any CPU + {91D69463-23E2-E2C7-AA7E-A78B13CED620}.Release|x86.ActiveCfg = Release|Any CPU + {91D69463-23E2-E2C7-AA7E-A78B13CED620}.Release|x86.Build.0 = Release|Any CPU + {34EFF636-81A7-8DF6-7CC9-4DA784BAC7F3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {34EFF636-81A7-8DF6-7CC9-4DA784BAC7F3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {34EFF636-81A7-8DF6-7CC9-4DA784BAC7F3}.Debug|x64.ActiveCfg = Debug|Any CPU + {34EFF636-81A7-8DF6-7CC9-4DA784BAC7F3}.Debug|x64.Build.0 = Debug|Any CPU + {34EFF636-81A7-8DF6-7CC9-4DA784BAC7F3}.Debug|x86.ActiveCfg = Debug|Any CPU + {34EFF636-81A7-8DF6-7CC9-4DA784BAC7F3}.Debug|x86.Build.0 = Debug|Any CPU + {34EFF636-81A7-8DF6-7CC9-4DA784BAC7F3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {34EFF636-81A7-8DF6-7CC9-4DA784BAC7F3}.Release|Any CPU.Build.0 = Release|Any CPU + {34EFF636-81A7-8DF6-7CC9-4DA784BAC7F3}.Release|x64.ActiveCfg = Release|Any CPU + {34EFF636-81A7-8DF6-7CC9-4DA784BAC7F3}.Release|x64.Build.0 = Release|Any CPU + {34EFF636-81A7-8DF6-7CC9-4DA784BAC7F3}.Release|x86.ActiveCfg = Release|Any CPU + {34EFF636-81A7-8DF6-7CC9-4DA784BAC7F3}.Release|x86.Build.0 = Release|Any CPU + {5DCF16A8-97C6-2CB4-6A63-0370239039EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5DCF16A8-97C6-2CB4-6A63-0370239039EB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5DCF16A8-97C6-2CB4-6A63-0370239039EB}.Debug|x64.ActiveCfg = Debug|Any CPU + {5DCF16A8-97C6-2CB4-6A63-0370239039EB}.Debug|x64.Build.0 = Debug|Any CPU + {5DCF16A8-97C6-2CB4-6A63-0370239039EB}.Debug|x86.ActiveCfg = Debug|Any CPU + {5DCF16A8-97C6-2CB4-6A63-0370239039EB}.Debug|x86.Build.0 = Debug|Any CPU + {5DCF16A8-97C6-2CB4-6A63-0370239039EB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5DCF16A8-97C6-2CB4-6A63-0370239039EB}.Release|Any CPU.Build.0 = Release|Any CPU + {5DCF16A8-97C6-2CB4-6A63-0370239039EB}.Release|x64.ActiveCfg = Release|Any CPU + {5DCF16A8-97C6-2CB4-6A63-0370239039EB}.Release|x64.Build.0 = Release|Any CPU + {5DCF16A8-97C6-2CB4-6A63-0370239039EB}.Release|x86.ActiveCfg = Release|Any CPU + {5DCF16A8-97C6-2CB4-6A63-0370239039EB}.Release|x86.Build.0 = Release|Any CPU + {EB093C48-CDAC-106B-1196-AE34809B34C0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EB093C48-CDAC-106B-1196-AE34809B34C0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EB093C48-CDAC-106B-1196-AE34809B34C0}.Debug|x64.ActiveCfg = Debug|Any CPU + {EB093C48-CDAC-106B-1196-AE34809B34C0}.Debug|x64.Build.0 = Debug|Any CPU + {EB093C48-CDAC-106B-1196-AE34809B34C0}.Debug|x86.ActiveCfg = Debug|Any CPU + {EB093C48-CDAC-106B-1196-AE34809B34C0}.Debug|x86.Build.0 = Debug|Any CPU + {EB093C48-CDAC-106B-1196-AE34809B34C0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EB093C48-CDAC-106B-1196-AE34809B34C0}.Release|Any CPU.Build.0 = Release|Any CPU + {EB093C48-CDAC-106B-1196-AE34809B34C0}.Release|x64.ActiveCfg = Release|Any CPU + {EB093C48-CDAC-106B-1196-AE34809B34C0}.Release|x64.Build.0 = Release|Any CPU + {EB093C48-CDAC-106B-1196-AE34809B34C0}.Release|x86.ActiveCfg = Release|Any CPU + {EB093C48-CDAC-106B-1196-AE34809B34C0}.Release|x86.Build.0 = Release|Any CPU + {92C62F7B-8028-6EE1-B71B-F45F459B8E97}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {92C62F7B-8028-6EE1-B71B-F45F459B8E97}.Debug|Any CPU.Build.0 = Debug|Any CPU + {92C62F7B-8028-6EE1-B71B-F45F459B8E97}.Debug|x64.ActiveCfg = Debug|Any CPU + {92C62F7B-8028-6EE1-B71B-F45F459B8E97}.Debug|x64.Build.0 = Debug|Any CPU + {92C62F7B-8028-6EE1-B71B-F45F459B8E97}.Debug|x86.ActiveCfg = Debug|Any CPU + {92C62F7B-8028-6EE1-B71B-F45F459B8E97}.Debug|x86.Build.0 = Debug|Any CPU + {92C62F7B-8028-6EE1-B71B-F45F459B8E97}.Release|Any CPU.ActiveCfg = Release|Any CPU + {92C62F7B-8028-6EE1-B71B-F45F459B8E97}.Release|Any CPU.Build.0 = Release|Any CPU + {92C62F7B-8028-6EE1-B71B-F45F459B8E97}.Release|x64.ActiveCfg = Release|Any CPU + {92C62F7B-8028-6EE1-B71B-F45F459B8E97}.Release|x64.Build.0 = Release|Any CPU + {92C62F7B-8028-6EE1-B71B-F45F459B8E97}.Release|x86.ActiveCfg = Release|Any CPU + {92C62F7B-8028-6EE1-B71B-F45F459B8E97}.Release|x86.Build.0 = Release|Any CPU + {F664A948-E352-5808-E780-77A03F19E93E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F664A948-E352-5808-E780-77A03F19E93E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F664A948-E352-5808-E780-77A03F19E93E}.Debug|x64.ActiveCfg = Debug|Any CPU + {F664A948-E352-5808-E780-77A03F19E93E}.Debug|x64.Build.0 = Debug|Any CPU + {F664A948-E352-5808-E780-77A03F19E93E}.Debug|x86.ActiveCfg = Debug|Any CPU + {F664A948-E352-5808-E780-77A03F19E93E}.Debug|x86.Build.0 = Debug|Any CPU + {F664A948-E352-5808-E780-77A03F19E93E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F664A948-E352-5808-E780-77A03F19E93E}.Release|Any CPU.Build.0 = Release|Any CPU + {F664A948-E352-5808-E780-77A03F19E93E}.Release|x64.ActiveCfg = Release|Any CPU + {F664A948-E352-5808-E780-77A03F19E93E}.Release|x64.Build.0 = Release|Any CPU + {F664A948-E352-5808-E780-77A03F19E93E}.Release|x86.ActiveCfg = Release|Any CPU + {F664A948-E352-5808-E780-77A03F19E93E}.Release|x86.Build.0 = Release|Any CPU + {FA83F778-5252-0B80-5555-E69F790322EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FA83F778-5252-0B80-5555-E69F790322EA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FA83F778-5252-0B80-5555-E69F790322EA}.Debug|x64.ActiveCfg = Debug|Any CPU + {FA83F778-5252-0B80-5555-E69F790322EA}.Debug|x64.Build.0 = Debug|Any CPU + {FA83F778-5252-0B80-5555-E69F790322EA}.Debug|x86.ActiveCfg = Debug|Any CPU + {FA83F778-5252-0B80-5555-E69F790322EA}.Debug|x86.Build.0 = Debug|Any CPU + {FA83F778-5252-0B80-5555-E69F790322EA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FA83F778-5252-0B80-5555-E69F790322EA}.Release|Any CPU.Build.0 = Release|Any CPU + {FA83F778-5252-0B80-5555-E69F790322EA}.Release|x64.ActiveCfg = Release|Any CPU + {FA83F778-5252-0B80-5555-E69F790322EA}.Release|x64.Build.0 = Release|Any CPU + {FA83F778-5252-0B80-5555-E69F790322EA}.Release|x86.ActiveCfg = Release|Any CPU + {FA83F778-5252-0B80-5555-E69F790322EA}.Release|x86.Build.0 = Release|Any CPU + {F3A27846-6DE0-3448-222C-25A273E86B2E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F3A27846-6DE0-3448-222C-25A273E86B2E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F3A27846-6DE0-3448-222C-25A273E86B2E}.Debug|x64.ActiveCfg = Debug|Any CPU + {F3A27846-6DE0-3448-222C-25A273E86B2E}.Debug|x64.Build.0 = Debug|Any CPU + {F3A27846-6DE0-3448-222C-25A273E86B2E}.Debug|x86.ActiveCfg = Debug|Any CPU + {F3A27846-6DE0-3448-222C-25A273E86B2E}.Debug|x86.Build.0 = Debug|Any CPU + {F3A27846-6DE0-3448-222C-25A273E86B2E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F3A27846-6DE0-3448-222C-25A273E86B2E}.Release|Any CPU.Build.0 = Release|Any CPU + {F3A27846-6DE0-3448-222C-25A273E86B2E}.Release|x64.ActiveCfg = Release|Any CPU + {F3A27846-6DE0-3448-222C-25A273E86B2E}.Release|x64.Build.0 = Release|Any CPU + {F3A27846-6DE0-3448-222C-25A273E86B2E}.Release|x86.ActiveCfg = Release|Any CPU + {F3A27846-6DE0-3448-222C-25A273E86B2E}.Release|x86.Build.0 = Release|Any CPU + {166F4DEC-9886-92D5-6496-085664E9F08F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {166F4DEC-9886-92D5-6496-085664E9F08F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {166F4DEC-9886-92D5-6496-085664E9F08F}.Debug|x64.ActiveCfg = Debug|Any CPU + {166F4DEC-9886-92D5-6496-085664E9F08F}.Debug|x64.Build.0 = Debug|Any CPU + {166F4DEC-9886-92D5-6496-085664E9F08F}.Debug|x86.ActiveCfg = Debug|Any CPU + {166F4DEC-9886-92D5-6496-085664E9F08F}.Debug|x86.Build.0 = Debug|Any CPU + {166F4DEC-9886-92D5-6496-085664E9F08F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {166F4DEC-9886-92D5-6496-085664E9F08F}.Release|Any CPU.Build.0 = Release|Any CPU + {166F4DEC-9886-92D5-6496-085664E9F08F}.Release|x64.ActiveCfg = Release|Any CPU + {166F4DEC-9886-92D5-6496-085664E9F08F}.Release|x64.Build.0 = Release|Any CPU + {166F4DEC-9886-92D5-6496-085664E9F08F}.Release|x86.ActiveCfg = Release|Any CPU + {166F4DEC-9886-92D5-6496-085664E9F08F}.Release|x86.Build.0 = Release|Any CPU + {C53E0895-879A-D9E6-0A43-24AD17A2F270}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C53E0895-879A-D9E6-0A43-24AD17A2F270}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C53E0895-879A-D9E6-0A43-24AD17A2F270}.Debug|x64.ActiveCfg = Debug|Any CPU + {C53E0895-879A-D9E6-0A43-24AD17A2F270}.Debug|x64.Build.0 = Debug|Any CPU + {C53E0895-879A-D9E6-0A43-24AD17A2F270}.Debug|x86.ActiveCfg = Debug|Any CPU + {C53E0895-879A-D9E6-0A43-24AD17A2F270}.Debug|x86.Build.0 = Debug|Any CPU + {C53E0895-879A-D9E6-0A43-24AD17A2F270}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C53E0895-879A-D9E6-0A43-24AD17A2F270}.Release|Any CPU.Build.0 = Release|Any CPU + {C53E0895-879A-D9E6-0A43-24AD17A2F270}.Release|x64.ActiveCfg = Release|Any CPU + {C53E0895-879A-D9E6-0A43-24AD17A2F270}.Release|x64.Build.0 = Release|Any CPU + {C53E0895-879A-D9E6-0A43-24AD17A2F270}.Release|x86.ActiveCfg = Release|Any CPU + {C53E0895-879A-D9E6-0A43-24AD17A2F270}.Release|x86.Build.0 = Release|Any CPU + {246FCC7C-1437-742D-BAE5-E77A24164F08}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {246FCC7C-1437-742D-BAE5-E77A24164F08}.Debug|Any CPU.Build.0 = Debug|Any CPU + {246FCC7C-1437-742D-BAE5-E77A24164F08}.Debug|x64.ActiveCfg = Debug|Any CPU + {246FCC7C-1437-742D-BAE5-E77A24164F08}.Debug|x64.Build.0 = Debug|Any CPU + {246FCC7C-1437-742D-BAE5-E77A24164F08}.Debug|x86.ActiveCfg = Debug|Any CPU + {246FCC7C-1437-742D-BAE5-E77A24164F08}.Debug|x86.Build.0 = Debug|Any CPU + {246FCC7C-1437-742D-BAE5-E77A24164F08}.Release|Any CPU.ActiveCfg = Release|Any CPU + {246FCC7C-1437-742D-BAE5-E77A24164F08}.Release|Any CPU.Build.0 = Release|Any CPU + {246FCC7C-1437-742D-BAE5-E77A24164F08}.Release|x64.ActiveCfg = Release|Any CPU + {246FCC7C-1437-742D-BAE5-E77A24164F08}.Release|x64.Build.0 = Release|Any CPU + {246FCC7C-1437-742D-BAE5-E77A24164F08}.Release|x86.ActiveCfg = Release|Any CPU + {246FCC7C-1437-742D-BAE5-E77A24164F08}.Release|x86.Build.0 = Release|Any CPU + {0AED303F-69E6-238F-EF80-81985080EDB7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0AED303F-69E6-238F-EF80-81985080EDB7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0AED303F-69E6-238F-EF80-81985080EDB7}.Debug|x64.ActiveCfg = Debug|Any CPU + {0AED303F-69E6-238F-EF80-81985080EDB7}.Debug|x64.Build.0 = Debug|Any CPU + {0AED303F-69E6-238F-EF80-81985080EDB7}.Debug|x86.ActiveCfg = Debug|Any CPU + {0AED303F-69E6-238F-EF80-81985080EDB7}.Debug|x86.Build.0 = Debug|Any CPU + {0AED303F-69E6-238F-EF80-81985080EDB7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0AED303F-69E6-238F-EF80-81985080EDB7}.Release|Any CPU.Build.0 = Release|Any CPU + {0AED303F-69E6-238F-EF80-81985080EDB7}.Release|x64.ActiveCfg = Release|Any CPU + {0AED303F-69E6-238F-EF80-81985080EDB7}.Release|x64.Build.0 = Release|Any CPU + {0AED303F-69E6-238F-EF80-81985080EDB7}.Release|x86.ActiveCfg = Release|Any CPU + {0AED303F-69E6-238F-EF80-81985080EDB7}.Release|x86.Build.0 = Release|Any CPU + {2904D288-CE64-A565-2C46-C2E85A96A1EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2904D288-CE64-A565-2C46-C2E85A96A1EE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2904D288-CE64-A565-2C46-C2E85A96A1EE}.Debug|x64.ActiveCfg = Debug|Any CPU + {2904D288-CE64-A565-2C46-C2E85A96A1EE}.Debug|x64.Build.0 = Debug|Any CPU + {2904D288-CE64-A565-2C46-C2E85A96A1EE}.Debug|x86.ActiveCfg = Debug|Any CPU + {2904D288-CE64-A565-2C46-C2E85A96A1EE}.Debug|x86.Build.0 = Debug|Any CPU + {2904D288-CE64-A565-2C46-C2E85A96A1EE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2904D288-CE64-A565-2C46-C2E85A96A1EE}.Release|Any CPU.Build.0 = Release|Any CPU + {2904D288-CE64-A565-2C46-C2E85A96A1EE}.Release|x64.ActiveCfg = Release|Any CPU + {2904D288-CE64-A565-2C46-C2E85A96A1EE}.Release|x64.Build.0 = Release|Any CPU + {2904D288-CE64-A565-2C46-C2E85A96A1EE}.Release|x86.ActiveCfg = Release|Any CPU + {2904D288-CE64-A565-2C46-C2E85A96A1EE}.Release|x86.Build.0 = Release|Any CPU + {A6667CC3-B77F-023E-3A67-05F99E9FF46A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A6667CC3-B77F-023E-3A67-05F99E9FF46A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A6667CC3-B77F-023E-3A67-05F99E9FF46A}.Debug|x64.ActiveCfg = Debug|Any CPU + {A6667CC3-B77F-023E-3A67-05F99E9FF46A}.Debug|x64.Build.0 = Debug|Any CPU + {A6667CC3-B77F-023E-3A67-05F99E9FF46A}.Debug|x86.ActiveCfg = Debug|Any CPU + {A6667CC3-B77F-023E-3A67-05F99E9FF46A}.Debug|x86.Build.0 = Debug|Any CPU + {A6667CC3-B77F-023E-3A67-05F99E9FF46A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A6667CC3-B77F-023E-3A67-05F99E9FF46A}.Release|Any CPU.Build.0 = Release|Any CPU + {A6667CC3-B77F-023E-3A67-05F99E9FF46A}.Release|x64.ActiveCfg = Release|Any CPU + {A6667CC3-B77F-023E-3A67-05F99E9FF46A}.Release|x64.Build.0 = Release|Any CPU + {A6667CC3-B77F-023E-3A67-05F99E9FF46A}.Release|x86.ActiveCfg = Release|Any CPU + {A6667CC3-B77F-023E-3A67-05F99E9FF46A}.Release|x86.Build.0 = Release|Any CPU + {A26E2816-F787-F76B-1D6C-E086DD3E19CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A26E2816-F787-F76B-1D6C-E086DD3E19CE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A26E2816-F787-F76B-1D6C-E086DD3E19CE}.Debug|x64.ActiveCfg = Debug|Any CPU + {A26E2816-F787-F76B-1D6C-E086DD3E19CE}.Debug|x64.Build.0 = Debug|Any CPU + {A26E2816-F787-F76B-1D6C-E086DD3E19CE}.Debug|x86.ActiveCfg = Debug|Any CPU + {A26E2816-F787-F76B-1D6C-E086DD3E19CE}.Debug|x86.Build.0 = Debug|Any CPU + {A26E2816-F787-F76B-1D6C-E086DD3E19CE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A26E2816-F787-F76B-1D6C-E086DD3E19CE}.Release|Any CPU.Build.0 = Release|Any CPU + {A26E2816-F787-F76B-1D6C-E086DD3E19CE}.Release|x64.ActiveCfg = Release|Any CPU + {A26E2816-F787-F76B-1D6C-E086DD3E19CE}.Release|x64.Build.0 = Release|Any CPU + {A26E2816-F787-F76B-1D6C-E086DD3E19CE}.Release|x86.ActiveCfg = Release|Any CPU + {A26E2816-F787-F76B-1D6C-E086DD3E19CE}.Release|x86.Build.0 = Release|Any CPU + {B3DEC619-67AC-1B5A-4F3E-A1F24C3F6877}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B3DEC619-67AC-1B5A-4F3E-A1F24C3F6877}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B3DEC619-67AC-1B5A-4F3E-A1F24C3F6877}.Debug|x64.ActiveCfg = Debug|Any CPU + {B3DEC619-67AC-1B5A-4F3E-A1F24C3F6877}.Debug|x64.Build.0 = Debug|Any CPU + {B3DEC619-67AC-1B5A-4F3E-A1F24C3F6877}.Debug|x86.ActiveCfg = Debug|Any CPU + {B3DEC619-67AC-1B5A-4F3E-A1F24C3F6877}.Debug|x86.Build.0 = Debug|Any CPU + {B3DEC619-67AC-1B5A-4F3E-A1F24C3F6877}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B3DEC619-67AC-1B5A-4F3E-A1F24C3F6877}.Release|Any CPU.Build.0 = Release|Any CPU + {B3DEC619-67AC-1B5A-4F3E-A1F24C3F6877}.Release|x64.ActiveCfg = Release|Any CPU + {B3DEC619-67AC-1B5A-4F3E-A1F24C3F6877}.Release|x64.Build.0 = Release|Any CPU + {B3DEC619-67AC-1B5A-4F3E-A1F24C3F6877}.Release|x86.ActiveCfg = Release|Any CPU + {B3DEC619-67AC-1B5A-4F3E-A1F24C3F6877}.Release|x86.Build.0 = Release|Any CPU + {90DB65B4-8F6E-FB8E-0281-505AD8BC6BA6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {90DB65B4-8F6E-FB8E-0281-505AD8BC6BA6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {90DB65B4-8F6E-FB8E-0281-505AD8BC6BA6}.Debug|x64.ActiveCfg = Debug|Any CPU + {90DB65B4-8F6E-FB8E-0281-505AD8BC6BA6}.Debug|x64.Build.0 = Debug|Any CPU + {90DB65B4-8F6E-FB8E-0281-505AD8BC6BA6}.Debug|x86.ActiveCfg = Debug|Any CPU + {90DB65B4-8F6E-FB8E-0281-505AD8BC6BA6}.Debug|x86.Build.0 = Debug|Any CPU + {90DB65B4-8F6E-FB8E-0281-505AD8BC6BA6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {90DB65B4-8F6E-FB8E-0281-505AD8BC6BA6}.Release|Any CPU.Build.0 = Release|Any CPU + {90DB65B4-8F6E-FB8E-0281-505AD8BC6BA6}.Release|x64.ActiveCfg = Release|Any CPU + {90DB65B4-8F6E-FB8E-0281-505AD8BC6BA6}.Release|x64.Build.0 = Release|Any CPU + {90DB65B4-8F6E-FB8E-0281-505AD8BC6BA6}.Release|x86.ActiveCfg = Release|Any CPU + {90DB65B4-8F6E-FB8E-0281-505AD8BC6BA6}.Release|x86.Build.0 = Release|Any CPU + {059FBB86-DEE6-8207-3F23-2A1A3EC00DEA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {059FBB86-DEE6-8207-3F23-2A1A3EC00DEA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {059FBB86-DEE6-8207-3F23-2A1A3EC00DEA}.Debug|x64.ActiveCfg = Debug|Any CPU + {059FBB86-DEE6-8207-3F23-2A1A3EC00DEA}.Debug|x64.Build.0 = Debug|Any CPU + {059FBB86-DEE6-8207-3F23-2A1A3EC00DEA}.Debug|x86.ActiveCfg = Debug|Any CPU + {059FBB86-DEE6-8207-3F23-2A1A3EC00DEA}.Debug|x86.Build.0 = Debug|Any CPU + {059FBB86-DEE6-8207-3F23-2A1A3EC00DEA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {059FBB86-DEE6-8207-3F23-2A1A3EC00DEA}.Release|Any CPU.Build.0 = Release|Any CPU + {059FBB86-DEE6-8207-3F23-2A1A3EC00DEA}.Release|x64.ActiveCfg = Release|Any CPU + {059FBB86-DEE6-8207-3F23-2A1A3EC00DEA}.Release|x64.Build.0 = Release|Any CPU + {059FBB86-DEE6-8207-3F23-2A1A3EC00DEA}.Release|x86.ActiveCfg = Release|Any CPU + {059FBB86-DEE6-8207-3F23-2A1A3EC00DEA}.Release|x86.Build.0 = Release|Any CPU + {8BBA3159-C4CC-F685-A28C-7FE6CBD3D2A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8BBA3159-C4CC-F685-A28C-7FE6CBD3D2A1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8BBA3159-C4CC-F685-A28C-7FE6CBD3D2A1}.Debug|x64.ActiveCfg = Debug|Any CPU + {8BBA3159-C4CC-F685-A28C-7FE6CBD3D2A1}.Debug|x64.Build.0 = Debug|Any CPU + {8BBA3159-C4CC-F685-A28C-7FE6CBD3D2A1}.Debug|x86.ActiveCfg = Debug|Any CPU + {8BBA3159-C4CC-F685-A28C-7FE6CBD3D2A1}.Debug|x86.Build.0 = Debug|Any CPU + {8BBA3159-C4CC-F685-A28C-7FE6CBD3D2A1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8BBA3159-C4CC-F685-A28C-7FE6CBD3D2A1}.Release|Any CPU.Build.0 = Release|Any CPU + {8BBA3159-C4CC-F685-A28C-7FE6CBD3D2A1}.Release|x64.ActiveCfg = Release|Any CPU + {8BBA3159-C4CC-F685-A28C-7FE6CBD3D2A1}.Release|x64.Build.0 = Release|Any CPU + {8BBA3159-C4CC-F685-A28C-7FE6CBD3D2A1}.Release|x86.ActiveCfg = Release|Any CPU + {8BBA3159-C4CC-F685-A28C-7FE6CBD3D2A1}.Release|x86.Build.0 = Release|Any CPU + {EA0974E3-CD2B-5792-EF1E-9B5B7CCBDF00}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EA0974E3-CD2B-5792-EF1E-9B5B7CCBDF00}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EA0974E3-CD2B-5792-EF1E-9B5B7CCBDF00}.Debug|x64.ActiveCfg = Debug|Any CPU + {EA0974E3-CD2B-5792-EF1E-9B5B7CCBDF00}.Debug|x64.Build.0 = Debug|Any CPU + {EA0974E3-CD2B-5792-EF1E-9B5B7CCBDF00}.Debug|x86.ActiveCfg = Debug|Any CPU + {EA0974E3-CD2B-5792-EF1E-9B5B7CCBDF00}.Debug|x86.Build.0 = Debug|Any CPU + {EA0974E3-CD2B-5792-EF1E-9B5B7CCBDF00}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EA0974E3-CD2B-5792-EF1E-9B5B7CCBDF00}.Release|Any CPU.Build.0 = Release|Any CPU + {EA0974E3-CD2B-5792-EF1E-9B5B7CCBDF00}.Release|x64.ActiveCfg = Release|Any CPU + {EA0974E3-CD2B-5792-EF1E-9B5B7CCBDF00}.Release|x64.Build.0 = Release|Any CPU + {EA0974E3-CD2B-5792-EF1E-9B5B7CCBDF00}.Release|x86.ActiveCfg = Release|Any CPU + {EA0974E3-CD2B-5792-EF1E-9B5B7CCBDF00}.Release|x86.Build.0 = Release|Any CPU + {632A1F0D-1BA5-C84B-B716-2BE638A92780}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {632A1F0D-1BA5-C84B-B716-2BE638A92780}.Debug|Any CPU.Build.0 = Debug|Any CPU + {632A1F0D-1BA5-C84B-B716-2BE638A92780}.Debug|x64.ActiveCfg = Debug|Any CPU + {632A1F0D-1BA5-C84B-B716-2BE638A92780}.Debug|x64.Build.0 = Debug|Any CPU + {632A1F0D-1BA5-C84B-B716-2BE638A92780}.Debug|x86.ActiveCfg = Debug|Any CPU + {632A1F0D-1BA5-C84B-B716-2BE638A92780}.Debug|x86.Build.0 = Debug|Any CPU + {632A1F0D-1BA5-C84B-B716-2BE638A92780}.Release|Any CPU.ActiveCfg = Release|Any CPU + {632A1F0D-1BA5-C84B-B716-2BE638A92780}.Release|Any CPU.Build.0 = Release|Any CPU + {632A1F0D-1BA5-C84B-B716-2BE638A92780}.Release|x64.ActiveCfg = Release|Any CPU + {632A1F0D-1BA5-C84B-B716-2BE638A92780}.Release|x64.Build.0 = Release|Any CPU + {632A1F0D-1BA5-C84B-B716-2BE638A92780}.Release|x86.ActiveCfg = Release|Any CPU + {632A1F0D-1BA5-C84B-B716-2BE638A92780}.Release|x86.Build.0 = Release|Any CPU + {9DE7852B-7E2D-257E-B0F1-45D2687854ED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9DE7852B-7E2D-257E-B0F1-45D2687854ED}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9DE7852B-7E2D-257E-B0F1-45D2687854ED}.Debug|x64.ActiveCfg = Debug|Any CPU + {9DE7852B-7E2D-257E-B0F1-45D2687854ED}.Debug|x64.Build.0 = Debug|Any CPU + {9DE7852B-7E2D-257E-B0F1-45D2687854ED}.Debug|x86.ActiveCfg = Debug|Any CPU + {9DE7852B-7E2D-257E-B0F1-45D2687854ED}.Debug|x86.Build.0 = Debug|Any CPU + {9DE7852B-7E2D-257E-B0F1-45D2687854ED}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9DE7852B-7E2D-257E-B0F1-45D2687854ED}.Release|Any CPU.Build.0 = Release|Any CPU + {9DE7852B-7E2D-257E-B0F1-45D2687854ED}.Release|x64.ActiveCfg = Release|Any CPU + {9DE7852B-7E2D-257E-B0F1-45D2687854ED}.Release|x64.Build.0 = Release|Any CPU + {9DE7852B-7E2D-257E-B0F1-45D2687854ED}.Release|x86.ActiveCfg = Release|Any CPU + {9DE7852B-7E2D-257E-B0F1-45D2687854ED}.Release|x86.Build.0 = Release|Any CPU + {DC2AFC89-C3C8-4E9B-13A7-027EB6386EFA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DC2AFC89-C3C8-4E9B-13A7-027EB6386EFA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DC2AFC89-C3C8-4E9B-13A7-027EB6386EFA}.Debug|x64.ActiveCfg = Debug|Any CPU + {DC2AFC89-C3C8-4E9B-13A7-027EB6386EFA}.Debug|x64.Build.0 = Debug|Any CPU + {DC2AFC89-C3C8-4E9B-13A7-027EB6386EFA}.Debug|x86.ActiveCfg = Debug|Any CPU + {DC2AFC89-C3C8-4E9B-13A7-027EB6386EFA}.Debug|x86.Build.0 = Debug|Any CPU + {DC2AFC89-C3C8-4E9B-13A7-027EB6386EFA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DC2AFC89-C3C8-4E9B-13A7-027EB6386EFA}.Release|Any CPU.Build.0 = Release|Any CPU + {DC2AFC89-C3C8-4E9B-13A7-027EB6386EFA}.Release|x64.ActiveCfg = Release|Any CPU + {DC2AFC89-C3C8-4E9B-13A7-027EB6386EFA}.Release|x64.Build.0 = Release|Any CPU + {DC2AFC89-C3C8-4E9B-13A7-027EB6386EFA}.Release|x86.ActiveCfg = Release|Any CPU + {DC2AFC89-C3C8-4E9B-13A7-027EB6386EFA}.Release|x86.Build.0 = Release|Any CPU + {9151601C-8784-01A6-C2E7-A5C0FAAB0AEF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9151601C-8784-01A6-C2E7-A5C0FAAB0AEF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9151601C-8784-01A6-C2E7-A5C0FAAB0AEF}.Debug|x64.ActiveCfg = Debug|Any CPU + {9151601C-8784-01A6-C2E7-A5C0FAAB0AEF}.Debug|x64.Build.0 = Debug|Any CPU + {9151601C-8784-01A6-C2E7-A5C0FAAB0AEF}.Debug|x86.ActiveCfg = Debug|Any CPU + {9151601C-8784-01A6-C2E7-A5C0FAAB0AEF}.Debug|x86.Build.0 = Debug|Any CPU + {9151601C-8784-01A6-C2E7-A5C0FAAB0AEF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9151601C-8784-01A6-C2E7-A5C0FAAB0AEF}.Release|Any CPU.Build.0 = Release|Any CPU + {9151601C-8784-01A6-C2E7-A5C0FAAB0AEF}.Release|x64.ActiveCfg = Release|Any CPU + {9151601C-8784-01A6-C2E7-A5C0FAAB0AEF}.Release|x64.Build.0 = Release|Any CPU + {9151601C-8784-01A6-C2E7-A5C0FAAB0AEF}.Release|x86.ActiveCfg = Release|Any CPU + {9151601C-8784-01A6-C2E7-A5C0FAAB0AEF}.Release|x86.Build.0 = Release|Any CPU + {CB296A20-2732-77C1-7F23-27D5BAEDD0C7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CB296A20-2732-77C1-7F23-27D5BAEDD0C7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CB296A20-2732-77C1-7F23-27D5BAEDD0C7}.Debug|x64.ActiveCfg = Debug|Any CPU + {CB296A20-2732-77C1-7F23-27D5BAEDD0C7}.Debug|x64.Build.0 = Debug|Any CPU + {CB296A20-2732-77C1-7F23-27D5BAEDD0C7}.Debug|x86.ActiveCfg = Debug|Any CPU + {CB296A20-2732-77C1-7F23-27D5BAEDD0C7}.Debug|x86.Build.0 = Debug|Any CPU + {CB296A20-2732-77C1-7F23-27D5BAEDD0C7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CB296A20-2732-77C1-7F23-27D5BAEDD0C7}.Release|Any CPU.Build.0 = Release|Any CPU + {CB296A20-2732-77C1-7F23-27D5BAEDD0C7}.Release|x64.ActiveCfg = Release|Any CPU + {CB296A20-2732-77C1-7F23-27D5BAEDD0C7}.Release|x64.Build.0 = Release|Any CPU + {CB296A20-2732-77C1-7F23-27D5BAEDD0C7}.Release|x86.ActiveCfg = Release|Any CPU + {CB296A20-2732-77C1-7F23-27D5BAEDD0C7}.Release|x86.Build.0 = Release|Any CPU + {0DBEC9BA-FE1D-3898-B2C6-E4357DC23E0F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0DBEC9BA-FE1D-3898-B2C6-E4357DC23E0F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0DBEC9BA-FE1D-3898-B2C6-E4357DC23E0F}.Debug|x64.ActiveCfg = Debug|Any CPU + {0DBEC9BA-FE1D-3898-B2C6-E4357DC23E0F}.Debug|x64.Build.0 = Debug|Any CPU + {0DBEC9BA-FE1D-3898-B2C6-E4357DC23E0F}.Debug|x86.ActiveCfg = Debug|Any CPU + {0DBEC9BA-FE1D-3898-B2C6-E4357DC23E0F}.Debug|x86.Build.0 = Debug|Any CPU + {0DBEC9BA-FE1D-3898-B2C6-E4357DC23E0F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0DBEC9BA-FE1D-3898-B2C6-E4357DC23E0F}.Release|Any CPU.Build.0 = Release|Any CPU + {0DBEC9BA-FE1D-3898-B2C6-E4357DC23E0F}.Release|x64.ActiveCfg = Release|Any CPU + {0DBEC9BA-FE1D-3898-B2C6-E4357DC23E0F}.Release|x64.Build.0 = Release|Any CPU + {0DBEC9BA-FE1D-3898-B2C6-E4357DC23E0F}.Release|x86.ActiveCfg = Release|Any CPU + {0DBEC9BA-FE1D-3898-B2C6-E4357DC23E0F}.Release|x86.Build.0 = Release|Any CPU + {A63897D9-9531-989B-7309-E384BCFC2BB9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A63897D9-9531-989B-7309-E384BCFC2BB9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A63897D9-9531-989B-7309-E384BCFC2BB9}.Debug|x64.ActiveCfg = Debug|Any CPU + {A63897D9-9531-989B-7309-E384BCFC2BB9}.Debug|x64.Build.0 = Debug|Any CPU + {A63897D9-9531-989B-7309-E384BCFC2BB9}.Debug|x86.ActiveCfg = Debug|Any CPU + {A63897D9-9531-989B-7309-E384BCFC2BB9}.Debug|x86.Build.0 = Debug|Any CPU + {A63897D9-9531-989B-7309-E384BCFC2BB9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A63897D9-9531-989B-7309-E384BCFC2BB9}.Release|Any CPU.Build.0 = Release|Any CPU + {A63897D9-9531-989B-7309-E384BCFC2BB9}.Release|x64.ActiveCfg = Release|Any CPU + {A63897D9-9531-989B-7309-E384BCFC2BB9}.Release|x64.Build.0 = Release|Any CPU + {A63897D9-9531-989B-7309-E384BCFC2BB9}.Release|x86.ActiveCfg = Release|Any CPU + {A63897D9-9531-989B-7309-E384BCFC2BB9}.Release|x86.Build.0 = Release|Any CPU + {8C594D82-3463-3367-4F06-900AC707753D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8C594D82-3463-3367-4F06-900AC707753D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8C594D82-3463-3367-4F06-900AC707753D}.Debug|x64.ActiveCfg = Debug|Any CPU + {8C594D82-3463-3367-4F06-900AC707753D}.Debug|x64.Build.0 = Debug|Any CPU + {8C594D82-3463-3367-4F06-900AC707753D}.Debug|x86.ActiveCfg = Debug|Any CPU + {8C594D82-3463-3367-4F06-900AC707753D}.Debug|x86.Build.0 = Debug|Any CPU + {8C594D82-3463-3367-4F06-900AC707753D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8C594D82-3463-3367-4F06-900AC707753D}.Release|Any CPU.Build.0 = Release|Any CPU + {8C594D82-3463-3367-4F06-900AC707753D}.Release|x64.ActiveCfg = Release|Any CPU + {8C594D82-3463-3367-4F06-900AC707753D}.Release|x64.Build.0 = Release|Any CPU + {8C594D82-3463-3367-4F06-900AC707753D}.Release|x86.ActiveCfg = Release|Any CPU + {8C594D82-3463-3367-4F06-900AC707753D}.Release|x86.Build.0 = Release|Any CPU + {52F400CD-D473-7A1F-7986-89011CD2A887}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {52F400CD-D473-7A1F-7986-89011CD2A887}.Debug|Any CPU.Build.0 = Debug|Any CPU + {52F400CD-D473-7A1F-7986-89011CD2A887}.Debug|x64.ActiveCfg = Debug|Any CPU + {52F400CD-D473-7A1F-7986-89011CD2A887}.Debug|x64.Build.0 = Debug|Any CPU + {52F400CD-D473-7A1F-7986-89011CD2A887}.Debug|x86.ActiveCfg = Debug|Any CPU + {52F400CD-D473-7A1F-7986-89011CD2A887}.Debug|x86.Build.0 = Debug|Any CPU + {52F400CD-D473-7A1F-7986-89011CD2A887}.Release|Any CPU.ActiveCfg = Release|Any CPU + {52F400CD-D473-7A1F-7986-89011CD2A887}.Release|Any CPU.Build.0 = Release|Any CPU + {52F400CD-D473-7A1F-7986-89011CD2A887}.Release|x64.ActiveCfg = Release|Any CPU + {52F400CD-D473-7A1F-7986-89011CD2A887}.Release|x64.Build.0 = Release|Any CPU + {52F400CD-D473-7A1F-7986-89011CD2A887}.Release|x86.ActiveCfg = Release|Any CPU + {52F400CD-D473-7A1F-7986-89011CD2A887}.Release|x86.Build.0 = Release|Any CPU + {9588FBF9-C37E-D16E-2E8F-CFA226EAC01D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9588FBF9-C37E-D16E-2E8F-CFA226EAC01D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9588FBF9-C37E-D16E-2E8F-CFA226EAC01D}.Debug|x64.ActiveCfg = Debug|Any CPU + {9588FBF9-C37E-D16E-2E8F-CFA226EAC01D}.Debug|x64.Build.0 = Debug|Any CPU + {9588FBF9-C37E-D16E-2E8F-CFA226EAC01D}.Debug|x86.ActiveCfg = Debug|Any CPU + {9588FBF9-C37E-D16E-2E8F-CFA226EAC01D}.Debug|x86.Build.0 = Debug|Any CPU + {9588FBF9-C37E-D16E-2E8F-CFA226EAC01D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9588FBF9-C37E-D16E-2E8F-CFA226EAC01D}.Release|Any CPU.Build.0 = Release|Any CPU + {9588FBF9-C37E-D16E-2E8F-CFA226EAC01D}.Release|x64.ActiveCfg = Release|Any CPU + {9588FBF9-C37E-D16E-2E8F-CFA226EAC01D}.Release|x64.Build.0 = Release|Any CPU + {9588FBF9-C37E-D16E-2E8F-CFA226EAC01D}.Release|x86.ActiveCfg = Release|Any CPU + {9588FBF9-C37E-D16E-2E8F-CFA226EAC01D}.Release|x86.Build.0 = Release|Any CPU + {97998C88-E6E1-D5E2-B632-537B58E00CBF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {97998C88-E6E1-D5E2-B632-537B58E00CBF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {97998C88-E6E1-D5E2-B632-537B58E00CBF}.Debug|x64.ActiveCfg = Debug|Any CPU + {97998C88-E6E1-D5E2-B632-537B58E00CBF}.Debug|x64.Build.0 = Debug|Any CPU + {97998C88-E6E1-D5E2-B632-537B58E00CBF}.Debug|x86.ActiveCfg = Debug|Any CPU + {97998C88-E6E1-D5E2-B632-537B58E00CBF}.Debug|x86.Build.0 = Debug|Any CPU + {97998C88-E6E1-D5E2-B632-537B58E00CBF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {97998C88-E6E1-D5E2-B632-537B58E00CBF}.Release|Any CPU.Build.0 = Release|Any CPU + {97998C88-E6E1-D5E2-B632-537B58E00CBF}.Release|x64.ActiveCfg = Release|Any CPU + {97998C88-E6E1-D5E2-B632-537B58E00CBF}.Release|x64.Build.0 = Release|Any CPU + {97998C88-E6E1-D5E2-B632-537B58E00CBF}.Release|x86.ActiveCfg = Release|Any CPU + {97998C88-E6E1-D5E2-B632-537B58E00CBF}.Release|x86.Build.0 = Release|Any CPU + {BAD08D96-A80A-D27F-5D9C-656AEEB3D568}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BAD08D96-A80A-D27F-5D9C-656AEEB3D568}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BAD08D96-A80A-D27F-5D9C-656AEEB3D568}.Debug|x64.ActiveCfg = Debug|Any CPU + {BAD08D96-A80A-D27F-5D9C-656AEEB3D568}.Debug|x64.Build.0 = Debug|Any CPU + {BAD08D96-A80A-D27F-5D9C-656AEEB3D568}.Debug|x86.ActiveCfg = Debug|Any CPU + {BAD08D96-A80A-D27F-5D9C-656AEEB3D568}.Debug|x86.Build.0 = Debug|Any CPU + {BAD08D96-A80A-D27F-5D9C-656AEEB3D568}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BAD08D96-A80A-D27F-5D9C-656AEEB3D568}.Release|Any CPU.Build.0 = Release|Any CPU + {BAD08D96-A80A-D27F-5D9C-656AEEB3D568}.Release|x64.ActiveCfg = Release|Any CPU + {BAD08D96-A80A-D27F-5D9C-656AEEB3D568}.Release|x64.Build.0 = Release|Any CPU + {BAD08D96-A80A-D27F-5D9C-656AEEB3D568}.Release|x86.ActiveCfg = Release|Any CPU + {BAD08D96-A80A-D27F-5D9C-656AEEB3D568}.Release|x86.Build.0 = Release|Any CPU + {F63694F1-B56D-6E72-3F5D-5D38B1541F0F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F63694F1-B56D-6E72-3F5D-5D38B1541F0F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F63694F1-B56D-6E72-3F5D-5D38B1541F0F}.Debug|x64.ActiveCfg = Debug|Any CPU + {F63694F1-B56D-6E72-3F5D-5D38B1541F0F}.Debug|x64.Build.0 = Debug|Any CPU + {F63694F1-B56D-6E72-3F5D-5D38B1541F0F}.Debug|x86.ActiveCfg = Debug|Any CPU + {F63694F1-B56D-6E72-3F5D-5D38B1541F0F}.Debug|x86.Build.0 = Debug|Any CPU + {F63694F1-B56D-6E72-3F5D-5D38B1541F0F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F63694F1-B56D-6E72-3F5D-5D38B1541F0F}.Release|Any CPU.Build.0 = Release|Any CPU + {F63694F1-B56D-6E72-3F5D-5D38B1541F0F}.Release|x64.ActiveCfg = Release|Any CPU + {F63694F1-B56D-6E72-3F5D-5D38B1541F0F}.Release|x64.Build.0 = Release|Any CPU + {F63694F1-B56D-6E72-3F5D-5D38B1541F0F}.Release|x86.ActiveCfg = Release|Any CPU + {F63694F1-B56D-6E72-3F5D-5D38B1541F0F}.Release|x86.Build.0 = Release|Any CPU + {20D1569C-2A47-38B8-075E-47225B674394}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {20D1569C-2A47-38B8-075E-47225B674394}.Debug|Any CPU.Build.0 = Debug|Any CPU + {20D1569C-2A47-38B8-075E-47225B674394}.Debug|x64.ActiveCfg = Debug|Any CPU + {20D1569C-2A47-38B8-075E-47225B674394}.Debug|x64.Build.0 = Debug|Any CPU + {20D1569C-2A47-38B8-075E-47225B674394}.Debug|x86.ActiveCfg = Debug|Any CPU + {20D1569C-2A47-38B8-075E-47225B674394}.Debug|x86.Build.0 = Debug|Any CPU + {20D1569C-2A47-38B8-075E-47225B674394}.Release|Any CPU.ActiveCfg = Release|Any CPU + {20D1569C-2A47-38B8-075E-47225B674394}.Release|Any CPU.Build.0 = Release|Any CPU + {20D1569C-2A47-38B8-075E-47225B674394}.Release|x64.ActiveCfg = Release|Any CPU + {20D1569C-2A47-38B8-075E-47225B674394}.Release|x64.Build.0 = Release|Any CPU + {20D1569C-2A47-38B8-075E-47225B674394}.Release|x86.ActiveCfg = Release|Any CPU + {20D1569C-2A47-38B8-075E-47225B674394}.Release|x86.Build.0 = Release|Any CPU + {38A9EE9B-6FC8-93BC-0D43-2A906E678D66}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {38A9EE9B-6FC8-93BC-0D43-2A906E678D66}.Debug|Any CPU.Build.0 = Debug|Any CPU + {38A9EE9B-6FC8-93BC-0D43-2A906E678D66}.Debug|x64.ActiveCfg = Debug|Any CPU + {38A9EE9B-6FC8-93BC-0D43-2A906E678D66}.Debug|x64.Build.0 = Debug|Any CPU + {38A9EE9B-6FC8-93BC-0D43-2A906E678D66}.Debug|x86.ActiveCfg = Debug|Any CPU + {38A9EE9B-6FC8-93BC-0D43-2A906E678D66}.Debug|x86.Build.0 = Debug|Any CPU + {38A9EE9B-6FC8-93BC-0D43-2A906E678D66}.Release|Any CPU.ActiveCfg = Release|Any CPU + {38A9EE9B-6FC8-93BC-0D43-2A906E678D66}.Release|Any CPU.Build.0 = Release|Any CPU + {38A9EE9B-6FC8-93BC-0D43-2A906E678D66}.Release|x64.ActiveCfg = Release|Any CPU + {38A9EE9B-6FC8-93BC-0D43-2A906E678D66}.Release|x64.Build.0 = Release|Any CPU + {38A9EE9B-6FC8-93BC-0D43-2A906E678D66}.Release|x86.ActiveCfg = Release|Any CPU + {38A9EE9B-6FC8-93BC-0D43-2A906E678D66}.Release|x86.Build.0 = Release|Any CPU + {19868E2D-7163-2108-1094-F13887C4F070}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {19868E2D-7163-2108-1094-F13887C4F070}.Debug|Any CPU.Build.0 = Debug|Any CPU + {19868E2D-7163-2108-1094-F13887C4F070}.Debug|x64.ActiveCfg = Debug|Any CPU + {19868E2D-7163-2108-1094-F13887C4F070}.Debug|x64.Build.0 = Debug|Any CPU + {19868E2D-7163-2108-1094-F13887C4F070}.Debug|x86.ActiveCfg = Debug|Any CPU + {19868E2D-7163-2108-1094-F13887C4F070}.Debug|x86.Build.0 = Debug|Any CPU + {19868E2D-7163-2108-1094-F13887C4F070}.Release|Any CPU.ActiveCfg = Release|Any CPU + {19868E2D-7163-2108-1094-F13887C4F070}.Release|Any CPU.Build.0 = Release|Any CPU + {19868E2D-7163-2108-1094-F13887C4F070}.Release|x64.ActiveCfg = Release|Any CPU + {19868E2D-7163-2108-1094-F13887C4F070}.Release|x64.Build.0 = Release|Any CPU + {19868E2D-7163-2108-1094-F13887C4F070}.Release|x86.ActiveCfg = Release|Any CPU + {19868E2D-7163-2108-1094-F13887C4F070}.Release|x86.Build.0 = Release|Any CPU + {CC319FC5-F4B1-C3DD-7310-4DAD343E0125}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CC319FC5-F4B1-C3DD-7310-4DAD343E0125}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CC319FC5-F4B1-C3DD-7310-4DAD343E0125}.Debug|x64.ActiveCfg = Debug|Any CPU + {CC319FC5-F4B1-C3DD-7310-4DAD343E0125}.Debug|x64.Build.0 = Debug|Any CPU + {CC319FC5-F4B1-C3DD-7310-4DAD343E0125}.Debug|x86.ActiveCfg = Debug|Any CPU + {CC319FC5-F4B1-C3DD-7310-4DAD343E0125}.Debug|x86.Build.0 = Debug|Any CPU + {CC319FC5-F4B1-C3DD-7310-4DAD343E0125}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CC319FC5-F4B1-C3DD-7310-4DAD343E0125}.Release|Any CPU.Build.0 = Release|Any CPU + {CC319FC5-F4B1-C3DD-7310-4DAD343E0125}.Release|x64.ActiveCfg = Release|Any CPU + {CC319FC5-F4B1-C3DD-7310-4DAD343E0125}.Release|x64.Build.0 = Release|Any CPU + {CC319FC5-F4B1-C3DD-7310-4DAD343E0125}.Release|x86.ActiveCfg = Release|Any CPU + {CC319FC5-F4B1-C3DD-7310-4DAD343E0125}.Release|x86.Build.0 = Release|Any CPU + {84F711C2-C210-28D2-F0D9-B13733FEE23D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {84F711C2-C210-28D2-F0D9-B13733FEE23D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {84F711C2-C210-28D2-F0D9-B13733FEE23D}.Debug|x64.ActiveCfg = Debug|Any CPU + {84F711C2-C210-28D2-F0D9-B13733FEE23D}.Debug|x64.Build.0 = Debug|Any CPU + {84F711C2-C210-28D2-F0D9-B13733FEE23D}.Debug|x86.ActiveCfg = Debug|Any CPU + {84F711C2-C210-28D2-F0D9-B13733FEE23D}.Debug|x86.Build.0 = Debug|Any CPU + {84F711C2-C210-28D2-F0D9-B13733FEE23D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {84F711C2-C210-28D2-F0D9-B13733FEE23D}.Release|Any CPU.Build.0 = Release|Any CPU + {84F711C2-C210-28D2-F0D9-B13733FEE23D}.Release|x64.ActiveCfg = Release|Any CPU + {84F711C2-C210-28D2-F0D9-B13733FEE23D}.Release|x64.Build.0 = Release|Any CPU + {84F711C2-C210-28D2-F0D9-B13733FEE23D}.Release|x86.ActiveCfg = Release|Any CPU + {84F711C2-C210-28D2-F0D9-B13733FEE23D}.Release|x86.Build.0 = Release|Any CPU + {CBB14B90-27F9-8DD6-DFC4-3507DBD1FBC6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CBB14B90-27F9-8DD6-DFC4-3507DBD1FBC6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CBB14B90-27F9-8DD6-DFC4-3507DBD1FBC6}.Debug|x64.ActiveCfg = Debug|Any CPU + {CBB14B90-27F9-8DD6-DFC4-3507DBD1FBC6}.Debug|x64.Build.0 = Debug|Any CPU + {CBB14B90-27F9-8DD6-DFC4-3507DBD1FBC6}.Debug|x86.ActiveCfg = Debug|Any CPU + {CBB14B90-27F9-8DD6-DFC4-3507DBD1FBC6}.Debug|x86.Build.0 = Debug|Any CPU + {CBB14B90-27F9-8DD6-DFC4-3507DBD1FBC6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CBB14B90-27F9-8DD6-DFC4-3507DBD1FBC6}.Release|Any CPU.Build.0 = Release|Any CPU + {CBB14B90-27F9-8DD6-DFC4-3507DBD1FBC6}.Release|x64.ActiveCfg = Release|Any CPU + {CBB14B90-27F9-8DD6-DFC4-3507DBD1FBC6}.Release|x64.Build.0 = Release|Any CPU + {CBB14B90-27F9-8DD6-DFC4-3507DBD1FBC6}.Release|x86.ActiveCfg = Release|Any CPU + {CBB14B90-27F9-8DD6-DFC4-3507DBD1FBC6}.Release|x86.Build.0 = Release|Any CPU + {A78EBC0F-C62C-8F56-95C0-330E376242A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A78EBC0F-C62C-8F56-95C0-330E376242A2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A78EBC0F-C62C-8F56-95C0-330E376242A2}.Debug|x64.ActiveCfg = Debug|Any CPU + {A78EBC0F-C62C-8F56-95C0-330E376242A2}.Debug|x64.Build.0 = Debug|Any CPU + {A78EBC0F-C62C-8F56-95C0-330E376242A2}.Debug|x86.ActiveCfg = Debug|Any CPU + {A78EBC0F-C62C-8F56-95C0-330E376242A2}.Debug|x86.Build.0 = Debug|Any CPU + {A78EBC0F-C62C-8F56-95C0-330E376242A2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A78EBC0F-C62C-8F56-95C0-330E376242A2}.Release|Any CPU.Build.0 = Release|Any CPU + {A78EBC0F-C62C-8F56-95C0-330E376242A2}.Release|x64.ActiveCfg = Release|Any CPU + {A78EBC0F-C62C-8F56-95C0-330E376242A2}.Release|x64.Build.0 = Release|Any CPU + {A78EBC0F-C62C-8F56-95C0-330E376242A2}.Release|x86.ActiveCfg = Release|Any CPU + {A78EBC0F-C62C-8F56-95C0-330E376242A2}.Release|x86.Build.0 = Release|Any CPU + {6D26FB21-7E48-024B-E5D4-E3F0F31976BB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6D26FB21-7E48-024B-E5D4-E3F0F31976BB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6D26FB21-7E48-024B-E5D4-E3F0F31976BB}.Debug|x64.ActiveCfg = Debug|Any CPU + {6D26FB21-7E48-024B-E5D4-E3F0F31976BB}.Debug|x64.Build.0 = Debug|Any CPU + {6D26FB21-7E48-024B-E5D4-E3F0F31976BB}.Debug|x86.ActiveCfg = Debug|Any CPU + {6D26FB21-7E48-024B-E5D4-E3F0F31976BB}.Debug|x86.Build.0 = Debug|Any CPU + {6D26FB21-7E48-024B-E5D4-E3F0F31976BB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6D26FB21-7E48-024B-E5D4-E3F0F31976BB}.Release|Any CPU.Build.0 = Release|Any CPU + {6D26FB21-7E48-024B-E5D4-E3F0F31976BB}.Release|x64.ActiveCfg = Release|Any CPU + {6D26FB21-7E48-024B-E5D4-E3F0F31976BB}.Release|x64.Build.0 = Release|Any CPU + {6D26FB21-7E48-024B-E5D4-E3F0F31976BB}.Release|x86.ActiveCfg = Release|Any CPU + {6D26FB21-7E48-024B-E5D4-E3F0F31976BB}.Release|x86.Build.0 = Release|Any CPU + {79104479-B087-E5D0-5523-F1803282A246}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {79104479-B087-E5D0-5523-F1803282A246}.Debug|Any CPU.Build.0 = Debug|Any CPU + {79104479-B087-E5D0-5523-F1803282A246}.Debug|x64.ActiveCfg = Debug|Any CPU + {79104479-B087-E5D0-5523-F1803282A246}.Debug|x64.Build.0 = Debug|Any CPU + {79104479-B087-E5D0-5523-F1803282A246}.Debug|x86.ActiveCfg = Debug|Any CPU + {79104479-B087-E5D0-5523-F1803282A246}.Debug|x86.Build.0 = Debug|Any CPU + {79104479-B087-E5D0-5523-F1803282A246}.Release|Any CPU.ActiveCfg = Release|Any CPU + {79104479-B087-E5D0-5523-F1803282A246}.Release|Any CPU.Build.0 = Release|Any CPU + {79104479-B087-E5D0-5523-F1803282A246}.Release|x64.ActiveCfg = Release|Any CPU + {79104479-B087-E5D0-5523-F1803282A246}.Release|x64.Build.0 = Release|Any CPU + {79104479-B087-E5D0-5523-F1803282A246}.Release|x86.ActiveCfg = Release|Any CPU + {79104479-B087-E5D0-5523-F1803282A246}.Release|x86.Build.0 = Release|Any CPU + {F17A6F0B-3120-2BA9-84D8-5F8BA0B9705D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F17A6F0B-3120-2BA9-84D8-5F8BA0B9705D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F17A6F0B-3120-2BA9-84D8-5F8BA0B9705D}.Debug|x64.ActiveCfg = Debug|Any CPU + {F17A6F0B-3120-2BA9-84D8-5F8BA0B9705D}.Debug|x64.Build.0 = Debug|Any CPU + {F17A6F0B-3120-2BA9-84D8-5F8BA0B9705D}.Debug|x86.ActiveCfg = Debug|Any CPU + {F17A6F0B-3120-2BA9-84D8-5F8BA0B9705D}.Debug|x86.Build.0 = Debug|Any CPU + {F17A6F0B-3120-2BA9-84D8-5F8BA0B9705D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F17A6F0B-3120-2BA9-84D8-5F8BA0B9705D}.Release|Any CPU.Build.0 = Release|Any CPU + {F17A6F0B-3120-2BA9-84D8-5F8BA0B9705D}.Release|x64.ActiveCfg = Release|Any CPU + {F17A6F0B-3120-2BA9-84D8-5F8BA0B9705D}.Release|x64.Build.0 = Release|Any CPU + {F17A6F0B-3120-2BA9-84D8-5F8BA0B9705D}.Release|x86.ActiveCfg = Release|Any CPU + {F17A6F0B-3120-2BA9-84D8-5F8BA0B9705D}.Release|x86.Build.0 = Release|Any CPU + {FBD908D6-AF93-CC62-C09D-F0BB3E0CEA7F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FBD908D6-AF93-CC62-C09D-F0BB3E0CEA7F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FBD908D6-AF93-CC62-C09D-F0BB3E0CEA7F}.Debug|x64.ActiveCfg = Debug|Any CPU + {FBD908D6-AF93-CC62-C09D-F0BB3E0CEA7F}.Debug|x64.Build.0 = Debug|Any CPU + {FBD908D6-AF93-CC62-C09D-F0BB3E0CEA7F}.Debug|x86.ActiveCfg = Debug|Any CPU + {FBD908D6-AF93-CC62-C09D-F0BB3E0CEA7F}.Debug|x86.Build.0 = Debug|Any CPU + {FBD908D6-AF93-CC62-C09D-F0BB3E0CEA7F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FBD908D6-AF93-CC62-C09D-F0BB3E0CEA7F}.Release|Any CPU.Build.0 = Release|Any CPU + {FBD908D6-AF93-CC62-C09D-F0BB3E0CEA7F}.Release|x64.ActiveCfg = Release|Any CPU + {FBD908D6-AF93-CC62-C09D-F0BB3E0CEA7F}.Release|x64.Build.0 = Release|Any CPU + {FBD908D6-AF93-CC62-C09D-F0BB3E0CEA7F}.Release|x86.ActiveCfg = Release|Any CPU + {FBD908D6-AF93-CC62-C09D-F0BB3E0CEA7F}.Release|x86.Build.0 = Release|Any CPU + {37F9B25E-81CF-95C5-0311-EA6DA191E415}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {37F9B25E-81CF-95C5-0311-EA6DA191E415}.Debug|Any CPU.Build.0 = Debug|Any CPU + {37F9B25E-81CF-95C5-0311-EA6DA191E415}.Debug|x64.ActiveCfg = Debug|Any CPU + {37F9B25E-81CF-95C5-0311-EA6DA191E415}.Debug|x64.Build.0 = Debug|Any CPU + {37F9B25E-81CF-95C5-0311-EA6DA191E415}.Debug|x86.ActiveCfg = Debug|Any CPU + {37F9B25E-81CF-95C5-0311-EA6DA191E415}.Debug|x86.Build.0 = Debug|Any CPU + {37F9B25E-81CF-95C5-0311-EA6DA191E415}.Release|Any CPU.ActiveCfg = Release|Any CPU + {37F9B25E-81CF-95C5-0311-EA6DA191E415}.Release|Any CPU.Build.0 = Release|Any CPU + {37F9B25E-81CF-95C5-0311-EA6DA191E415}.Release|x64.ActiveCfg = Release|Any CPU + {37F9B25E-81CF-95C5-0311-EA6DA191E415}.Release|x64.Build.0 = Release|Any CPU + {37F9B25E-81CF-95C5-0311-EA6DA191E415}.Release|x86.ActiveCfg = Release|Any CPU + {37F9B25E-81CF-95C5-0311-EA6DA191E415}.Release|x86.Build.0 = Release|Any CPU + {28D91816-206C-576E-1A83-FD98E08C2E3C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {28D91816-206C-576E-1A83-FD98E08C2E3C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {28D91816-206C-576E-1A83-FD98E08C2E3C}.Debug|x64.ActiveCfg = Debug|Any CPU + {28D91816-206C-576E-1A83-FD98E08C2E3C}.Debug|x64.Build.0 = Debug|Any CPU + {28D91816-206C-576E-1A83-FD98E08C2E3C}.Debug|x86.ActiveCfg = Debug|Any CPU + {28D91816-206C-576E-1A83-FD98E08C2E3C}.Debug|x86.Build.0 = Debug|Any CPU + {28D91816-206C-576E-1A83-FD98E08C2E3C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {28D91816-206C-576E-1A83-FD98E08C2E3C}.Release|Any CPU.Build.0 = Release|Any CPU + {28D91816-206C-576E-1A83-FD98E08C2E3C}.Release|x64.ActiveCfg = Release|Any CPU + {28D91816-206C-576E-1A83-FD98E08C2E3C}.Release|x64.Build.0 = Release|Any CPU + {28D91816-206C-576E-1A83-FD98E08C2E3C}.Release|x86.ActiveCfg = Release|Any CPU + {28D91816-206C-576E-1A83-FD98E08C2E3C}.Release|x86.Build.0 = Release|Any CPU + {5EFEC79C-A9F1-96A4-692C-733566107170}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5EFEC79C-A9F1-96A4-692C-733566107170}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5EFEC79C-A9F1-96A4-692C-733566107170}.Debug|x64.ActiveCfg = Debug|Any CPU + {5EFEC79C-A9F1-96A4-692C-733566107170}.Debug|x64.Build.0 = Debug|Any CPU + {5EFEC79C-A9F1-96A4-692C-733566107170}.Debug|x86.ActiveCfg = Debug|Any CPU + {5EFEC79C-A9F1-96A4-692C-733566107170}.Debug|x86.Build.0 = Debug|Any CPU + {5EFEC79C-A9F1-96A4-692C-733566107170}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5EFEC79C-A9F1-96A4-692C-733566107170}.Release|Any CPU.Build.0 = Release|Any CPU + {5EFEC79C-A9F1-96A4-692C-733566107170}.Release|x64.ActiveCfg = Release|Any CPU + {5EFEC79C-A9F1-96A4-692C-733566107170}.Release|x64.Build.0 = Release|Any CPU + {5EFEC79C-A9F1-96A4-692C-733566107170}.Release|x86.ActiveCfg = Release|Any CPU + {5EFEC79C-A9F1-96A4-692C-733566107170}.Release|x86.Build.0 = Release|Any CPU + {F4E7E32B-D78B-5A08-F7B5-E8D4C7ED20D3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F4E7E32B-D78B-5A08-F7B5-E8D4C7ED20D3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F4E7E32B-D78B-5A08-F7B5-E8D4C7ED20D3}.Debug|x64.ActiveCfg = Debug|Any CPU + {F4E7E32B-D78B-5A08-F7B5-E8D4C7ED20D3}.Debug|x64.Build.0 = Debug|Any CPU + {F4E7E32B-D78B-5A08-F7B5-E8D4C7ED20D3}.Debug|x86.ActiveCfg = Debug|Any CPU + {F4E7E32B-D78B-5A08-F7B5-E8D4C7ED20D3}.Debug|x86.Build.0 = Debug|Any CPU + {F4E7E32B-D78B-5A08-F7B5-E8D4C7ED20D3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F4E7E32B-D78B-5A08-F7B5-E8D4C7ED20D3}.Release|Any CPU.Build.0 = Release|Any CPU + {F4E7E32B-D78B-5A08-F7B5-E8D4C7ED20D3}.Release|x64.ActiveCfg = Release|Any CPU + {F4E7E32B-D78B-5A08-F7B5-E8D4C7ED20D3}.Release|x64.Build.0 = Release|Any CPU + {F4E7E32B-D78B-5A08-F7B5-E8D4C7ED20D3}.Release|x86.ActiveCfg = Release|Any CPU + {F4E7E32B-D78B-5A08-F7B5-E8D4C7ED20D3}.Release|x86.Build.0 = Release|Any CPU + {3A1CFB24-6EAA-9A87-7783-BFC56B0E5394}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3A1CFB24-6EAA-9A87-7783-BFC56B0E5394}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3A1CFB24-6EAA-9A87-7783-BFC56B0E5394}.Debug|x64.ActiveCfg = Debug|Any CPU + {3A1CFB24-6EAA-9A87-7783-BFC56B0E5394}.Debug|x64.Build.0 = Debug|Any CPU + {3A1CFB24-6EAA-9A87-7783-BFC56B0E5394}.Debug|x86.ActiveCfg = Debug|Any CPU + {3A1CFB24-6EAA-9A87-7783-BFC56B0E5394}.Debug|x86.Build.0 = Debug|Any CPU + {3A1CFB24-6EAA-9A87-7783-BFC56B0E5394}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3A1CFB24-6EAA-9A87-7783-BFC56B0E5394}.Release|Any CPU.Build.0 = Release|Any CPU + {3A1CFB24-6EAA-9A87-7783-BFC56B0E5394}.Release|x64.ActiveCfg = Release|Any CPU + {3A1CFB24-6EAA-9A87-7783-BFC56B0E5394}.Release|x64.Build.0 = Release|Any CPU + {3A1CFB24-6EAA-9A87-7783-BFC56B0E5394}.Release|x86.ActiveCfg = Release|Any CPU + {3A1CFB24-6EAA-9A87-7783-BFC56B0E5394}.Release|x86.Build.0 = Release|Any CPU + {B1969736-DE03-ADEB-2659-55B2B82B38A8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B1969736-DE03-ADEB-2659-55B2B82B38A8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B1969736-DE03-ADEB-2659-55B2B82B38A8}.Debug|x64.ActiveCfg = Debug|Any CPU + {B1969736-DE03-ADEB-2659-55B2B82B38A8}.Debug|x64.Build.0 = Debug|Any CPU + {B1969736-DE03-ADEB-2659-55B2B82B38A8}.Debug|x86.ActiveCfg = Debug|Any CPU + {B1969736-DE03-ADEB-2659-55B2B82B38A8}.Debug|x86.Build.0 = Debug|Any CPU + {B1969736-DE03-ADEB-2659-55B2B82B38A8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B1969736-DE03-ADEB-2659-55B2B82B38A8}.Release|Any CPU.Build.0 = Release|Any CPU + {B1969736-DE03-ADEB-2659-55B2B82B38A8}.Release|x64.ActiveCfg = Release|Any CPU + {B1969736-DE03-ADEB-2659-55B2B82B38A8}.Release|x64.Build.0 = Release|Any CPU + {B1969736-DE03-ADEB-2659-55B2B82B38A8}.Release|x86.ActiveCfg = Release|Any CPU + {B1969736-DE03-ADEB-2659-55B2B82B38A8}.Release|x86.Build.0 = Release|Any CPU + {D166FCF0-F220-A013-133A-620521740411}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D166FCF0-F220-A013-133A-620521740411}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D166FCF0-F220-A013-133A-620521740411}.Debug|x64.ActiveCfg = Debug|Any CPU + {D166FCF0-F220-A013-133A-620521740411}.Debug|x64.Build.0 = Debug|Any CPU + {D166FCF0-F220-A013-133A-620521740411}.Debug|x86.ActiveCfg = Debug|Any CPU + {D166FCF0-F220-A013-133A-620521740411}.Debug|x86.Build.0 = Debug|Any CPU + {D166FCF0-F220-A013-133A-620521740411}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D166FCF0-F220-A013-133A-620521740411}.Release|Any CPU.Build.0 = Release|Any CPU + {D166FCF0-F220-A013-133A-620521740411}.Release|x64.ActiveCfg = Release|Any CPU + {D166FCF0-F220-A013-133A-620521740411}.Release|x64.Build.0 = Release|Any CPU + {D166FCF0-F220-A013-133A-620521740411}.Release|x86.ActiveCfg = Release|Any CPU + {D166FCF0-F220-A013-133A-620521740411}.Release|x86.Build.0 = Release|Any CPU + {F638D731-2DB2-2278-D9F8-019418A264F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F638D731-2DB2-2278-D9F8-019418A264F2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F638D731-2DB2-2278-D9F8-019418A264F2}.Debug|x64.ActiveCfg = Debug|Any CPU + {F638D731-2DB2-2278-D9F8-019418A264F2}.Debug|x64.Build.0 = Debug|Any CPU + {F638D731-2DB2-2278-D9F8-019418A264F2}.Debug|x86.ActiveCfg = Debug|Any CPU + {F638D731-2DB2-2278-D9F8-019418A264F2}.Debug|x86.Build.0 = Debug|Any CPU + {F638D731-2DB2-2278-D9F8-019418A264F2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F638D731-2DB2-2278-D9F8-019418A264F2}.Release|Any CPU.Build.0 = Release|Any CPU + {F638D731-2DB2-2278-D9F8-019418A264F2}.Release|x64.ActiveCfg = Release|Any CPU + {F638D731-2DB2-2278-D9F8-019418A264F2}.Release|x64.Build.0 = Release|Any CPU + {F638D731-2DB2-2278-D9F8-019418A264F2}.Release|x86.ActiveCfg = Release|Any CPU + {F638D731-2DB2-2278-D9F8-019418A264F2}.Release|x86.Build.0 = Release|Any CPU + {CAEB1FEB-B3F1-4B9D-7FEC-1983BCA60D81}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CAEB1FEB-B3F1-4B9D-7FEC-1983BCA60D81}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CAEB1FEB-B3F1-4B9D-7FEC-1983BCA60D81}.Debug|x64.ActiveCfg = Debug|Any CPU + {CAEB1FEB-B3F1-4B9D-7FEC-1983BCA60D81}.Debug|x64.Build.0 = Debug|Any CPU + {CAEB1FEB-B3F1-4B9D-7FEC-1983BCA60D81}.Debug|x86.ActiveCfg = Debug|Any CPU + {CAEB1FEB-B3F1-4B9D-7FEC-1983BCA60D81}.Debug|x86.Build.0 = Debug|Any CPU + {CAEB1FEB-B3F1-4B9D-7FEC-1983BCA60D81}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CAEB1FEB-B3F1-4B9D-7FEC-1983BCA60D81}.Release|Any CPU.Build.0 = Release|Any CPU + {CAEB1FEB-B3F1-4B9D-7FEC-1983BCA60D81}.Release|x64.ActiveCfg = Release|Any CPU + {CAEB1FEB-B3F1-4B9D-7FEC-1983BCA60D81}.Release|x64.Build.0 = Release|Any CPU + {CAEB1FEB-B3F1-4B9D-7FEC-1983BCA60D81}.Release|x86.ActiveCfg = Release|Any CPU + {CAEB1FEB-B3F1-4B9D-7FEC-1983BCA60D81}.Release|x86.Build.0 = Release|Any CPU + {B07074FE-3D4E-5957-5F81-B75B5D25BD1B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B07074FE-3D4E-5957-5F81-B75B5D25BD1B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B07074FE-3D4E-5957-5F81-B75B5D25BD1B}.Debug|x64.ActiveCfg = Debug|Any CPU + {B07074FE-3D4E-5957-5F81-B75B5D25BD1B}.Debug|x64.Build.0 = Debug|Any CPU + {B07074FE-3D4E-5957-5F81-B75B5D25BD1B}.Debug|x86.ActiveCfg = Debug|Any CPU + {B07074FE-3D4E-5957-5F81-B75B5D25BD1B}.Debug|x86.Build.0 = Debug|Any CPU + {B07074FE-3D4E-5957-5F81-B75B5D25BD1B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B07074FE-3D4E-5957-5F81-B75B5D25BD1B}.Release|Any CPU.Build.0 = Release|Any CPU + {B07074FE-3D4E-5957-5F81-B75B5D25BD1B}.Release|x64.ActiveCfg = Release|Any CPU + {B07074FE-3D4E-5957-5F81-B75B5D25BD1B}.Release|x64.Build.0 = Release|Any CPU + {B07074FE-3D4E-5957-5F81-B75B5D25BD1B}.Release|x86.ActiveCfg = Release|Any CPU + {B07074FE-3D4E-5957-5F81-B75B5D25BD1B}.Release|x86.Build.0 = Release|Any CPU + {91B8E22B-C90B-AEBD-707E-57BBD549BA32}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {91B8E22B-C90B-AEBD-707E-57BBD549BA32}.Debug|Any CPU.Build.0 = Debug|Any CPU + {91B8E22B-C90B-AEBD-707E-57BBD549BA32}.Debug|x64.ActiveCfg = Debug|Any CPU + {91B8E22B-C90B-AEBD-707E-57BBD549BA32}.Debug|x64.Build.0 = Debug|Any CPU + {91B8E22B-C90B-AEBD-707E-57BBD549BA32}.Debug|x86.ActiveCfg = Debug|Any CPU + {91B8E22B-C90B-AEBD-707E-57BBD549BA32}.Debug|x86.Build.0 = Debug|Any CPU + {91B8E22B-C90B-AEBD-707E-57BBD549BA32}.Release|Any CPU.ActiveCfg = Release|Any CPU + {91B8E22B-C90B-AEBD-707E-57BBD549BA32}.Release|Any CPU.Build.0 = Release|Any CPU + {91B8E22B-C90B-AEBD-707E-57BBD549BA32}.Release|x64.ActiveCfg = Release|Any CPU + {91B8E22B-C90B-AEBD-707E-57BBD549BA32}.Release|x64.Build.0 = Release|Any CPU + {91B8E22B-C90B-AEBD-707E-57BBD549BA32}.Release|x86.ActiveCfg = Release|Any CPU + {91B8E22B-C90B-AEBD-707E-57BBD549BA32}.Release|x86.Build.0 = Release|Any CPU + {B7B5D764-C3A0-1743-0739-29966F993626}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B7B5D764-C3A0-1743-0739-29966F993626}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B7B5D764-C3A0-1743-0739-29966F993626}.Debug|x64.ActiveCfg = Debug|Any CPU + {B7B5D764-C3A0-1743-0739-29966F993626}.Debug|x64.Build.0 = Debug|Any CPU + {B7B5D764-C3A0-1743-0739-29966F993626}.Debug|x86.ActiveCfg = Debug|Any CPU + {B7B5D764-C3A0-1743-0739-29966F993626}.Debug|x86.Build.0 = Debug|Any CPU + {B7B5D764-C3A0-1743-0739-29966F993626}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B7B5D764-C3A0-1743-0739-29966F993626}.Release|Any CPU.Build.0 = Release|Any CPU + {B7B5D764-C3A0-1743-0739-29966F993626}.Release|x64.ActiveCfg = Release|Any CPU + {B7B5D764-C3A0-1743-0739-29966F993626}.Release|x64.Build.0 = Release|Any CPU + {B7B5D764-C3A0-1743-0739-29966F993626}.Release|x86.ActiveCfg = Release|Any CPU + {B7B5D764-C3A0-1743-0739-29966F993626}.Release|x86.Build.0 = Release|Any CPU + {E9039B92-DAFC-F20B-22D6-78F8E4EB7CF1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E9039B92-DAFC-F20B-22D6-78F8E4EB7CF1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E9039B92-DAFC-F20B-22D6-78F8E4EB7CF1}.Debug|x64.ActiveCfg = Debug|Any CPU + {E9039B92-DAFC-F20B-22D6-78F8E4EB7CF1}.Debug|x64.Build.0 = Debug|Any CPU + {E9039B92-DAFC-F20B-22D6-78F8E4EB7CF1}.Debug|x86.ActiveCfg = Debug|Any CPU + {E9039B92-DAFC-F20B-22D6-78F8E4EB7CF1}.Debug|x86.Build.0 = Debug|Any CPU + {E9039B92-DAFC-F20B-22D6-78F8E4EB7CF1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E9039B92-DAFC-F20B-22D6-78F8E4EB7CF1}.Release|Any CPU.Build.0 = Release|Any CPU + {E9039B92-DAFC-F20B-22D6-78F8E4EB7CF1}.Release|x64.ActiveCfg = Release|Any CPU + {E9039B92-DAFC-F20B-22D6-78F8E4EB7CF1}.Release|x64.Build.0 = Release|Any CPU + {E9039B92-DAFC-F20B-22D6-78F8E4EB7CF1}.Release|x86.ActiveCfg = Release|Any CPU + {E9039B92-DAFC-F20B-22D6-78F8E4EB7CF1}.Release|x86.Build.0 = Release|Any CPU + {C4EDBBAF-875C-4839-05A8-F6F12A5ED52D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C4EDBBAF-875C-4839-05A8-F6F12A5ED52D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C4EDBBAF-875C-4839-05A8-F6F12A5ED52D}.Debug|x64.ActiveCfg = Debug|Any CPU + {C4EDBBAF-875C-4839-05A8-F6F12A5ED52D}.Debug|x64.Build.0 = Debug|Any CPU + {C4EDBBAF-875C-4839-05A8-F6F12A5ED52D}.Debug|x86.ActiveCfg = Debug|Any CPU + {C4EDBBAF-875C-4839-05A8-F6F12A5ED52D}.Debug|x86.Build.0 = Debug|Any CPU + {C4EDBBAF-875C-4839-05A8-F6F12A5ED52D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C4EDBBAF-875C-4839-05A8-F6F12A5ED52D}.Release|Any CPU.Build.0 = Release|Any CPU + {C4EDBBAF-875C-4839-05A8-F6F12A5ED52D}.Release|x64.ActiveCfg = Release|Any CPU + {C4EDBBAF-875C-4839-05A8-F6F12A5ED52D}.Release|x64.Build.0 = Release|Any CPU + {C4EDBBAF-875C-4839-05A8-F6F12A5ED52D}.Release|x86.ActiveCfg = Release|Any CPU + {C4EDBBAF-875C-4839-05A8-F6F12A5ED52D}.Release|x86.Build.0 = Release|Any CPU + {04444789-CEE4-3F3A-6EFA-18416E620B2A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {04444789-CEE4-3F3A-6EFA-18416E620B2A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {04444789-CEE4-3F3A-6EFA-18416E620B2A}.Debug|x64.ActiveCfg = Debug|Any CPU + {04444789-CEE4-3F3A-6EFA-18416E620B2A}.Debug|x64.Build.0 = Debug|Any CPU + {04444789-CEE4-3F3A-6EFA-18416E620B2A}.Debug|x86.ActiveCfg = Debug|Any CPU + {04444789-CEE4-3F3A-6EFA-18416E620B2A}.Debug|x86.Build.0 = Debug|Any CPU + {04444789-CEE4-3F3A-6EFA-18416E620B2A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {04444789-CEE4-3F3A-6EFA-18416E620B2A}.Release|Any CPU.Build.0 = Release|Any CPU + {04444789-CEE4-3F3A-6EFA-18416E620B2A}.Release|x64.ActiveCfg = Release|Any CPU + {04444789-CEE4-3F3A-6EFA-18416E620B2A}.Release|x64.Build.0 = Release|Any CPU + {04444789-CEE4-3F3A-6EFA-18416E620B2A}.Release|x86.ActiveCfg = Release|Any CPU + {04444789-CEE4-3F3A-6EFA-18416E620B2A}.Release|x86.Build.0 = Release|Any CPU + {AD1B6448-2DC9-2F9A-D143-F09BBDF6F01F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AD1B6448-2DC9-2F9A-D143-F09BBDF6F01F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AD1B6448-2DC9-2F9A-D143-F09BBDF6F01F}.Debug|x64.ActiveCfg = Debug|Any CPU + {AD1B6448-2DC9-2F9A-D143-F09BBDF6F01F}.Debug|x64.Build.0 = Debug|Any CPU + {AD1B6448-2DC9-2F9A-D143-F09BBDF6F01F}.Debug|x86.ActiveCfg = Debug|Any CPU + {AD1B6448-2DC9-2F9A-D143-F09BBDF6F01F}.Debug|x86.Build.0 = Debug|Any CPU + {AD1B6448-2DC9-2F9A-D143-F09BBDF6F01F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AD1B6448-2DC9-2F9A-D143-F09BBDF6F01F}.Release|Any CPU.Build.0 = Release|Any CPU + {AD1B6448-2DC9-2F9A-D143-F09BBDF6F01F}.Release|x64.ActiveCfg = Release|Any CPU + {AD1B6448-2DC9-2F9A-D143-F09BBDF6F01F}.Release|x64.Build.0 = Release|Any CPU + {AD1B6448-2DC9-2F9A-D143-F09BBDF6F01F}.Release|x86.ActiveCfg = Release|Any CPU + {AD1B6448-2DC9-2F9A-D143-F09BBDF6F01F}.Release|x86.Build.0 = Release|Any CPU + {0EAC8F64-9588-1EF0-C33A-67590CF27590}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0EAC8F64-9588-1EF0-C33A-67590CF27590}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0EAC8F64-9588-1EF0-C33A-67590CF27590}.Debug|x64.ActiveCfg = Debug|Any CPU + {0EAC8F64-9588-1EF0-C33A-67590CF27590}.Debug|x64.Build.0 = Debug|Any CPU + {0EAC8F64-9588-1EF0-C33A-67590CF27590}.Debug|x86.ActiveCfg = Debug|Any CPU + {0EAC8F64-9588-1EF0-C33A-67590CF27590}.Debug|x86.Build.0 = Debug|Any CPU + {0EAC8F64-9588-1EF0-C33A-67590CF27590}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0EAC8F64-9588-1EF0-C33A-67590CF27590}.Release|Any CPU.Build.0 = Release|Any CPU + {0EAC8F64-9588-1EF0-C33A-67590CF27590}.Release|x64.ActiveCfg = Release|Any CPU + {0EAC8F64-9588-1EF0-C33A-67590CF27590}.Release|x64.Build.0 = Release|Any CPU + {0EAC8F64-9588-1EF0-C33A-67590CF27590}.Release|x86.ActiveCfg = Release|Any CPU + {0EAC8F64-9588-1EF0-C33A-67590CF27590}.Release|x86.Build.0 = Release|Any CPU + {761CAD6D-98CB-1936-9065-BF1A756671FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {761CAD6D-98CB-1936-9065-BF1A756671FF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {761CAD6D-98CB-1936-9065-BF1A756671FF}.Debug|x64.ActiveCfg = Debug|Any CPU + {761CAD6D-98CB-1936-9065-BF1A756671FF}.Debug|x64.Build.0 = Debug|Any CPU + {761CAD6D-98CB-1936-9065-BF1A756671FF}.Debug|x86.ActiveCfg = Debug|Any CPU + {761CAD6D-98CB-1936-9065-BF1A756671FF}.Debug|x86.Build.0 = Debug|Any CPU + {761CAD6D-98CB-1936-9065-BF1A756671FF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {761CAD6D-98CB-1936-9065-BF1A756671FF}.Release|Any CPU.Build.0 = Release|Any CPU + {761CAD6D-98CB-1936-9065-BF1A756671FF}.Release|x64.ActiveCfg = Release|Any CPU + {761CAD6D-98CB-1936-9065-BF1A756671FF}.Release|x64.Build.0 = Release|Any CPU + {761CAD6D-98CB-1936-9065-BF1A756671FF}.Release|x86.ActiveCfg = Release|Any CPU + {761CAD6D-98CB-1936-9065-BF1A756671FF}.Release|x86.Build.0 = Release|Any CPU + {7974C4F0-BC89-2775-8943-2DF909F3B08B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7974C4F0-BC89-2775-8943-2DF909F3B08B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7974C4F0-BC89-2775-8943-2DF909F3B08B}.Debug|x64.ActiveCfg = Debug|Any CPU + {7974C4F0-BC89-2775-8943-2DF909F3B08B}.Debug|x64.Build.0 = Debug|Any CPU + {7974C4F0-BC89-2775-8943-2DF909F3B08B}.Debug|x86.ActiveCfg = Debug|Any CPU + {7974C4F0-BC89-2775-8943-2DF909F3B08B}.Debug|x86.Build.0 = Debug|Any CPU + {7974C4F0-BC89-2775-8943-2DF909F3B08B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7974C4F0-BC89-2775-8943-2DF909F3B08B}.Release|Any CPU.Build.0 = Release|Any CPU + {7974C4F0-BC89-2775-8943-2DF909F3B08B}.Release|x64.ActiveCfg = Release|Any CPU + {7974C4F0-BC89-2775-8943-2DF909F3B08B}.Release|x64.Build.0 = Release|Any CPU + {7974C4F0-BC89-2775-8943-2DF909F3B08B}.Release|x86.ActiveCfg = Release|Any CPU + {7974C4F0-BC89-2775-8943-2DF909F3B08B}.Release|x86.Build.0 = Release|Any CPU + {B1B31937-CCC8-D97A-F66D-1849734B780B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B1B31937-CCC8-D97A-F66D-1849734B780B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B1B31937-CCC8-D97A-F66D-1849734B780B}.Debug|x64.ActiveCfg = Debug|Any CPU + {B1B31937-CCC8-D97A-F66D-1849734B780B}.Debug|x64.Build.0 = Debug|Any CPU + {B1B31937-CCC8-D97A-F66D-1849734B780B}.Debug|x86.ActiveCfg = Debug|Any CPU + {B1B31937-CCC8-D97A-F66D-1849734B780B}.Debug|x86.Build.0 = Debug|Any CPU + {B1B31937-CCC8-D97A-F66D-1849734B780B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B1B31937-CCC8-D97A-F66D-1849734B780B}.Release|Any CPU.Build.0 = Release|Any CPU + {B1B31937-CCC8-D97A-F66D-1849734B780B}.Release|x64.ActiveCfg = Release|Any CPU + {B1B31937-CCC8-D97A-F66D-1849734B780B}.Release|x64.Build.0 = Release|Any CPU + {B1B31937-CCC8-D97A-F66D-1849734B780B}.Release|x86.ActiveCfg = Release|Any CPU + {B1B31937-CCC8-D97A-F66D-1849734B780B}.Release|x86.Build.0 = Release|Any CPU + {9A566B3A-E281-09AF-EC1B-BD4FDB3248CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9A566B3A-E281-09AF-EC1B-BD4FDB3248CE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9A566B3A-E281-09AF-EC1B-BD4FDB3248CE}.Debug|x64.ActiveCfg = Debug|Any CPU + {9A566B3A-E281-09AF-EC1B-BD4FDB3248CE}.Debug|x64.Build.0 = Debug|Any CPU + {9A566B3A-E281-09AF-EC1B-BD4FDB3248CE}.Debug|x86.ActiveCfg = Debug|Any CPU + {9A566B3A-E281-09AF-EC1B-BD4FDB3248CE}.Debug|x86.Build.0 = Debug|Any CPU + {9A566B3A-E281-09AF-EC1B-BD4FDB3248CE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9A566B3A-E281-09AF-EC1B-BD4FDB3248CE}.Release|Any CPU.Build.0 = Release|Any CPU + {9A566B3A-E281-09AF-EC1B-BD4FDB3248CE}.Release|x64.ActiveCfg = Release|Any CPU + {9A566B3A-E281-09AF-EC1B-BD4FDB3248CE}.Release|x64.Build.0 = Release|Any CPU + {9A566B3A-E281-09AF-EC1B-BD4FDB3248CE}.Release|x86.ActiveCfg = Release|Any CPU + {9A566B3A-E281-09AF-EC1B-BD4FDB3248CE}.Release|x86.Build.0 = Release|Any CPU + {A345E5AC-BDDB-A817-3C92-08C8865D1EF9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A345E5AC-BDDB-A817-3C92-08C8865D1EF9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A345E5AC-BDDB-A817-3C92-08C8865D1EF9}.Debug|x64.ActiveCfg = Debug|Any CPU + {A345E5AC-BDDB-A817-3C92-08C8865D1EF9}.Debug|x64.Build.0 = Debug|Any CPU + {A345E5AC-BDDB-A817-3C92-08C8865D1EF9}.Debug|x86.ActiveCfg = Debug|Any CPU + {A345E5AC-BDDB-A817-3C92-08C8865D1EF9}.Debug|x86.Build.0 = Debug|Any CPU + {A345E5AC-BDDB-A817-3C92-08C8865D1EF9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A345E5AC-BDDB-A817-3C92-08C8865D1EF9}.Release|Any CPU.Build.0 = Release|Any CPU + {A345E5AC-BDDB-A817-3C92-08C8865D1EF9}.Release|x64.ActiveCfg = Release|Any CPU + {A345E5AC-BDDB-A817-3C92-08C8865D1EF9}.Release|x64.Build.0 = Release|Any CPU + {A345E5AC-BDDB-A817-3C92-08C8865D1EF9}.Release|x86.ActiveCfg = Release|Any CPU + {A345E5AC-BDDB-A817-3C92-08C8865D1EF9}.Release|x86.Build.0 = Release|Any CPU + {905DD8ED-3D10-7C2B-B199-B98E85267BB8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {905DD8ED-3D10-7C2B-B199-B98E85267BB8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {905DD8ED-3D10-7C2B-B199-B98E85267BB8}.Debug|x64.ActiveCfg = Debug|Any CPU + {905DD8ED-3D10-7C2B-B199-B98E85267BB8}.Debug|x64.Build.0 = Debug|Any CPU + {905DD8ED-3D10-7C2B-B199-B98E85267BB8}.Debug|x86.ActiveCfg = Debug|Any CPU + {905DD8ED-3D10-7C2B-B199-B98E85267BB8}.Debug|x86.Build.0 = Debug|Any CPU + {905DD8ED-3D10-7C2B-B199-B98E85267BB8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {905DD8ED-3D10-7C2B-B199-B98E85267BB8}.Release|Any CPU.Build.0 = Release|Any CPU + {905DD8ED-3D10-7C2B-B199-B98E85267BB8}.Release|x64.ActiveCfg = Release|Any CPU + {905DD8ED-3D10-7C2B-B199-B98E85267BB8}.Release|x64.Build.0 = Release|Any CPU + {905DD8ED-3D10-7C2B-B199-B98E85267BB8}.Release|x86.ActiveCfg = Release|Any CPU + {905DD8ED-3D10-7C2B-B199-B98E85267BB8}.Release|x86.Build.0 = Release|Any CPU + {C2D3B3C7-E556-9B72-024A-FF5EDEAF4CB5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C2D3B3C7-E556-9B72-024A-FF5EDEAF4CB5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C2D3B3C7-E556-9B72-024A-FF5EDEAF4CB5}.Debug|x64.ActiveCfg = Debug|Any CPU + {C2D3B3C7-E556-9B72-024A-FF5EDEAF4CB5}.Debug|x64.Build.0 = Debug|Any CPU + {C2D3B3C7-E556-9B72-024A-FF5EDEAF4CB5}.Debug|x86.ActiveCfg = Debug|Any CPU + {C2D3B3C7-E556-9B72-024A-FF5EDEAF4CB5}.Debug|x86.Build.0 = Debug|Any CPU + {C2D3B3C7-E556-9B72-024A-FF5EDEAF4CB5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C2D3B3C7-E556-9B72-024A-FF5EDEAF4CB5}.Release|Any CPU.Build.0 = Release|Any CPU + {C2D3B3C7-E556-9B72-024A-FF5EDEAF4CB5}.Release|x64.ActiveCfg = Release|Any CPU + {C2D3B3C7-E556-9B72-024A-FF5EDEAF4CB5}.Release|x64.Build.0 = Release|Any CPU + {C2D3B3C7-E556-9B72-024A-FF5EDEAF4CB5}.Release|x86.ActiveCfg = Release|Any CPU + {C2D3B3C7-E556-9B72-024A-FF5EDEAF4CB5}.Release|x86.Build.0 = Release|Any CPU + {31AC6B88-D6C8-E2EF-39AF-1B21AFD76C89}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {31AC6B88-D6C8-E2EF-39AF-1B21AFD76C89}.Debug|Any CPU.Build.0 = Debug|Any CPU + {31AC6B88-D6C8-E2EF-39AF-1B21AFD76C89}.Debug|x64.ActiveCfg = Debug|Any CPU + {31AC6B88-D6C8-E2EF-39AF-1B21AFD76C89}.Debug|x64.Build.0 = Debug|Any CPU + {31AC6B88-D6C8-E2EF-39AF-1B21AFD76C89}.Debug|x86.ActiveCfg = Debug|Any CPU + {31AC6B88-D6C8-E2EF-39AF-1B21AFD76C89}.Debug|x86.Build.0 = Debug|Any CPU + {31AC6B88-D6C8-E2EF-39AF-1B21AFD76C89}.Release|Any CPU.ActiveCfg = Release|Any CPU + {31AC6B88-D6C8-E2EF-39AF-1B21AFD76C89}.Release|Any CPU.Build.0 = Release|Any CPU + {31AC6B88-D6C8-E2EF-39AF-1B21AFD76C89}.Release|x64.ActiveCfg = Release|Any CPU + {31AC6B88-D6C8-E2EF-39AF-1B21AFD76C89}.Release|x64.Build.0 = Release|Any CPU + {31AC6B88-D6C8-E2EF-39AF-1B21AFD76C89}.Release|x86.ActiveCfg = Release|Any CPU + {31AC6B88-D6C8-E2EF-39AF-1B21AFD76C89}.Release|x86.Build.0 = Release|Any CPU + {90B84537-F992-234C-C998-91C6AD65AB12}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {90B84537-F992-234C-C998-91C6AD65AB12}.Debug|Any CPU.Build.0 = Debug|Any CPU + {90B84537-F992-234C-C998-91C6AD65AB12}.Debug|x64.ActiveCfg = Debug|Any CPU + {90B84537-F992-234C-C998-91C6AD65AB12}.Debug|x64.Build.0 = Debug|Any CPU + {90B84537-F992-234C-C998-91C6AD65AB12}.Debug|x86.ActiveCfg = Debug|Any CPU + {90B84537-F992-234C-C998-91C6AD65AB12}.Debug|x86.Build.0 = Debug|Any CPU + {90B84537-F992-234C-C998-91C6AD65AB12}.Release|Any CPU.ActiveCfg = Release|Any CPU + {90B84537-F992-234C-C998-91C6AD65AB12}.Release|Any CPU.Build.0 = Release|Any CPU + {90B84537-F992-234C-C998-91C6AD65AB12}.Release|x64.ActiveCfg = Release|Any CPU + {90B84537-F992-234C-C998-91C6AD65AB12}.Release|x64.Build.0 = Release|Any CPU + {90B84537-F992-234C-C998-91C6AD65AB12}.Release|x86.ActiveCfg = Release|Any CPU + {90B84537-F992-234C-C998-91C6AD65AB12}.Release|x86.Build.0 = Release|Any CPU + {F22333B6-7E27-679B-8475-B4B9AB1CB186}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F22333B6-7E27-679B-8475-B4B9AB1CB186}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F22333B6-7E27-679B-8475-B4B9AB1CB186}.Debug|x64.ActiveCfg = Debug|Any CPU + {F22333B6-7E27-679B-8475-B4B9AB1CB186}.Debug|x64.Build.0 = Debug|Any CPU + {F22333B6-7E27-679B-8475-B4B9AB1CB186}.Debug|x86.ActiveCfg = Debug|Any CPU + {F22333B6-7E27-679B-8475-B4B9AB1CB186}.Debug|x86.Build.0 = Debug|Any CPU + {F22333B6-7E27-679B-8475-B4B9AB1CB186}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F22333B6-7E27-679B-8475-B4B9AB1CB186}.Release|Any CPU.Build.0 = Release|Any CPU + {F22333B6-7E27-679B-8475-B4B9AB1CB186}.Release|x64.ActiveCfg = Release|Any CPU + {F22333B6-7E27-679B-8475-B4B9AB1CB186}.Release|x64.Build.0 = Release|Any CPU + {F22333B6-7E27-679B-8475-B4B9AB1CB186}.Release|x86.ActiveCfg = Release|Any CPU + {F22333B6-7E27-679B-8475-B4B9AB1CB186}.Release|x86.Build.0 = Release|Any CPU + {CE042F3A-6851-FAAB-9E9C-AD905B4AAC8D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CE042F3A-6851-FAAB-9E9C-AD905B4AAC8D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CE042F3A-6851-FAAB-9E9C-AD905B4AAC8D}.Debug|x64.ActiveCfg = Debug|Any CPU + {CE042F3A-6851-FAAB-9E9C-AD905B4AAC8D}.Debug|x64.Build.0 = Debug|Any CPU + {CE042F3A-6851-FAAB-9E9C-AD905B4AAC8D}.Debug|x86.ActiveCfg = Debug|Any CPU + {CE042F3A-6851-FAAB-9E9C-AD905B4AAC8D}.Debug|x86.Build.0 = Debug|Any CPU + {CE042F3A-6851-FAAB-9E9C-AD905B4AAC8D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CE042F3A-6851-FAAB-9E9C-AD905B4AAC8D}.Release|Any CPU.Build.0 = Release|Any CPU + {CE042F3A-6851-FAAB-9E9C-AD905B4AAC8D}.Release|x64.ActiveCfg = Release|Any CPU + {CE042F3A-6851-FAAB-9E9C-AD905B4AAC8D}.Release|x64.Build.0 = Release|Any CPU + {CE042F3A-6851-FAAB-9E9C-AD905B4AAC8D}.Release|x86.ActiveCfg = Release|Any CPU + {CE042F3A-6851-FAAB-9E9C-AD905B4AAC8D}.Release|x86.Build.0 = Release|Any CPU + {D6B56A54-4057-9F76-BC7E-56E896E5D276}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D6B56A54-4057-9F76-BC7E-56E896E5D276}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D6B56A54-4057-9F76-BC7E-56E896E5D276}.Debug|x64.ActiveCfg = Debug|Any CPU + {D6B56A54-4057-9F76-BC7E-56E896E5D276}.Debug|x64.Build.0 = Debug|Any CPU + {D6B56A54-4057-9F76-BC7E-56E896E5D276}.Debug|x86.ActiveCfg = Debug|Any CPU + {D6B56A54-4057-9F76-BC7E-56E896E5D276}.Debug|x86.Build.0 = Debug|Any CPU + {D6B56A54-4057-9F76-BC7E-56E896E5D276}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D6B56A54-4057-9F76-BC7E-56E896E5D276}.Release|Any CPU.Build.0 = Release|Any CPU + {D6B56A54-4057-9F76-BC7E-56E896E5D276}.Release|x64.ActiveCfg = Release|Any CPU + {D6B56A54-4057-9F76-BC7E-56E896E5D276}.Release|x64.Build.0 = Release|Any CPU + {D6B56A54-4057-9F76-BC7E-56E896E5D276}.Release|x86.ActiveCfg = Release|Any CPU + {D6B56A54-4057-9F76-BC7E-56E896E5D276}.Release|x86.Build.0 = Release|Any CPU + {9258E4F2-762C-C780-F118-2CABD0281CC9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9258E4F2-762C-C780-F118-2CABD0281CC9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9258E4F2-762C-C780-F118-2CABD0281CC9}.Debug|x64.ActiveCfg = Debug|Any CPU + {9258E4F2-762C-C780-F118-2CABD0281CC9}.Debug|x64.Build.0 = Debug|Any CPU + {9258E4F2-762C-C780-F118-2CABD0281CC9}.Debug|x86.ActiveCfg = Debug|Any CPU + {9258E4F2-762C-C780-F118-2CABD0281CC9}.Debug|x86.Build.0 = Debug|Any CPU + {9258E4F2-762C-C780-F118-2CABD0281CC9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9258E4F2-762C-C780-F118-2CABD0281CC9}.Release|Any CPU.Build.0 = Release|Any CPU + {9258E4F2-762C-C780-F118-2CABD0281CC9}.Release|x64.ActiveCfg = Release|Any CPU + {9258E4F2-762C-C780-F118-2CABD0281CC9}.Release|x64.Build.0 = Release|Any CPU + {9258E4F2-762C-C780-F118-2CABD0281CC9}.Release|x86.ActiveCfg = Release|Any CPU + {9258E4F2-762C-C780-F118-2CABD0281CC9}.Release|x86.Build.0 = Release|Any CPU + {D6752A7E-0FD2-6626-80E3-CE4D4816D2B0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D6752A7E-0FD2-6626-80E3-CE4D4816D2B0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D6752A7E-0FD2-6626-80E3-CE4D4816D2B0}.Debug|x64.ActiveCfg = Debug|Any CPU + {D6752A7E-0FD2-6626-80E3-CE4D4816D2B0}.Debug|x64.Build.0 = Debug|Any CPU + {D6752A7E-0FD2-6626-80E3-CE4D4816D2B0}.Debug|x86.ActiveCfg = Debug|Any CPU + {D6752A7E-0FD2-6626-80E3-CE4D4816D2B0}.Debug|x86.Build.0 = Debug|Any CPU + {D6752A7E-0FD2-6626-80E3-CE4D4816D2B0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D6752A7E-0FD2-6626-80E3-CE4D4816D2B0}.Release|Any CPU.Build.0 = Release|Any CPU + {D6752A7E-0FD2-6626-80E3-CE4D4816D2B0}.Release|x64.ActiveCfg = Release|Any CPU + {D6752A7E-0FD2-6626-80E3-CE4D4816D2B0}.Release|x64.Build.0 = Release|Any CPU + {D6752A7E-0FD2-6626-80E3-CE4D4816D2B0}.Release|x86.ActiveCfg = Release|Any CPU + {D6752A7E-0FD2-6626-80E3-CE4D4816D2B0}.Release|x86.Build.0 = Release|Any CPU + {AF85AC87-521A-2F0E-5F10-836E416EC716}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AF85AC87-521A-2F0E-5F10-836E416EC716}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AF85AC87-521A-2F0E-5F10-836E416EC716}.Debug|x64.ActiveCfg = Debug|Any CPU + {AF85AC87-521A-2F0E-5F10-836E416EC716}.Debug|x64.Build.0 = Debug|Any CPU + {AF85AC87-521A-2F0E-5F10-836E416EC716}.Debug|x86.ActiveCfg = Debug|Any CPU + {AF85AC87-521A-2F0E-5F10-836E416EC716}.Debug|x86.Build.0 = Debug|Any CPU + {AF85AC87-521A-2F0E-5F10-836E416EC716}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AF85AC87-521A-2F0E-5F10-836E416EC716}.Release|Any CPU.Build.0 = Release|Any CPU + {AF85AC87-521A-2F0E-5F10-836E416EC716}.Release|x64.ActiveCfg = Release|Any CPU + {AF85AC87-521A-2F0E-5F10-836E416EC716}.Release|x64.Build.0 = Release|Any CPU + {AF85AC87-521A-2F0E-5F10-836E416EC716}.Release|x86.ActiveCfg = Release|Any CPU + {AF85AC87-521A-2F0E-5F10-836E416EC716}.Release|x86.Build.0 = Release|Any CPU + {FB946C57-55B3-08C6-18AE-1672D46C5308}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FB946C57-55B3-08C6-18AE-1672D46C5308}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FB946C57-55B3-08C6-18AE-1672D46C5308}.Debug|x64.ActiveCfg = Debug|Any CPU + {FB946C57-55B3-08C6-18AE-1672D46C5308}.Debug|x64.Build.0 = Debug|Any CPU + {FB946C57-55B3-08C6-18AE-1672D46C5308}.Debug|x86.ActiveCfg = Debug|Any CPU + {FB946C57-55B3-08C6-18AE-1672D46C5308}.Debug|x86.Build.0 = Debug|Any CPU + {FB946C57-55B3-08C6-18AE-1672D46C5308}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FB946C57-55B3-08C6-18AE-1672D46C5308}.Release|Any CPU.Build.0 = Release|Any CPU + {FB946C57-55B3-08C6-18AE-1672D46C5308}.Release|x64.ActiveCfg = Release|Any CPU + {FB946C57-55B3-08C6-18AE-1672D46C5308}.Release|x64.Build.0 = Release|Any CPU + {FB946C57-55B3-08C6-18AE-1672D46C5308}.Release|x86.ActiveCfg = Release|Any CPU + {FB946C57-55B3-08C6-18AE-1672D46C5308}.Release|x86.Build.0 = Release|Any CPU + {99A47EAA-44B8-8E06-DA0E-05B225009FDF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {99A47EAA-44B8-8E06-DA0E-05B225009FDF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {99A47EAA-44B8-8E06-DA0E-05B225009FDF}.Debug|x64.ActiveCfg = Debug|Any CPU + {99A47EAA-44B8-8E06-DA0E-05B225009FDF}.Debug|x64.Build.0 = Debug|Any CPU + {99A47EAA-44B8-8E06-DA0E-05B225009FDF}.Debug|x86.ActiveCfg = Debug|Any CPU + {99A47EAA-44B8-8E06-DA0E-05B225009FDF}.Debug|x86.Build.0 = Debug|Any CPU + {99A47EAA-44B8-8E06-DA0E-05B225009FDF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {99A47EAA-44B8-8E06-DA0E-05B225009FDF}.Release|Any CPU.Build.0 = Release|Any CPU + {99A47EAA-44B8-8E06-DA0E-05B225009FDF}.Release|x64.ActiveCfg = Release|Any CPU + {99A47EAA-44B8-8E06-DA0E-05B225009FDF}.Release|x64.Build.0 = Release|Any CPU + {99A47EAA-44B8-8E06-DA0E-05B225009FDF}.Release|x86.ActiveCfg = Release|Any CPU + {99A47EAA-44B8-8E06-DA0E-05B225009FDF}.Release|x86.Build.0 = Release|Any CPU + {4F0EF830-4308-347B-A31D-270A9812D15E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4F0EF830-4308-347B-A31D-270A9812D15E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4F0EF830-4308-347B-A31D-270A9812D15E}.Debug|x64.ActiveCfg = Debug|Any CPU + {4F0EF830-4308-347B-A31D-270A9812D15E}.Debug|x64.Build.0 = Debug|Any CPU + {4F0EF830-4308-347B-A31D-270A9812D15E}.Debug|x86.ActiveCfg = Debug|Any CPU + {4F0EF830-4308-347B-A31D-270A9812D15E}.Debug|x86.Build.0 = Debug|Any CPU + {4F0EF830-4308-347B-A31D-270A9812D15E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4F0EF830-4308-347B-A31D-270A9812D15E}.Release|Any CPU.Build.0 = Release|Any CPU + {4F0EF830-4308-347B-A31D-270A9812D15E}.Release|x64.ActiveCfg = Release|Any CPU + {4F0EF830-4308-347B-A31D-270A9812D15E}.Release|x64.Build.0 = Release|Any CPU + {4F0EF830-4308-347B-A31D-270A9812D15E}.Release|x86.ActiveCfg = Release|Any CPU + {4F0EF830-4308-347B-A31D-270A9812D15E}.Release|x86.Build.0 = Release|Any CPU + {B7EE2F70-A634-8B4D-93F4-EA1EEFD9E5E8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B7EE2F70-A634-8B4D-93F4-EA1EEFD9E5E8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B7EE2F70-A634-8B4D-93F4-EA1EEFD9E5E8}.Debug|x64.ActiveCfg = Debug|Any CPU + {B7EE2F70-A634-8B4D-93F4-EA1EEFD9E5E8}.Debug|x64.Build.0 = Debug|Any CPU + {B7EE2F70-A634-8B4D-93F4-EA1EEFD9E5E8}.Debug|x86.ActiveCfg = Debug|Any CPU + {B7EE2F70-A634-8B4D-93F4-EA1EEFD9E5E8}.Debug|x86.Build.0 = Debug|Any CPU + {B7EE2F70-A634-8B4D-93F4-EA1EEFD9E5E8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B7EE2F70-A634-8B4D-93F4-EA1EEFD9E5E8}.Release|Any CPU.Build.0 = Release|Any CPU + {B7EE2F70-A634-8B4D-93F4-EA1EEFD9E5E8}.Release|x64.ActiveCfg = Release|Any CPU + {B7EE2F70-A634-8B4D-93F4-EA1EEFD9E5E8}.Release|x64.Build.0 = Release|Any CPU + {B7EE2F70-A634-8B4D-93F4-EA1EEFD9E5E8}.Release|x86.ActiveCfg = Release|Any CPU + {B7EE2F70-A634-8B4D-93F4-EA1EEFD9E5E8}.Release|x86.Build.0 = Release|Any CPU + {A5298720-984E-6574-D41B-CFE7CA408182}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A5298720-984E-6574-D41B-CFE7CA408182}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A5298720-984E-6574-D41B-CFE7CA408182}.Debug|x64.ActiveCfg = Debug|Any CPU + {A5298720-984E-6574-D41B-CFE7CA408182}.Debug|x64.Build.0 = Debug|Any CPU + {A5298720-984E-6574-D41B-CFE7CA408182}.Debug|x86.ActiveCfg = Debug|Any CPU + {A5298720-984E-6574-D41B-CFE7CA408182}.Debug|x86.Build.0 = Debug|Any CPU + {A5298720-984E-6574-D41B-CFE7CA408182}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A5298720-984E-6574-D41B-CFE7CA408182}.Release|Any CPU.Build.0 = Release|Any CPU + {A5298720-984E-6574-D41B-CFE7CA408182}.Release|x64.ActiveCfg = Release|Any CPU + {A5298720-984E-6574-D41B-CFE7CA408182}.Release|x64.Build.0 = Release|Any CPU + {A5298720-984E-6574-D41B-CFE7CA408182}.Release|x86.ActiveCfg = Release|Any CPU + {A5298720-984E-6574-D41B-CFE7CA408182}.Release|x86.Build.0 = Release|Any CPU + {CB033CB6-F90B-E201-BA86-C867544E7247}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CB033CB6-F90B-E201-BA86-C867544E7247}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CB033CB6-F90B-E201-BA86-C867544E7247}.Debug|x64.ActiveCfg = Debug|Any CPU + {CB033CB6-F90B-E201-BA86-C867544E7247}.Debug|x64.Build.0 = Debug|Any CPU + {CB033CB6-F90B-E201-BA86-C867544E7247}.Debug|x86.ActiveCfg = Debug|Any CPU + {CB033CB6-F90B-E201-BA86-C867544E7247}.Debug|x86.Build.0 = Debug|Any CPU + {CB033CB6-F90B-E201-BA86-C867544E7247}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CB033CB6-F90B-E201-BA86-C867544E7247}.Release|Any CPU.Build.0 = Release|Any CPU + {CB033CB6-F90B-E201-BA86-C867544E7247}.Release|x64.ActiveCfg = Release|Any CPU + {CB033CB6-F90B-E201-BA86-C867544E7247}.Release|x64.Build.0 = Release|Any CPU + {CB033CB6-F90B-E201-BA86-C867544E7247}.Release|x86.ActiveCfg = Release|Any CPU + {CB033CB6-F90B-E201-BA86-C867544E7247}.Release|x86.Build.0 = Release|Any CPU + {E9F5BFF2-0D0E-7B41-9AF0-83384F4B8825}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E9F5BFF2-0D0E-7B41-9AF0-83384F4B8825}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E9F5BFF2-0D0E-7B41-9AF0-83384F4B8825}.Debug|x64.ActiveCfg = Debug|Any CPU + {E9F5BFF2-0D0E-7B41-9AF0-83384F4B8825}.Debug|x64.Build.0 = Debug|Any CPU + {E9F5BFF2-0D0E-7B41-9AF0-83384F4B8825}.Debug|x86.ActiveCfg = Debug|Any CPU + {E9F5BFF2-0D0E-7B41-9AF0-83384F4B8825}.Debug|x86.Build.0 = Debug|Any CPU + {E9F5BFF2-0D0E-7B41-9AF0-83384F4B8825}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E9F5BFF2-0D0E-7B41-9AF0-83384F4B8825}.Release|Any CPU.Build.0 = Release|Any CPU + {E9F5BFF2-0D0E-7B41-9AF0-83384F4B8825}.Release|x64.ActiveCfg = Release|Any CPU + {E9F5BFF2-0D0E-7B41-9AF0-83384F4B8825}.Release|x64.Build.0 = Release|Any CPU + {E9F5BFF2-0D0E-7B41-9AF0-83384F4B8825}.Release|x86.ActiveCfg = Release|Any CPU + {E9F5BFF2-0D0E-7B41-9AF0-83384F4B8825}.Release|x86.Build.0 = Release|Any CPU + {668466AC-CD66-BAA0-0322-148549E373CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {668466AC-CD66-BAA0-0322-148549E373CB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {668466AC-CD66-BAA0-0322-148549E373CB}.Debug|x64.ActiveCfg = Debug|Any CPU + {668466AC-CD66-BAA0-0322-148549E373CB}.Debug|x64.Build.0 = Debug|Any CPU + {668466AC-CD66-BAA0-0322-148549E373CB}.Debug|x86.ActiveCfg = Debug|Any CPU + {668466AC-CD66-BAA0-0322-148549E373CB}.Debug|x86.Build.0 = Debug|Any CPU + {668466AC-CD66-BAA0-0322-148549E373CB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {668466AC-CD66-BAA0-0322-148549E373CB}.Release|Any CPU.Build.0 = Release|Any CPU + {668466AC-CD66-BAA0-0322-148549E373CB}.Release|x64.ActiveCfg = Release|Any CPU + {668466AC-CD66-BAA0-0322-148549E373CB}.Release|x64.Build.0 = Release|Any CPU + {668466AC-CD66-BAA0-0322-148549E373CB}.Release|x86.ActiveCfg = Release|Any CPU + {668466AC-CD66-BAA0-0322-148549E373CB}.Release|x86.Build.0 = Release|Any CPU + {07EBBFA6-798E-76A3-CAF0-67828B00B58E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {07EBBFA6-798E-76A3-CAF0-67828B00B58E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {07EBBFA6-798E-76A3-CAF0-67828B00B58E}.Debug|x64.ActiveCfg = Debug|Any CPU + {07EBBFA6-798E-76A3-CAF0-67828B00B58E}.Debug|x64.Build.0 = Debug|Any CPU + {07EBBFA6-798E-76A3-CAF0-67828B00B58E}.Debug|x86.ActiveCfg = Debug|Any CPU + {07EBBFA6-798E-76A3-CAF0-67828B00B58E}.Debug|x86.Build.0 = Debug|Any CPU + {07EBBFA6-798E-76A3-CAF0-67828B00B58E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {07EBBFA6-798E-76A3-CAF0-67828B00B58E}.Release|Any CPU.Build.0 = Release|Any CPU + {07EBBFA6-798E-76A3-CAF0-67828B00B58E}.Release|x64.ActiveCfg = Release|Any CPU + {07EBBFA6-798E-76A3-CAF0-67828B00B58E}.Release|x64.Build.0 = Release|Any CPU + {07EBBFA6-798E-76A3-CAF0-67828B00B58E}.Release|x86.ActiveCfg = Release|Any CPU + {07EBBFA6-798E-76A3-CAF0-67828B00B58E}.Release|x86.Build.0 = Release|Any CPU + {181ED0FE-FE20-069F-7CCF-86FF5449D7F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {181ED0FE-FE20-069F-7CCF-86FF5449D7F5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {181ED0FE-FE20-069F-7CCF-86FF5449D7F5}.Debug|x64.ActiveCfg = Debug|Any CPU + {181ED0FE-FE20-069F-7CCF-86FF5449D7F5}.Debug|x64.Build.0 = Debug|Any CPU + {181ED0FE-FE20-069F-7CCF-86FF5449D7F5}.Debug|x86.ActiveCfg = Debug|Any CPU + {181ED0FE-FE20-069F-7CCF-86FF5449D7F5}.Debug|x86.Build.0 = Debug|Any CPU + {181ED0FE-FE20-069F-7CCF-86FF5449D7F5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {181ED0FE-FE20-069F-7CCF-86FF5449D7F5}.Release|Any CPU.Build.0 = Release|Any CPU + {181ED0FE-FE20-069F-7CCF-86FF5449D7F5}.Release|x64.ActiveCfg = Release|Any CPU + {181ED0FE-FE20-069F-7CCF-86FF5449D7F5}.Release|x64.Build.0 = Release|Any CPU + {181ED0FE-FE20-069F-7CCF-86FF5449D7F5}.Release|x86.ActiveCfg = Release|Any CPU + {181ED0FE-FE20-069F-7CCF-86FF5449D7F5}.Release|x86.Build.0 = Release|Any CPU + {5E683B7C-B584-0E56-C8D6-D29050DE70FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5E683B7C-B584-0E56-C8D6-D29050DE70FB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5E683B7C-B584-0E56-C8D6-D29050DE70FB}.Debug|x64.ActiveCfg = Debug|Any CPU + {5E683B7C-B584-0E56-C8D6-D29050DE70FB}.Debug|x64.Build.0 = Debug|Any CPU + {5E683B7C-B584-0E56-C8D6-D29050DE70FB}.Debug|x86.ActiveCfg = Debug|Any CPU + {5E683B7C-B584-0E56-C8D6-D29050DE70FB}.Debug|x86.Build.0 = Debug|Any CPU + {5E683B7C-B584-0E56-C8D6-D29050DE70FB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5E683B7C-B584-0E56-C8D6-D29050DE70FB}.Release|Any CPU.Build.0 = Release|Any CPU + {5E683B7C-B584-0E56-C8D6-D29050DE70FB}.Release|x64.ActiveCfg = Release|Any CPU + {5E683B7C-B584-0E56-C8D6-D29050DE70FB}.Release|x64.Build.0 = Release|Any CPU + {5E683B7C-B584-0E56-C8D6-D29050DE70FB}.Release|x86.ActiveCfg = Release|Any CPU + {5E683B7C-B584-0E56-C8D6-D29050DE70FB}.Release|x86.Build.0 = Release|Any CPU + {4163E755-1563-6A72-60E7-BB2B69F5ABA2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4163E755-1563-6A72-60E7-BB2B69F5ABA2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4163E755-1563-6A72-60E7-BB2B69F5ABA2}.Debug|x64.ActiveCfg = Debug|Any CPU + {4163E755-1563-6A72-60E7-BB2B69F5ABA2}.Debug|x64.Build.0 = Debug|Any CPU + {4163E755-1563-6A72-60E7-BB2B69F5ABA2}.Debug|x86.ActiveCfg = Debug|Any CPU + {4163E755-1563-6A72-60E7-BB2B69F5ABA2}.Debug|x86.Build.0 = Debug|Any CPU + {4163E755-1563-6A72-60E7-BB2B69F5ABA2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4163E755-1563-6A72-60E7-BB2B69F5ABA2}.Release|Any CPU.Build.0 = Release|Any CPU + {4163E755-1563-6A72-60E7-BB2B69F5ABA2}.Release|x64.ActiveCfg = Release|Any CPU + {4163E755-1563-6A72-60E7-BB2B69F5ABA2}.Release|x64.Build.0 = Release|Any CPU + {4163E755-1563-6A72-60E7-BB2B69F5ABA2}.Release|x86.ActiveCfg = Release|Any CPU + {4163E755-1563-6A72-60E7-BB2B69F5ABA2}.Release|x86.Build.0 = Release|Any CPU + {AE6F3DA7-2993-6926-323E-A29295D55C36}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AE6F3DA7-2993-6926-323E-A29295D55C36}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AE6F3DA7-2993-6926-323E-A29295D55C36}.Debug|x64.ActiveCfg = Debug|Any CPU + {AE6F3DA7-2993-6926-323E-A29295D55C36}.Debug|x64.Build.0 = Debug|Any CPU + {AE6F3DA7-2993-6926-323E-A29295D55C36}.Debug|x86.ActiveCfg = Debug|Any CPU + {AE6F3DA7-2993-6926-323E-A29295D55C36}.Debug|x86.Build.0 = Debug|Any CPU + {AE6F3DA7-2993-6926-323E-A29295D55C36}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AE6F3DA7-2993-6926-323E-A29295D55C36}.Release|Any CPU.Build.0 = Release|Any CPU + {AE6F3DA7-2993-6926-323E-A29295D55C36}.Release|x64.ActiveCfg = Release|Any CPU + {AE6F3DA7-2993-6926-323E-A29295D55C36}.Release|x64.Build.0 = Release|Any CPU + {AE6F3DA7-2993-6926-323E-A29295D55C36}.Release|x86.ActiveCfg = Release|Any CPU + {AE6F3DA7-2993-6926-323E-A29295D55C36}.Release|x86.Build.0 = Release|Any CPU + {D013641A-8457-6215-05A1-74BB57B58409}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D013641A-8457-6215-05A1-74BB57B58409}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D013641A-8457-6215-05A1-74BB57B58409}.Debug|x64.ActiveCfg = Debug|Any CPU + {D013641A-8457-6215-05A1-74BB57B58409}.Debug|x64.Build.0 = Debug|Any CPU + {D013641A-8457-6215-05A1-74BB57B58409}.Debug|x86.ActiveCfg = Debug|Any CPU + {D013641A-8457-6215-05A1-74BB57B58409}.Debug|x86.Build.0 = Debug|Any CPU + {D013641A-8457-6215-05A1-74BB57B58409}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D013641A-8457-6215-05A1-74BB57B58409}.Release|Any CPU.Build.0 = Release|Any CPU + {D013641A-8457-6215-05A1-74BB57B58409}.Release|x64.ActiveCfg = Release|Any CPU + {D013641A-8457-6215-05A1-74BB57B58409}.Release|x64.Build.0 = Release|Any CPU + {D013641A-8457-6215-05A1-74BB57B58409}.Release|x86.ActiveCfg = Release|Any CPU + {D013641A-8457-6215-05A1-74BB57B58409}.Release|x86.Build.0 = Release|Any CPU + {4FC29140-9C5F-8DD5-B405-6982BD1DA5A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4FC29140-9C5F-8DD5-B405-6982BD1DA5A3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4FC29140-9C5F-8DD5-B405-6982BD1DA5A3}.Debug|x64.ActiveCfg = Debug|Any CPU + {4FC29140-9C5F-8DD5-B405-6982BD1DA5A3}.Debug|x64.Build.0 = Debug|Any CPU + {4FC29140-9C5F-8DD5-B405-6982BD1DA5A3}.Debug|x86.ActiveCfg = Debug|Any CPU + {4FC29140-9C5F-8DD5-B405-6982BD1DA5A3}.Debug|x86.Build.0 = Debug|Any CPU + {4FC29140-9C5F-8DD5-B405-6982BD1DA5A3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4FC29140-9C5F-8DD5-B405-6982BD1DA5A3}.Release|Any CPU.Build.0 = Release|Any CPU + {4FC29140-9C5F-8DD5-B405-6982BD1DA5A3}.Release|x64.ActiveCfg = Release|Any CPU + {4FC29140-9C5F-8DD5-B405-6982BD1DA5A3}.Release|x64.Build.0 = Release|Any CPU + {4FC29140-9C5F-8DD5-B405-6982BD1DA5A3}.Release|x86.ActiveCfg = Release|Any CPU + {4FC29140-9C5F-8DD5-B405-6982BD1DA5A3}.Release|x86.Build.0 = Release|Any CPU + {B9C9A1E4-3BB8-C8BE-7819-660A582D2952}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B9C9A1E4-3BB8-C8BE-7819-660A582D2952}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B9C9A1E4-3BB8-C8BE-7819-660A582D2952}.Debug|x64.ActiveCfg = Debug|Any CPU + {B9C9A1E4-3BB8-C8BE-7819-660A582D2952}.Debug|x64.Build.0 = Debug|Any CPU + {B9C9A1E4-3BB8-C8BE-7819-660A582D2952}.Debug|x86.ActiveCfg = Debug|Any CPU + {B9C9A1E4-3BB8-C8BE-7819-660A582D2952}.Debug|x86.Build.0 = Debug|Any CPU + {B9C9A1E4-3BB8-C8BE-7819-660A582D2952}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B9C9A1E4-3BB8-C8BE-7819-660A582D2952}.Release|Any CPU.Build.0 = Release|Any CPU + {B9C9A1E4-3BB8-C8BE-7819-660A582D2952}.Release|x64.ActiveCfg = Release|Any CPU + {B9C9A1E4-3BB8-C8BE-7819-660A582D2952}.Release|x64.Build.0 = Release|Any CPU + {B9C9A1E4-3BB8-C8BE-7819-660A582D2952}.Release|x86.ActiveCfg = Release|Any CPU + {B9C9A1E4-3BB8-C8BE-7819-660A582D2952}.Release|x86.Build.0 = Release|Any CPU + {2BBAB3B4-2E18-F945-F7AB-6207D7F72714}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2BBAB3B4-2E18-F945-F7AB-6207D7F72714}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2BBAB3B4-2E18-F945-F7AB-6207D7F72714}.Debug|x64.ActiveCfg = Debug|Any CPU + {2BBAB3B4-2E18-F945-F7AB-6207D7F72714}.Debug|x64.Build.0 = Debug|Any CPU + {2BBAB3B4-2E18-F945-F7AB-6207D7F72714}.Debug|x86.ActiveCfg = Debug|Any CPU + {2BBAB3B4-2E18-F945-F7AB-6207D7F72714}.Debug|x86.Build.0 = Debug|Any CPU + {2BBAB3B4-2E18-F945-F7AB-6207D7F72714}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2BBAB3B4-2E18-F945-F7AB-6207D7F72714}.Release|Any CPU.Build.0 = Release|Any CPU + {2BBAB3B4-2E18-F945-F7AB-6207D7F72714}.Release|x64.ActiveCfg = Release|Any CPU + {2BBAB3B4-2E18-F945-F7AB-6207D7F72714}.Release|x64.Build.0 = Release|Any CPU + {2BBAB3B4-2E18-F945-F7AB-6207D7F72714}.Release|x86.ActiveCfg = Release|Any CPU + {2BBAB3B4-2E18-F945-F7AB-6207D7F72714}.Release|x86.Build.0 = Release|Any CPU + {BA492274-A505-BCD5-3DA5-EE0C94DD5748}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BA492274-A505-BCD5-3DA5-EE0C94DD5748}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BA492274-A505-BCD5-3DA5-EE0C94DD5748}.Debug|x64.ActiveCfg = Debug|Any CPU + {BA492274-A505-BCD5-3DA5-EE0C94DD5748}.Debug|x64.Build.0 = Debug|Any CPU + {BA492274-A505-BCD5-3DA5-EE0C94DD5748}.Debug|x86.ActiveCfg = Debug|Any CPU + {BA492274-A505-BCD5-3DA5-EE0C94DD5748}.Debug|x86.Build.0 = Debug|Any CPU + {BA492274-A505-BCD5-3DA5-EE0C94DD5748}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BA492274-A505-BCD5-3DA5-EE0C94DD5748}.Release|Any CPU.Build.0 = Release|Any CPU + {BA492274-A505-BCD5-3DA5-EE0C94DD5748}.Release|x64.ActiveCfg = Release|Any CPU + {BA492274-A505-BCD5-3DA5-EE0C94DD5748}.Release|x64.Build.0 = Release|Any CPU + {BA492274-A505-BCD5-3DA5-EE0C94DD5748}.Release|x86.ActiveCfg = Release|Any CPU + {BA492274-A505-BCD5-3DA5-EE0C94DD5748}.Release|x86.Build.0 = Release|Any CPU + {029F8300-57F5-9CCD-505E-708937686679}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {029F8300-57F5-9CCD-505E-708937686679}.Debug|Any CPU.Build.0 = Debug|Any CPU + {029F8300-57F5-9CCD-505E-708937686679}.Debug|x64.ActiveCfg = Debug|Any CPU + {029F8300-57F5-9CCD-505E-708937686679}.Debug|x64.Build.0 = Debug|Any CPU + {029F8300-57F5-9CCD-505E-708937686679}.Debug|x86.ActiveCfg = Debug|Any CPU + {029F8300-57F5-9CCD-505E-708937686679}.Debug|x86.Build.0 = Debug|Any CPU + {029F8300-57F5-9CCD-505E-708937686679}.Release|Any CPU.ActiveCfg = Release|Any CPU + {029F8300-57F5-9CCD-505E-708937686679}.Release|Any CPU.Build.0 = Release|Any CPU + {029F8300-57F5-9CCD-505E-708937686679}.Release|x64.ActiveCfg = Release|Any CPU + {029F8300-57F5-9CCD-505E-708937686679}.Release|x64.Build.0 = Release|Any CPU + {029F8300-57F5-9CCD-505E-708937686679}.Release|x86.ActiveCfg = Release|Any CPU + {029F8300-57F5-9CCD-505E-708937686679}.Release|x86.Build.0 = Release|Any CPU + {A5D2DB78-8045-29AC-E4B1-66E72F2C7FF0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A5D2DB78-8045-29AC-E4B1-66E72F2C7FF0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A5D2DB78-8045-29AC-E4B1-66E72F2C7FF0}.Debug|x64.ActiveCfg = Debug|Any CPU + {A5D2DB78-8045-29AC-E4B1-66E72F2C7FF0}.Debug|x64.Build.0 = Debug|Any CPU + {A5D2DB78-8045-29AC-E4B1-66E72F2C7FF0}.Debug|x86.ActiveCfg = Debug|Any CPU + {A5D2DB78-8045-29AC-E4B1-66E72F2C7FF0}.Debug|x86.Build.0 = Debug|Any CPU + {A5D2DB78-8045-29AC-E4B1-66E72F2C7FF0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A5D2DB78-8045-29AC-E4B1-66E72F2C7FF0}.Release|Any CPU.Build.0 = Release|Any CPU + {A5D2DB78-8045-29AC-E4B1-66E72F2C7FF0}.Release|x64.ActiveCfg = Release|Any CPU + {A5D2DB78-8045-29AC-E4B1-66E72F2C7FF0}.Release|x64.Build.0 = Release|Any CPU + {A5D2DB78-8045-29AC-E4B1-66E72F2C7FF0}.Release|x86.ActiveCfg = Release|Any CPU + {A5D2DB78-8045-29AC-E4B1-66E72F2C7FF0}.Release|x86.Build.0 = Release|Any CPU + {294792C0-DC28-3C5D-2D59-33DC99CD6C61}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {294792C0-DC28-3C5D-2D59-33DC99CD6C61}.Debug|Any CPU.Build.0 = Debug|Any CPU + {294792C0-DC28-3C5D-2D59-33DC99CD6C61}.Debug|x64.ActiveCfg = Debug|Any CPU + {294792C0-DC28-3C5D-2D59-33DC99CD6C61}.Debug|x64.Build.0 = Debug|Any CPU + {294792C0-DC28-3C5D-2D59-33DC99CD6C61}.Debug|x86.ActiveCfg = Debug|Any CPU + {294792C0-DC28-3C5D-2D59-33DC99CD6C61}.Debug|x86.Build.0 = Debug|Any CPU + {294792C0-DC28-3C5D-2D59-33DC99CD6C61}.Release|Any CPU.ActiveCfg = Release|Any CPU + {294792C0-DC28-3C5D-2D59-33DC99CD6C61}.Release|Any CPU.Build.0 = Release|Any CPU + {294792C0-DC28-3C5D-2D59-33DC99CD6C61}.Release|x64.ActiveCfg = Release|Any CPU + {294792C0-DC28-3C5D-2D59-33DC99CD6C61}.Release|x64.Build.0 = Release|Any CPU + {294792C0-DC28-3C5D-2D59-33DC99CD6C61}.Release|x86.ActiveCfg = Release|Any CPU + {294792C0-DC28-3C5D-2D59-33DC99CD6C61}.Release|x86.Build.0 = Release|Any CPU + {58D8630F-C0F4-B772-8572-BCC98FF0F0D8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {58D8630F-C0F4-B772-8572-BCC98FF0F0D8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {58D8630F-C0F4-B772-8572-BCC98FF0F0D8}.Debug|x64.ActiveCfg = Debug|Any CPU + {58D8630F-C0F4-B772-8572-BCC98FF0F0D8}.Debug|x64.Build.0 = Debug|Any CPU + {58D8630F-C0F4-B772-8572-BCC98FF0F0D8}.Debug|x86.ActiveCfg = Debug|Any CPU + {58D8630F-C0F4-B772-8572-BCC98FF0F0D8}.Debug|x86.Build.0 = Debug|Any CPU + {58D8630F-C0F4-B772-8572-BCC98FF0F0D8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {58D8630F-C0F4-B772-8572-BCC98FF0F0D8}.Release|Any CPU.Build.0 = Release|Any CPU + {58D8630F-C0F4-B772-8572-BCC98FF0F0D8}.Release|x64.ActiveCfg = Release|Any CPU + {58D8630F-C0F4-B772-8572-BCC98FF0F0D8}.Release|x64.Build.0 = Release|Any CPU + {58D8630F-C0F4-B772-8572-BCC98FF0F0D8}.Release|x86.ActiveCfg = Release|Any CPU + {58D8630F-C0F4-B772-8572-BCC98FF0F0D8}.Release|x86.Build.0 = Release|Any CPU + {2B1B4954-1241-8F2E-75B6-2146D15D037B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2B1B4954-1241-8F2E-75B6-2146D15D037B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2B1B4954-1241-8F2E-75B6-2146D15D037B}.Debug|x64.ActiveCfg = Debug|Any CPU + {2B1B4954-1241-8F2E-75B6-2146D15D037B}.Debug|x64.Build.0 = Debug|Any CPU + {2B1B4954-1241-8F2E-75B6-2146D15D037B}.Debug|x86.ActiveCfg = Debug|Any CPU + {2B1B4954-1241-8F2E-75B6-2146D15D037B}.Debug|x86.Build.0 = Debug|Any CPU + {2B1B4954-1241-8F2E-75B6-2146D15D037B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2B1B4954-1241-8F2E-75B6-2146D15D037B}.Release|Any CPU.Build.0 = Release|Any CPU + {2B1B4954-1241-8F2E-75B6-2146D15D037B}.Release|x64.ActiveCfg = Release|Any CPU + {2B1B4954-1241-8F2E-75B6-2146D15D037B}.Release|x64.Build.0 = Release|Any CPU + {2B1B4954-1241-8F2E-75B6-2146D15D037B}.Release|x86.ActiveCfg = Release|Any CPU + {2B1B4954-1241-8F2E-75B6-2146D15D037B}.Release|x86.Build.0 = Release|Any CPU + {97A9C869-F385-6711-6B76-F3859C86DCAC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {97A9C869-F385-6711-6B76-F3859C86DCAC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {97A9C869-F385-6711-6B76-F3859C86DCAC}.Debug|x64.ActiveCfg = Debug|Any CPU + {97A9C869-F385-6711-6B76-F3859C86DCAC}.Debug|x64.Build.0 = Debug|Any CPU + {97A9C869-F385-6711-6B76-F3859C86DCAC}.Debug|x86.ActiveCfg = Debug|Any CPU + {97A9C869-F385-6711-6B76-F3859C86DCAC}.Debug|x86.Build.0 = Debug|Any CPU + {97A9C869-F385-6711-6B76-F3859C86DCAC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {97A9C869-F385-6711-6B76-F3859C86DCAC}.Release|Any CPU.Build.0 = Release|Any CPU + {97A9C869-F385-6711-6B76-F3859C86DCAC}.Release|x64.ActiveCfg = Release|Any CPU + {97A9C869-F385-6711-6B76-F3859C86DCAC}.Release|x64.Build.0 = Release|Any CPU + {97A9C869-F385-6711-6B76-F3859C86DCAC}.Release|x86.ActiveCfg = Release|Any CPU + {97A9C869-F385-6711-6B76-F3859C86DCAC}.Release|x86.Build.0 = Release|Any CPU + {201CE292-0186-2A38-55D7-69890B5817DF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {201CE292-0186-2A38-55D7-69890B5817DF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {201CE292-0186-2A38-55D7-69890B5817DF}.Debug|x64.ActiveCfg = Debug|Any CPU + {201CE292-0186-2A38-55D7-69890B5817DF}.Debug|x64.Build.0 = Debug|Any CPU + {201CE292-0186-2A38-55D7-69890B5817DF}.Debug|x86.ActiveCfg = Debug|Any CPU + {201CE292-0186-2A38-55D7-69890B5817DF}.Debug|x86.Build.0 = Debug|Any CPU + {201CE292-0186-2A38-55D7-69890B5817DF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {201CE292-0186-2A38-55D7-69890B5817DF}.Release|Any CPU.Build.0 = Release|Any CPU + {201CE292-0186-2A38-55D7-69890B5817DF}.Release|x64.ActiveCfg = Release|Any CPU + {201CE292-0186-2A38-55D7-69890B5817DF}.Release|x64.Build.0 = Release|Any CPU + {201CE292-0186-2A38-55D7-69890B5817DF}.Release|x86.ActiveCfg = Release|Any CPU + {201CE292-0186-2A38-55D7-69890B5817DF}.Release|x86.Build.0 = Release|Any CPU + {17A00031-9FF7-4F73-5319-23FA5817625F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {17A00031-9FF7-4F73-5319-23FA5817625F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {17A00031-9FF7-4F73-5319-23FA5817625F}.Debug|x64.ActiveCfg = Debug|Any CPU + {17A00031-9FF7-4F73-5319-23FA5817625F}.Debug|x64.Build.0 = Debug|Any CPU + {17A00031-9FF7-4F73-5319-23FA5817625F}.Debug|x86.ActiveCfg = Debug|Any CPU + {17A00031-9FF7-4F73-5319-23FA5817625F}.Debug|x86.Build.0 = Debug|Any CPU + {17A00031-9FF7-4F73-5319-23FA5817625F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {17A00031-9FF7-4F73-5319-23FA5817625F}.Release|Any CPU.Build.0 = Release|Any CPU + {17A00031-9FF7-4F73-5319-23FA5817625F}.Release|x64.ActiveCfg = Release|Any CPU + {17A00031-9FF7-4F73-5319-23FA5817625F}.Release|x64.Build.0 = Release|Any CPU + {17A00031-9FF7-4F73-5319-23FA5817625F}.Release|x86.ActiveCfg = Release|Any CPU + {17A00031-9FF7-4F73-5319-23FA5817625F}.Release|x86.Build.0 = Release|Any CPU + {11E0E129-091F-BEEB-A1B2-9BAEF5BE9FBC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {11E0E129-091F-BEEB-A1B2-9BAEF5BE9FBC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {11E0E129-091F-BEEB-A1B2-9BAEF5BE9FBC}.Debug|x64.ActiveCfg = Debug|Any CPU + {11E0E129-091F-BEEB-A1B2-9BAEF5BE9FBC}.Debug|x64.Build.0 = Debug|Any CPU + {11E0E129-091F-BEEB-A1B2-9BAEF5BE9FBC}.Debug|x86.ActiveCfg = Debug|Any CPU + {11E0E129-091F-BEEB-A1B2-9BAEF5BE9FBC}.Debug|x86.Build.0 = Debug|Any CPU + {11E0E129-091F-BEEB-A1B2-9BAEF5BE9FBC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {11E0E129-091F-BEEB-A1B2-9BAEF5BE9FBC}.Release|Any CPU.Build.0 = Release|Any CPU + {11E0E129-091F-BEEB-A1B2-9BAEF5BE9FBC}.Release|x64.ActiveCfg = Release|Any CPU + {11E0E129-091F-BEEB-A1B2-9BAEF5BE9FBC}.Release|x64.Build.0 = Release|Any CPU + {11E0E129-091F-BEEB-A1B2-9BAEF5BE9FBC}.Release|x86.ActiveCfg = Release|Any CPU + {11E0E129-091F-BEEB-A1B2-9BAEF5BE9FBC}.Release|x86.Build.0 = Release|Any CPU + {AEF63403-4889-5396-CDEA-3B713CEF2ED7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AEF63403-4889-5396-CDEA-3B713CEF2ED7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AEF63403-4889-5396-CDEA-3B713CEF2ED7}.Debug|x64.ActiveCfg = Debug|Any CPU + {AEF63403-4889-5396-CDEA-3B713CEF2ED7}.Debug|x64.Build.0 = Debug|Any CPU + {AEF63403-4889-5396-CDEA-3B713CEF2ED7}.Debug|x86.ActiveCfg = Debug|Any CPU + {AEF63403-4889-5396-CDEA-3B713CEF2ED7}.Debug|x86.Build.0 = Debug|Any CPU + {AEF63403-4889-5396-CDEA-3B713CEF2ED7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AEF63403-4889-5396-CDEA-3B713CEF2ED7}.Release|Any CPU.Build.0 = Release|Any CPU + {AEF63403-4889-5396-CDEA-3B713CEF2ED7}.Release|x64.ActiveCfg = Release|Any CPU + {AEF63403-4889-5396-CDEA-3B713CEF2ED7}.Release|x64.Build.0 = Release|Any CPU + {AEF63403-4889-5396-CDEA-3B713CEF2ED7}.Release|x86.ActiveCfg = Release|Any CPU + {AEF63403-4889-5396-CDEA-3B713CEF2ED7}.Release|x86.Build.0 = Release|Any CPU + {D24E7862-3930-A4F6-1DFA-DA88C759546C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D24E7862-3930-A4F6-1DFA-DA88C759546C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D24E7862-3930-A4F6-1DFA-DA88C759546C}.Debug|x64.ActiveCfg = Debug|Any CPU + {D24E7862-3930-A4F6-1DFA-DA88C759546C}.Debug|x64.Build.0 = Debug|Any CPU + {D24E7862-3930-A4F6-1DFA-DA88C759546C}.Debug|x86.ActiveCfg = Debug|Any CPU + {D24E7862-3930-A4F6-1DFA-DA88C759546C}.Debug|x86.Build.0 = Debug|Any CPU + {D24E7862-3930-A4F6-1DFA-DA88C759546C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D24E7862-3930-A4F6-1DFA-DA88C759546C}.Release|Any CPU.Build.0 = Release|Any CPU + {D24E7862-3930-A4F6-1DFA-DA88C759546C}.Release|x64.ActiveCfg = Release|Any CPU + {D24E7862-3930-A4F6-1DFA-DA88C759546C}.Release|x64.Build.0 = Release|Any CPU + {D24E7862-3930-A4F6-1DFA-DA88C759546C}.Release|x86.ActiveCfg = Release|Any CPU + {D24E7862-3930-A4F6-1DFA-DA88C759546C}.Release|x86.Build.0 = Release|Any CPU + {6DC62619-949E-92E6-F4F1-5A0320959929}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6DC62619-949E-92E6-F4F1-5A0320959929}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6DC62619-949E-92E6-F4F1-5A0320959929}.Debug|x64.ActiveCfg = Debug|Any CPU + {6DC62619-949E-92E6-F4F1-5A0320959929}.Debug|x64.Build.0 = Debug|Any CPU + {6DC62619-949E-92E6-F4F1-5A0320959929}.Debug|x86.ActiveCfg = Debug|Any CPU + {6DC62619-949E-92E6-F4F1-5A0320959929}.Debug|x86.Build.0 = Debug|Any CPU + {6DC62619-949E-92E6-F4F1-5A0320959929}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6DC62619-949E-92E6-F4F1-5A0320959929}.Release|Any CPU.Build.0 = Release|Any CPU + {6DC62619-949E-92E6-F4F1-5A0320959929}.Release|x64.ActiveCfg = Release|Any CPU + {6DC62619-949E-92E6-F4F1-5A0320959929}.Release|x64.Build.0 = Release|Any CPU + {6DC62619-949E-92E6-F4F1-5A0320959929}.Release|x86.ActiveCfg = Release|Any CPU + {6DC62619-949E-92E6-F4F1-5A0320959929}.Release|x86.Build.0 = Release|Any CPU + {37F1D83D-073C-C165-4C53-664AD87628E6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {37F1D83D-073C-C165-4C53-664AD87628E6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {37F1D83D-073C-C165-4C53-664AD87628E6}.Debug|x64.ActiveCfg = Debug|Any CPU + {37F1D83D-073C-C165-4C53-664AD87628E6}.Debug|x64.Build.0 = Debug|Any CPU + {37F1D83D-073C-C165-4C53-664AD87628E6}.Debug|x86.ActiveCfg = Debug|Any CPU + {37F1D83D-073C-C165-4C53-664AD87628E6}.Debug|x86.Build.0 = Debug|Any CPU + {37F1D83D-073C-C165-4C53-664AD87628E6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {37F1D83D-073C-C165-4C53-664AD87628E6}.Release|Any CPU.Build.0 = Release|Any CPU + {37F1D83D-073C-C165-4C53-664AD87628E6}.Release|x64.ActiveCfg = Release|Any CPU + {37F1D83D-073C-C165-4C53-664AD87628E6}.Release|x64.Build.0 = Release|Any CPU + {37F1D83D-073C-C165-4C53-664AD87628E6}.Release|x86.ActiveCfg = Release|Any CPU + {37F1D83D-073C-C165-4C53-664AD87628E6}.Release|x86.Build.0 = Release|Any CPU + {CDC236E8-6881-46C4-EE95-3C386AF009D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CDC236E8-6881-46C4-EE95-3C386AF009D0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CDC236E8-6881-46C4-EE95-3C386AF009D0}.Debug|x64.ActiveCfg = Debug|Any CPU + {CDC236E8-6881-46C4-EE95-3C386AF009D0}.Debug|x64.Build.0 = Debug|Any CPU + {CDC236E8-6881-46C4-EE95-3C386AF009D0}.Debug|x86.ActiveCfg = Debug|Any CPU + {CDC236E8-6881-46C4-EE95-3C386AF009D0}.Debug|x86.Build.0 = Debug|Any CPU + {CDC236E8-6881-46C4-EE95-3C386AF009D0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CDC236E8-6881-46C4-EE95-3C386AF009D0}.Release|Any CPU.Build.0 = Release|Any CPU + {CDC236E8-6881-46C4-EE95-3C386AF009D0}.Release|x64.ActiveCfg = Release|Any CPU + {CDC236E8-6881-46C4-EE95-3C386AF009D0}.Release|x64.Build.0 = Release|Any CPU + {CDC236E8-6881-46C4-EE95-3C386AF009D0}.Release|x86.ActiveCfg = Release|Any CPU + {CDC236E8-6881-46C4-EE95-3C386AF009D0}.Release|x86.Build.0 = Release|Any CPU + {ACC2785F-F4B9-13E4-EED2-C5D067242175}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {ACC2785F-F4B9-13E4-EED2-C5D067242175}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ACC2785F-F4B9-13E4-EED2-C5D067242175}.Debug|x64.ActiveCfg = Debug|Any CPU + {ACC2785F-F4B9-13E4-EED2-C5D067242175}.Debug|x64.Build.0 = Debug|Any CPU + {ACC2785F-F4B9-13E4-EED2-C5D067242175}.Debug|x86.ActiveCfg = Debug|Any CPU + {ACC2785F-F4B9-13E4-EED2-C5D067242175}.Debug|x86.Build.0 = Debug|Any CPU + {ACC2785F-F4B9-13E4-EED2-C5D067242175}.Release|Any CPU.ActiveCfg = Release|Any CPU + {ACC2785F-F4B9-13E4-EED2-C5D067242175}.Release|Any CPU.Build.0 = Release|Any CPU + {ACC2785F-F4B9-13E4-EED2-C5D067242175}.Release|x64.ActiveCfg = Release|Any CPU + {ACC2785F-F4B9-13E4-EED2-C5D067242175}.Release|x64.Build.0 = Release|Any CPU + {ACC2785F-F4B9-13E4-EED2-C5D067242175}.Release|x86.ActiveCfg = Release|Any CPU + {ACC2785F-F4B9-13E4-EED2-C5D067242175}.Release|x86.Build.0 = Release|Any CPU + {7F4A3CA3-C3DA-A698-5CBC-54F28D1C7DFB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7F4A3CA3-C3DA-A698-5CBC-54F28D1C7DFB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7F4A3CA3-C3DA-A698-5CBC-54F28D1C7DFB}.Debug|x64.ActiveCfg = Debug|Any CPU + {7F4A3CA3-C3DA-A698-5CBC-54F28D1C7DFB}.Debug|x64.Build.0 = Debug|Any CPU + {7F4A3CA3-C3DA-A698-5CBC-54F28D1C7DFB}.Debug|x86.ActiveCfg = Debug|Any CPU + {7F4A3CA3-C3DA-A698-5CBC-54F28D1C7DFB}.Debug|x86.Build.0 = Debug|Any CPU + {7F4A3CA3-C3DA-A698-5CBC-54F28D1C7DFB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7F4A3CA3-C3DA-A698-5CBC-54F28D1C7DFB}.Release|Any CPU.Build.0 = Release|Any CPU + {7F4A3CA3-C3DA-A698-5CBC-54F28D1C7DFB}.Release|x64.ActiveCfg = Release|Any CPU + {7F4A3CA3-C3DA-A698-5CBC-54F28D1C7DFB}.Release|x64.Build.0 = Release|Any CPU + {7F4A3CA3-C3DA-A698-5CBC-54F28D1C7DFB}.Release|x86.ActiveCfg = Release|Any CPU + {7F4A3CA3-C3DA-A698-5CBC-54F28D1C7DFB}.Release|x86.Build.0 = Release|Any CPU + {DAA1F516-DEC3-7FF2-3A63-78DD97BA062C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DAA1F516-DEC3-7FF2-3A63-78DD97BA062C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DAA1F516-DEC3-7FF2-3A63-78DD97BA062C}.Debug|x64.ActiveCfg = Debug|Any CPU + {DAA1F516-DEC3-7FF2-3A63-78DD97BA062C}.Debug|x64.Build.0 = Debug|Any CPU + {DAA1F516-DEC3-7FF2-3A63-78DD97BA062C}.Debug|x86.ActiveCfg = Debug|Any CPU + {DAA1F516-DEC3-7FF2-3A63-78DD97BA062C}.Debug|x86.Build.0 = Debug|Any CPU + {DAA1F516-DEC3-7FF2-3A63-78DD97BA062C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DAA1F516-DEC3-7FF2-3A63-78DD97BA062C}.Release|Any CPU.Build.0 = Release|Any CPU + {DAA1F516-DEC3-7FF2-3A63-78DD97BA062C}.Release|x64.ActiveCfg = Release|Any CPU + {DAA1F516-DEC3-7FF2-3A63-78DD97BA062C}.Release|x64.Build.0 = Release|Any CPU + {DAA1F516-DEC3-7FF2-3A63-78DD97BA062C}.Release|x86.ActiveCfg = Release|Any CPU + {DAA1F516-DEC3-7FF2-3A63-78DD97BA062C}.Release|x86.Build.0 = Release|Any CPU + {11EF0DE9-2648-F711-6194-70B5C40B3F3F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {11EF0DE9-2648-F711-6194-70B5C40B3F3F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {11EF0DE9-2648-F711-6194-70B5C40B3F3F}.Debug|x64.ActiveCfg = Debug|Any CPU + {11EF0DE9-2648-F711-6194-70B5C40B3F3F}.Debug|x64.Build.0 = Debug|Any CPU + {11EF0DE9-2648-F711-6194-70B5C40B3F3F}.Debug|x86.ActiveCfg = Debug|Any CPU + {11EF0DE9-2648-F711-6194-70B5C40B3F3F}.Debug|x86.Build.0 = Debug|Any CPU + {11EF0DE9-2648-F711-6194-70B5C40B3F3F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {11EF0DE9-2648-F711-6194-70B5C40B3F3F}.Release|Any CPU.Build.0 = Release|Any CPU + {11EF0DE9-2648-F711-6194-70B5C40B3F3F}.Release|x64.ActiveCfg = Release|Any CPU + {11EF0DE9-2648-F711-6194-70B5C40B3F3F}.Release|x64.Build.0 = Release|Any CPU + {11EF0DE9-2648-F711-6194-70B5C40B3F3F}.Release|x86.ActiveCfg = Release|Any CPU + {11EF0DE9-2648-F711-6194-70B5C40B3F3F}.Release|x86.Build.0 = Release|Any CPU + {01A21B47-07C5-6039-1B48-C5EACA3DBA2D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {01A21B47-07C5-6039-1B48-C5EACA3DBA2D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {01A21B47-07C5-6039-1B48-C5EACA3DBA2D}.Debug|x64.ActiveCfg = Debug|Any CPU + {01A21B47-07C5-6039-1B48-C5EACA3DBA2D}.Debug|x64.Build.0 = Debug|Any CPU + {01A21B47-07C5-6039-1B48-C5EACA3DBA2D}.Debug|x86.ActiveCfg = Debug|Any CPU + {01A21B47-07C5-6039-1B48-C5EACA3DBA2D}.Debug|x86.Build.0 = Debug|Any CPU + {01A21B47-07C5-6039-1B48-C5EACA3DBA2D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {01A21B47-07C5-6039-1B48-C5EACA3DBA2D}.Release|Any CPU.Build.0 = Release|Any CPU + {01A21B47-07C5-6039-1B48-C5EACA3DBA2D}.Release|x64.ActiveCfg = Release|Any CPU + {01A21B47-07C5-6039-1B48-C5EACA3DBA2D}.Release|x64.Build.0 = Release|Any CPU + {01A21B47-07C5-6039-1B48-C5EACA3DBA2D}.Release|x86.ActiveCfg = Release|Any CPU + {01A21B47-07C5-6039-1B48-C5EACA3DBA2D}.Release|x86.Build.0 = Release|Any CPU + {7CB7FEA8-8A12-A5D6-0057-AA65DB328617}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7CB7FEA8-8A12-A5D6-0057-AA65DB328617}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7CB7FEA8-8A12-A5D6-0057-AA65DB328617}.Debug|x64.ActiveCfg = Debug|Any CPU + {7CB7FEA8-8A12-A5D6-0057-AA65DB328617}.Debug|x64.Build.0 = Debug|Any CPU + {7CB7FEA8-8A12-A5D6-0057-AA65DB328617}.Debug|x86.ActiveCfg = Debug|Any CPU + {7CB7FEA8-8A12-A5D6-0057-AA65DB328617}.Debug|x86.Build.0 = Debug|Any CPU + {7CB7FEA8-8A12-A5D6-0057-AA65DB328617}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7CB7FEA8-8A12-A5D6-0057-AA65DB328617}.Release|Any CPU.Build.0 = Release|Any CPU + {7CB7FEA8-8A12-A5D6-0057-AA65DB328617}.Release|x64.ActiveCfg = Release|Any CPU + {7CB7FEA8-8A12-A5D6-0057-AA65DB328617}.Release|x64.Build.0 = Release|Any CPU + {7CB7FEA8-8A12-A5D6-0057-AA65DB328617}.Release|x86.ActiveCfg = Release|Any CPU + {7CB7FEA8-8A12-A5D6-0057-AA65DB328617}.Release|x86.Build.0 = Release|Any CPU + {0484DB46-3E40-1A10-131C-524AF1233EA7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0484DB46-3E40-1A10-131C-524AF1233EA7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0484DB46-3E40-1A10-131C-524AF1233EA7}.Debug|x64.ActiveCfg = Debug|Any CPU + {0484DB46-3E40-1A10-131C-524AF1233EA7}.Debug|x64.Build.0 = Debug|Any CPU + {0484DB46-3E40-1A10-131C-524AF1233EA7}.Debug|x86.ActiveCfg = Debug|Any CPU + {0484DB46-3E40-1A10-131C-524AF1233EA7}.Debug|x86.Build.0 = Debug|Any CPU + {0484DB46-3E40-1A10-131C-524AF1233EA7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0484DB46-3E40-1A10-131C-524AF1233EA7}.Release|Any CPU.Build.0 = Release|Any CPU + {0484DB46-3E40-1A10-131C-524AF1233EA7}.Release|x64.ActiveCfg = Release|Any CPU + {0484DB46-3E40-1A10-131C-524AF1233EA7}.Release|x64.Build.0 = Release|Any CPU + {0484DB46-3E40-1A10-131C-524AF1233EA7}.Release|x86.ActiveCfg = Release|Any CPU + {0484DB46-3E40-1A10-131C-524AF1233EA7}.Release|x86.Build.0 = Release|Any CPU + {64E1D9B1-B944-8AA3-799F-02E7DD33FB78}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {64E1D9B1-B944-8AA3-799F-02E7DD33FB78}.Debug|Any CPU.Build.0 = Debug|Any CPU + {64E1D9B1-B944-8AA3-799F-02E7DD33FB78}.Debug|x64.ActiveCfg = Debug|Any CPU + {64E1D9B1-B944-8AA3-799F-02E7DD33FB78}.Debug|x64.Build.0 = Debug|Any CPU + {64E1D9B1-B944-8AA3-799F-02E7DD33FB78}.Debug|x86.ActiveCfg = Debug|Any CPU + {64E1D9B1-B944-8AA3-799F-02E7DD33FB78}.Debug|x86.Build.0 = Debug|Any CPU + {64E1D9B1-B944-8AA3-799F-02E7DD33FB78}.Release|Any CPU.ActiveCfg = Release|Any CPU + {64E1D9B1-B944-8AA3-799F-02E7DD33FB78}.Release|Any CPU.Build.0 = Release|Any CPU + {64E1D9B1-B944-8AA3-799F-02E7DD33FB78}.Release|x64.ActiveCfg = Release|Any CPU + {64E1D9B1-B944-8AA3-799F-02E7DD33FB78}.Release|x64.Build.0 = Release|Any CPU + {64E1D9B1-B944-8AA3-799F-02E7DD33FB78}.Release|x86.ActiveCfg = Release|Any CPU + {64E1D9B1-B944-8AA3-799F-02E7DD33FB78}.Release|x86.Build.0 = Release|Any CPU + {D37991E1-585F-FF1B-9772-07477E40AF78}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D37991E1-585F-FF1B-9772-07477E40AF78}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D37991E1-585F-FF1B-9772-07477E40AF78}.Debug|x64.ActiveCfg = Debug|Any CPU + {D37991E1-585F-FF1B-9772-07477E40AF78}.Debug|x64.Build.0 = Debug|Any CPU + {D37991E1-585F-FF1B-9772-07477E40AF78}.Debug|x86.ActiveCfg = Debug|Any CPU + {D37991E1-585F-FF1B-9772-07477E40AF78}.Debug|x86.Build.0 = Debug|Any CPU + {D37991E1-585F-FF1B-9772-07477E40AF78}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D37991E1-585F-FF1B-9772-07477E40AF78}.Release|Any CPU.Build.0 = Release|Any CPU + {D37991E1-585F-FF1B-9772-07477E40AF78}.Release|x64.ActiveCfg = Release|Any CPU + {D37991E1-585F-FF1B-9772-07477E40AF78}.Release|x64.Build.0 = Release|Any CPU + {D37991E1-585F-FF1B-9772-07477E40AF78}.Release|x86.ActiveCfg = Release|Any CPU + {D37991E1-585F-FF1B-9772-07477E40AF78}.Release|x86.Build.0 = Release|Any CPU + {35A06F00-71AB-8A31-7D60-EBF41EA730CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {35A06F00-71AB-8A31-7D60-EBF41EA730CA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {35A06F00-71AB-8A31-7D60-EBF41EA730CA}.Debug|x64.ActiveCfg = Debug|Any CPU + {35A06F00-71AB-8A31-7D60-EBF41EA730CA}.Debug|x64.Build.0 = Debug|Any CPU + {35A06F00-71AB-8A31-7D60-EBF41EA730CA}.Debug|x86.ActiveCfg = Debug|Any CPU + {35A06F00-71AB-8A31-7D60-EBF41EA730CA}.Debug|x86.Build.0 = Debug|Any CPU + {35A06F00-71AB-8A31-7D60-EBF41EA730CA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {35A06F00-71AB-8A31-7D60-EBF41EA730CA}.Release|Any CPU.Build.0 = Release|Any CPU + {35A06F00-71AB-8A31-7D60-EBF41EA730CA}.Release|x64.ActiveCfg = Release|Any CPU + {35A06F00-71AB-8A31-7D60-EBF41EA730CA}.Release|x64.Build.0 = Release|Any CPU + {35A06F00-71AB-8A31-7D60-EBF41EA730CA}.Release|x86.ActiveCfg = Release|Any CPU + {35A06F00-71AB-8A31-7D60-EBF41EA730CA}.Release|x86.Build.0 = Release|Any CPU + {56120A54-1D4D-F07B-63B4-B15525C2ADD9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {56120A54-1D4D-F07B-63B4-B15525C2ADD9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {56120A54-1D4D-F07B-63B4-B15525C2ADD9}.Debug|x64.ActiveCfg = Debug|Any CPU + {56120A54-1D4D-F07B-63B4-B15525C2ADD9}.Debug|x64.Build.0 = Debug|Any CPU + {56120A54-1D4D-F07B-63B4-B15525C2ADD9}.Debug|x86.ActiveCfg = Debug|Any CPU + {56120A54-1D4D-F07B-63B4-B15525C2ADD9}.Debug|x86.Build.0 = Debug|Any CPU + {56120A54-1D4D-F07B-63B4-B15525C2ADD9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {56120A54-1D4D-F07B-63B4-B15525C2ADD9}.Release|Any CPU.Build.0 = Release|Any CPU + {56120A54-1D4D-F07B-63B4-B15525C2ADD9}.Release|x64.ActiveCfg = Release|Any CPU + {56120A54-1D4D-F07B-63B4-B15525C2ADD9}.Release|x64.Build.0 = Release|Any CPU + {56120A54-1D4D-F07B-63B4-B15525C2ADD9}.Release|x86.ActiveCfg = Release|Any CPU + {56120A54-1D4D-F07B-63B4-B15525C2ADD9}.Release|x86.Build.0 = Release|Any CPU + {BE47FB74-D163-0B1F-5293-0962EA7E8585}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BE47FB74-D163-0B1F-5293-0962EA7E8585}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BE47FB74-D163-0B1F-5293-0962EA7E8585}.Debug|x64.ActiveCfg = Debug|Any CPU + {BE47FB74-D163-0B1F-5293-0962EA7E8585}.Debug|x64.Build.0 = Debug|Any CPU + {BE47FB74-D163-0B1F-5293-0962EA7E8585}.Debug|x86.ActiveCfg = Debug|Any CPU + {BE47FB74-D163-0B1F-5293-0962EA7E8585}.Debug|x86.Build.0 = Debug|Any CPU + {BE47FB74-D163-0B1F-5293-0962EA7E8585}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BE47FB74-D163-0B1F-5293-0962EA7E8585}.Release|Any CPU.Build.0 = Release|Any CPU + {BE47FB74-D163-0B1F-5293-0962EA7E8585}.Release|x64.ActiveCfg = Release|Any CPU + {BE47FB74-D163-0B1F-5293-0962EA7E8585}.Release|x64.Build.0 = Release|Any CPU + {BE47FB74-D163-0B1F-5293-0962EA7E8585}.Release|x86.ActiveCfg = Release|Any CPU + {BE47FB74-D163-0B1F-5293-0962EA7E8585}.Release|x86.Build.0 = Release|Any CPU + {9AD932E9-0986-654C-B454-34E654C80697}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9AD932E9-0986-654C-B454-34E654C80697}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9AD932E9-0986-654C-B454-34E654C80697}.Debug|x64.ActiveCfg = Debug|Any CPU + {9AD932E9-0986-654C-B454-34E654C80697}.Debug|x64.Build.0 = Debug|Any CPU + {9AD932E9-0986-654C-B454-34E654C80697}.Debug|x86.ActiveCfg = Debug|Any CPU + {9AD932E9-0986-654C-B454-34E654C80697}.Debug|x86.Build.0 = Debug|Any CPU + {9AD932E9-0986-654C-B454-34E654C80697}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9AD932E9-0986-654C-B454-34E654C80697}.Release|Any CPU.Build.0 = Release|Any CPU + {9AD932E9-0986-654C-B454-34E654C80697}.Release|x64.ActiveCfg = Release|Any CPU + {9AD932E9-0986-654C-B454-34E654C80697}.Release|x64.Build.0 = Release|Any CPU + {9AD932E9-0986-654C-B454-34E654C80697}.Release|x86.ActiveCfg = Release|Any CPU + {9AD932E9-0986-654C-B454-34E654C80697}.Release|x86.Build.0 = Release|Any CPU + {00BE2B68-FC96-23F8-F61D-EE53B7AE06A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {00BE2B68-FC96-23F8-F61D-EE53B7AE06A1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {00BE2B68-FC96-23F8-F61D-EE53B7AE06A1}.Debug|x64.ActiveCfg = Debug|Any CPU + {00BE2B68-FC96-23F8-F61D-EE53B7AE06A1}.Debug|x64.Build.0 = Debug|Any CPU + {00BE2B68-FC96-23F8-F61D-EE53B7AE06A1}.Debug|x86.ActiveCfg = Debug|Any CPU + {00BE2B68-FC96-23F8-F61D-EE53B7AE06A1}.Debug|x86.Build.0 = Debug|Any CPU + {00BE2B68-FC96-23F8-F61D-EE53B7AE06A1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {00BE2B68-FC96-23F8-F61D-EE53B7AE06A1}.Release|Any CPU.Build.0 = Release|Any CPU + {00BE2B68-FC96-23F8-F61D-EE53B7AE06A1}.Release|x64.ActiveCfg = Release|Any CPU + {00BE2B68-FC96-23F8-F61D-EE53B7AE06A1}.Release|x64.Build.0 = Release|Any CPU + {00BE2B68-FC96-23F8-F61D-EE53B7AE06A1}.Release|x86.ActiveCfg = Release|Any CPU + {00BE2B68-FC96-23F8-F61D-EE53B7AE06A1}.Release|x86.Build.0 = Release|Any CPU + {570BA050-81A7-46EB-3DDD-422027EE2CA2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {570BA050-81A7-46EB-3DDD-422027EE2CA2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {570BA050-81A7-46EB-3DDD-422027EE2CA2}.Debug|x64.ActiveCfg = Debug|Any CPU + {570BA050-81A7-46EB-3DDD-422027EE2CA2}.Debug|x64.Build.0 = Debug|Any CPU + {570BA050-81A7-46EB-3DDD-422027EE2CA2}.Debug|x86.ActiveCfg = Debug|Any CPU + {570BA050-81A7-46EB-3DDD-422027EE2CA2}.Debug|x86.Build.0 = Debug|Any CPU + {570BA050-81A7-46EB-3DDD-422027EE2CA2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {570BA050-81A7-46EB-3DDD-422027EE2CA2}.Release|Any CPU.Build.0 = Release|Any CPU + {570BA050-81A7-46EB-3DDD-422027EE2CA2}.Release|x64.ActiveCfg = Release|Any CPU + {570BA050-81A7-46EB-3DDD-422027EE2CA2}.Release|x64.Build.0 = Release|Any CPU + {570BA050-81A7-46EB-3DDD-422027EE2CA2}.Release|x86.ActiveCfg = Release|Any CPU + {570BA050-81A7-46EB-3DDD-422027EE2CA2}.Release|x86.Build.0 = Release|Any CPU + {6C43FD78-3478-F245-3EE4-E410D1E7D7C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6C43FD78-3478-F245-3EE4-E410D1E7D7C5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6C43FD78-3478-F245-3EE4-E410D1E7D7C5}.Debug|x64.ActiveCfg = Debug|Any CPU + {6C43FD78-3478-F245-3EE4-E410D1E7D7C5}.Debug|x64.Build.0 = Debug|Any CPU + {6C43FD78-3478-F245-3EE4-E410D1E7D7C5}.Debug|x86.ActiveCfg = Debug|Any CPU + {6C43FD78-3478-F245-3EE4-E410D1E7D7C5}.Debug|x86.Build.0 = Debug|Any CPU + {6C43FD78-3478-F245-3EE4-E410D1E7D7C5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6C43FD78-3478-F245-3EE4-E410D1E7D7C5}.Release|Any CPU.Build.0 = Release|Any CPU + {6C43FD78-3478-F245-3EE4-E410D1E7D7C5}.Release|x64.ActiveCfg = Release|Any CPU + {6C43FD78-3478-F245-3EE4-E410D1E7D7C5}.Release|x64.Build.0 = Release|Any CPU + {6C43FD78-3478-F245-3EE4-E410D1E7D7C5}.Release|x86.ActiveCfg = Release|Any CPU + {6C43FD78-3478-F245-3EE4-E410D1E7D7C5}.Release|x86.Build.0 = Release|Any CPU + {7F0FFA06-EAC8-CC9A-3386-389638F12B59}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7F0FFA06-EAC8-CC9A-3386-389638F12B59}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7F0FFA06-EAC8-CC9A-3386-389638F12B59}.Debug|x64.ActiveCfg = Debug|Any CPU + {7F0FFA06-EAC8-CC9A-3386-389638F12B59}.Debug|x64.Build.0 = Debug|Any CPU + {7F0FFA06-EAC8-CC9A-3386-389638F12B59}.Debug|x86.ActiveCfg = Debug|Any CPU + {7F0FFA06-EAC8-CC9A-3386-389638F12B59}.Debug|x86.Build.0 = Debug|Any CPU + {7F0FFA06-EAC8-CC9A-3386-389638F12B59}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7F0FFA06-EAC8-CC9A-3386-389638F12B59}.Release|Any CPU.Build.0 = Release|Any CPU + {7F0FFA06-EAC8-CC9A-3386-389638F12B59}.Release|x64.ActiveCfg = Release|Any CPU + {7F0FFA06-EAC8-CC9A-3386-389638F12B59}.Release|x64.Build.0 = Release|Any CPU + {7F0FFA06-EAC8-CC9A-3386-389638F12B59}.Release|x86.ActiveCfg = Release|Any CPU + {7F0FFA06-EAC8-CC9A-3386-389638F12B59}.Release|x86.Build.0 = Release|Any CPU + {03B9D4BE-348B-9AD6-6FE9-6C40AE22053D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {03B9D4BE-348B-9AD6-6FE9-6C40AE22053D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {03B9D4BE-348B-9AD6-6FE9-6C40AE22053D}.Debug|x64.ActiveCfg = Debug|Any CPU + {03B9D4BE-348B-9AD6-6FE9-6C40AE22053D}.Debug|x64.Build.0 = Debug|Any CPU + {03B9D4BE-348B-9AD6-6FE9-6C40AE22053D}.Debug|x86.ActiveCfg = Debug|Any CPU + {03B9D4BE-348B-9AD6-6FE9-6C40AE22053D}.Debug|x86.Build.0 = Debug|Any CPU + {03B9D4BE-348B-9AD6-6FE9-6C40AE22053D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {03B9D4BE-348B-9AD6-6FE9-6C40AE22053D}.Release|Any CPU.Build.0 = Release|Any CPU + {03B9D4BE-348B-9AD6-6FE9-6C40AE22053D}.Release|x64.ActiveCfg = Release|Any CPU + {03B9D4BE-348B-9AD6-6FE9-6C40AE22053D}.Release|x64.Build.0 = Release|Any CPU + {03B9D4BE-348B-9AD6-6FE9-6C40AE22053D}.Release|x86.ActiveCfg = Release|Any CPU + {03B9D4BE-348B-9AD6-6FE9-6C40AE22053D}.Release|x86.Build.0 = Release|Any CPU + {35CF4CF2-8A84-378D-32F0-572F4AA900A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {35CF4CF2-8A84-378D-32F0-572F4AA900A3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {35CF4CF2-8A84-378D-32F0-572F4AA900A3}.Debug|x64.ActiveCfg = Debug|Any CPU + {35CF4CF2-8A84-378D-32F0-572F4AA900A3}.Debug|x64.Build.0 = Debug|Any CPU + {35CF4CF2-8A84-378D-32F0-572F4AA900A3}.Debug|x86.ActiveCfg = Debug|Any CPU + {35CF4CF2-8A84-378D-32F0-572F4AA900A3}.Debug|x86.Build.0 = Debug|Any CPU + {35CF4CF2-8A84-378D-32F0-572F4AA900A3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {35CF4CF2-8A84-378D-32F0-572F4AA900A3}.Release|Any CPU.Build.0 = Release|Any CPU + {35CF4CF2-8A84-378D-32F0-572F4AA900A3}.Release|x64.ActiveCfg = Release|Any CPU + {35CF4CF2-8A84-378D-32F0-572F4AA900A3}.Release|x64.Build.0 = Release|Any CPU + {35CF4CF2-8A84-378D-32F0-572F4AA900A3}.Release|x86.ActiveCfg = Release|Any CPU + {35CF4CF2-8A84-378D-32F0-572F4AA900A3}.Release|x86.Build.0 = Release|Any CPU + {13E03C69-0634-3330-26D9-DCF7DD136BC5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {13E03C69-0634-3330-26D9-DCF7DD136BC5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {13E03C69-0634-3330-26D9-DCF7DD136BC5}.Debug|x64.ActiveCfg = Debug|Any CPU + {13E03C69-0634-3330-26D9-DCF7DD136BC5}.Debug|x64.Build.0 = Debug|Any CPU + {13E03C69-0634-3330-26D9-DCF7DD136BC5}.Debug|x86.ActiveCfg = Debug|Any CPU + {13E03C69-0634-3330-26D9-DCF7DD136BC5}.Debug|x86.Build.0 = Debug|Any CPU + {13E03C69-0634-3330-26D9-DCF7DD136BC5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {13E03C69-0634-3330-26D9-DCF7DD136BC5}.Release|Any CPU.Build.0 = Release|Any CPU + {13E03C69-0634-3330-26D9-DCF7DD136BC5}.Release|x64.ActiveCfg = Release|Any CPU + {13E03C69-0634-3330-26D9-DCF7DD136BC5}.Release|x64.Build.0 = Release|Any CPU + {13E03C69-0634-3330-26D9-DCF7DD136BC5}.Release|x86.ActiveCfg = Release|Any CPU + {13E03C69-0634-3330-26D9-DCF7DD136BC5}.Release|x86.Build.0 = Release|Any CPU + {A80D212B-7E80-4251-16C0-60FA3670A5B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A80D212B-7E80-4251-16C0-60FA3670A5B4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A80D212B-7E80-4251-16C0-60FA3670A5B4}.Debug|x64.ActiveCfg = Debug|Any CPU + {A80D212B-7E80-4251-16C0-60FA3670A5B4}.Debug|x64.Build.0 = Debug|Any CPU + {A80D212B-7E80-4251-16C0-60FA3670A5B4}.Debug|x86.ActiveCfg = Debug|Any CPU + {A80D212B-7E80-4251-16C0-60FA3670A5B4}.Debug|x86.Build.0 = Debug|Any CPU + {A80D212B-7E80-4251-16C0-60FA3670A5B4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A80D212B-7E80-4251-16C0-60FA3670A5B4}.Release|Any CPU.Build.0 = Release|Any CPU + {A80D212B-7E80-4251-16C0-60FA3670A5B4}.Release|x64.ActiveCfg = Release|Any CPU + {A80D212B-7E80-4251-16C0-60FA3670A5B4}.Release|x64.Build.0 = Release|Any CPU + {A80D212B-7E80-4251-16C0-60FA3670A5B4}.Release|x86.ActiveCfg = Release|Any CPU + {A80D212B-7E80-4251-16C0-60FA3670A5B4}.Release|x86.Build.0 = Release|Any CPU + {2F4ACEB8-76C7-D5A2-6DD1-2EF713D9A197}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2F4ACEB8-76C7-D5A2-6DD1-2EF713D9A197}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2F4ACEB8-76C7-D5A2-6DD1-2EF713D9A197}.Debug|x64.ActiveCfg = Debug|Any CPU + {2F4ACEB8-76C7-D5A2-6DD1-2EF713D9A197}.Debug|x64.Build.0 = Debug|Any CPU + {2F4ACEB8-76C7-D5A2-6DD1-2EF713D9A197}.Debug|x86.ActiveCfg = Debug|Any CPU + {2F4ACEB8-76C7-D5A2-6DD1-2EF713D9A197}.Debug|x86.Build.0 = Debug|Any CPU + {2F4ACEB8-76C7-D5A2-6DD1-2EF713D9A197}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2F4ACEB8-76C7-D5A2-6DD1-2EF713D9A197}.Release|Any CPU.Build.0 = Release|Any CPU + {2F4ACEB8-76C7-D5A2-6DD1-2EF713D9A197}.Release|x64.ActiveCfg = Release|Any CPU + {2F4ACEB8-76C7-D5A2-6DD1-2EF713D9A197}.Release|x64.Build.0 = Release|Any CPU + {2F4ACEB8-76C7-D5A2-6DD1-2EF713D9A197}.Release|x86.ActiveCfg = Release|Any CPU + {2F4ACEB8-76C7-D5A2-6DD1-2EF713D9A197}.Release|x86.Build.0 = Release|Any CPU + {C146A9AF-6C13-B9DC-F555-37182A54430F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C146A9AF-6C13-B9DC-F555-37182A54430F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C146A9AF-6C13-B9DC-F555-37182A54430F}.Debug|x64.ActiveCfg = Debug|Any CPU + {C146A9AF-6C13-B9DC-F555-37182A54430F}.Debug|x64.Build.0 = Debug|Any CPU + {C146A9AF-6C13-B9DC-F555-37182A54430F}.Debug|x86.ActiveCfg = Debug|Any CPU + {C146A9AF-6C13-B9DC-F555-37182A54430F}.Debug|x86.Build.0 = Debug|Any CPU + {C146A9AF-6C13-B9DC-F555-37182A54430F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C146A9AF-6C13-B9DC-F555-37182A54430F}.Release|Any CPU.Build.0 = Release|Any CPU + {C146A9AF-6C13-B9DC-F555-37182A54430F}.Release|x64.ActiveCfg = Release|Any CPU + {C146A9AF-6C13-B9DC-F555-37182A54430F}.Release|x64.Build.0 = Release|Any CPU + {C146A9AF-6C13-B9DC-F555-37182A54430F}.Release|x86.ActiveCfg = Release|Any CPU + {C146A9AF-6C13-B9DC-F555-37182A54430F}.Release|x86.Build.0 = Release|Any CPU + {E5025FCF-78CB-4B5B-3377-AE008B1BE9D2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E5025FCF-78CB-4B5B-3377-AE008B1BE9D2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E5025FCF-78CB-4B5B-3377-AE008B1BE9D2}.Debug|x64.ActiveCfg = Debug|Any CPU + {E5025FCF-78CB-4B5B-3377-AE008B1BE9D2}.Debug|x64.Build.0 = Debug|Any CPU + {E5025FCF-78CB-4B5B-3377-AE008B1BE9D2}.Debug|x86.ActiveCfg = Debug|Any CPU + {E5025FCF-78CB-4B5B-3377-AE008B1BE9D2}.Debug|x86.Build.0 = Debug|Any CPU + {E5025FCF-78CB-4B5B-3377-AE008B1BE9D2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E5025FCF-78CB-4B5B-3377-AE008B1BE9D2}.Release|Any CPU.Build.0 = Release|Any CPU + {E5025FCF-78CB-4B5B-3377-AE008B1BE9D2}.Release|x64.ActiveCfg = Release|Any CPU + {E5025FCF-78CB-4B5B-3377-AE008B1BE9D2}.Release|x64.Build.0 = Release|Any CPU + {E5025FCF-78CB-4B5B-3377-AE008B1BE9D2}.Release|x86.ActiveCfg = Release|Any CPU + {E5025FCF-78CB-4B5B-3377-AE008B1BE9D2}.Release|x86.Build.0 = Release|Any CPU + {52698305-D6F8-C13C-0882-48FC37726404}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {52698305-D6F8-C13C-0882-48FC37726404}.Debug|Any CPU.Build.0 = Debug|Any CPU + {52698305-D6F8-C13C-0882-48FC37726404}.Debug|x64.ActiveCfg = Debug|Any CPU + {52698305-D6F8-C13C-0882-48FC37726404}.Debug|x64.Build.0 = Debug|Any CPU + {52698305-D6F8-C13C-0882-48FC37726404}.Debug|x86.ActiveCfg = Debug|Any CPU + {52698305-D6F8-C13C-0882-48FC37726404}.Debug|x86.Build.0 = Debug|Any CPU + {52698305-D6F8-C13C-0882-48FC37726404}.Release|Any CPU.ActiveCfg = Release|Any CPU + {52698305-D6F8-C13C-0882-48FC37726404}.Release|Any CPU.Build.0 = Release|Any CPU + {52698305-D6F8-C13C-0882-48FC37726404}.Release|x64.ActiveCfg = Release|Any CPU + {52698305-D6F8-C13C-0882-48FC37726404}.Release|x64.Build.0 = Release|Any CPU + {52698305-D6F8-C13C-0882-48FC37726404}.Release|x86.ActiveCfg = Release|Any CPU + {52698305-D6F8-C13C-0882-48FC37726404}.Release|x86.Build.0 = Release|Any CPU + {DE10AF97-E790-9D19-2399-70940A9B83A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DE10AF97-E790-9D19-2399-70940A9B83A7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DE10AF97-E790-9D19-2399-70940A9B83A7}.Debug|x64.ActiveCfg = Debug|Any CPU + {DE10AF97-E790-9D19-2399-70940A9B83A7}.Debug|x64.Build.0 = Debug|Any CPU + {DE10AF97-E790-9D19-2399-70940A9B83A7}.Debug|x86.ActiveCfg = Debug|Any CPU + {DE10AF97-E790-9D19-2399-70940A9B83A7}.Debug|x86.Build.0 = Debug|Any CPU + {DE10AF97-E790-9D19-2399-70940A9B83A7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DE10AF97-E790-9D19-2399-70940A9B83A7}.Release|Any CPU.Build.0 = Release|Any CPU + {DE10AF97-E790-9D19-2399-70940A9B83A7}.Release|x64.ActiveCfg = Release|Any CPU + {DE10AF97-E790-9D19-2399-70940A9B83A7}.Release|x64.Build.0 = Release|Any CPU + {DE10AF97-E790-9D19-2399-70940A9B83A7}.Release|x86.ActiveCfg = Release|Any CPU + {DE10AF97-E790-9D19-2399-70940A9B83A7}.Release|x86.Build.0 = Release|Any CPU + {5567139C-0365-B6A0-5DD0-978A09B9F176}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5567139C-0365-B6A0-5DD0-978A09B9F176}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5567139C-0365-B6A0-5DD0-978A09B9F176}.Debug|x64.ActiveCfg = Debug|Any CPU + {5567139C-0365-B6A0-5DD0-978A09B9F176}.Debug|x64.Build.0 = Debug|Any CPU + {5567139C-0365-B6A0-5DD0-978A09B9F176}.Debug|x86.ActiveCfg = Debug|Any CPU + {5567139C-0365-B6A0-5DD0-978A09B9F176}.Debug|x86.Build.0 = Debug|Any CPU + {5567139C-0365-B6A0-5DD0-978A09B9F176}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5567139C-0365-B6A0-5DD0-978A09B9F176}.Release|Any CPU.Build.0 = Release|Any CPU + {5567139C-0365-B6A0-5DD0-978A09B9F176}.Release|x64.ActiveCfg = Release|Any CPU + {5567139C-0365-B6A0-5DD0-978A09B9F176}.Release|x64.Build.0 = Release|Any CPU + {5567139C-0365-B6A0-5DD0-978A09B9F176}.Release|x86.ActiveCfg = Release|Any CPU + {5567139C-0365-B6A0-5DD0-978A09B9F176}.Release|x86.Build.0 = Release|Any CPU + {A56C1F0E-3E18-DBEE-7F97-B5FCBF23D1D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A56C1F0E-3E18-DBEE-7F97-B5FCBF23D1D6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A56C1F0E-3E18-DBEE-7F97-B5FCBF23D1D6}.Debug|x64.ActiveCfg = Debug|Any CPU + {A56C1F0E-3E18-DBEE-7F97-B5FCBF23D1D6}.Debug|x64.Build.0 = Debug|Any CPU + {A56C1F0E-3E18-DBEE-7F97-B5FCBF23D1D6}.Debug|x86.ActiveCfg = Debug|Any CPU + {A56C1F0E-3E18-DBEE-7F97-B5FCBF23D1D6}.Debug|x86.Build.0 = Debug|Any CPU + {A56C1F0E-3E18-DBEE-7F97-B5FCBF23D1D6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A56C1F0E-3E18-DBEE-7F97-B5FCBF23D1D6}.Release|Any CPU.Build.0 = Release|Any CPU + {A56C1F0E-3E18-DBEE-7F97-B5FCBF23D1D6}.Release|x64.ActiveCfg = Release|Any CPU + {A56C1F0E-3E18-DBEE-7F97-B5FCBF23D1D6}.Release|x64.Build.0 = Release|Any CPU + {A56C1F0E-3E18-DBEE-7F97-B5FCBF23D1D6}.Release|x86.ActiveCfg = Release|Any CPU + {A56C1F0E-3E18-DBEE-7F97-B5FCBF23D1D6}.Release|x86.Build.0 = Release|Any CPU + {256D269B-35EA-F833-2F1D-8E0058908DEE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {256D269B-35EA-F833-2F1D-8E0058908DEE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {256D269B-35EA-F833-2F1D-8E0058908DEE}.Debug|x64.ActiveCfg = Debug|Any CPU + {256D269B-35EA-F833-2F1D-8E0058908DEE}.Debug|x64.Build.0 = Debug|Any CPU + {256D269B-35EA-F833-2F1D-8E0058908DEE}.Debug|x86.ActiveCfg = Debug|Any CPU + {256D269B-35EA-F833-2F1D-8E0058908DEE}.Debug|x86.Build.0 = Debug|Any CPU + {256D269B-35EA-F833-2F1D-8E0058908DEE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {256D269B-35EA-F833-2F1D-8E0058908DEE}.Release|Any CPU.Build.0 = Release|Any CPU + {256D269B-35EA-F833-2F1D-8E0058908DEE}.Release|x64.ActiveCfg = Release|Any CPU + {256D269B-35EA-F833-2F1D-8E0058908DEE}.Release|x64.Build.0 = Release|Any CPU + {256D269B-35EA-F833-2F1D-8E0058908DEE}.Release|x86.ActiveCfg = Release|Any CPU + {256D269B-35EA-F833-2F1D-8E0058908DEE}.Release|x86.Build.0 = Release|Any CPU + {F02B63CD-2C69-61F7-7F96-930122D4D4D7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F02B63CD-2C69-61F7-7F96-930122D4D4D7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F02B63CD-2C69-61F7-7F96-930122D4D4D7}.Debug|x64.ActiveCfg = Debug|Any CPU + {F02B63CD-2C69-61F7-7F96-930122D4D4D7}.Debug|x64.Build.0 = Debug|Any CPU + {F02B63CD-2C69-61F7-7F96-930122D4D4D7}.Debug|x86.ActiveCfg = Debug|Any CPU + {F02B63CD-2C69-61F7-7F96-930122D4D4D7}.Debug|x86.Build.0 = Debug|Any CPU + {F02B63CD-2C69-61F7-7F96-930122D4D4D7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F02B63CD-2C69-61F7-7F96-930122D4D4D7}.Release|Any CPU.Build.0 = Release|Any CPU + {F02B63CD-2C69-61F7-7F96-930122D4D4D7}.Release|x64.ActiveCfg = Release|Any CPU + {F02B63CD-2C69-61F7-7F96-930122D4D4D7}.Release|x64.Build.0 = Release|Any CPU + {F02B63CD-2C69-61F7-7F96-930122D4D4D7}.Release|x86.ActiveCfg = Release|Any CPU + {F02B63CD-2C69-61F7-7F96-930122D4D4D7}.Release|x86.Build.0 = Release|Any CPU + {F061C879-063E-99DE-B301-E261DB12156F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F061C879-063E-99DE-B301-E261DB12156F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F061C879-063E-99DE-B301-E261DB12156F}.Debug|x64.ActiveCfg = Debug|Any CPU + {F061C879-063E-99DE-B301-E261DB12156F}.Debug|x64.Build.0 = Debug|Any CPU + {F061C879-063E-99DE-B301-E261DB12156F}.Debug|x86.ActiveCfg = Debug|Any CPU + {F061C879-063E-99DE-B301-E261DB12156F}.Debug|x86.Build.0 = Debug|Any CPU + {F061C879-063E-99DE-B301-E261DB12156F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F061C879-063E-99DE-B301-E261DB12156F}.Release|Any CPU.Build.0 = Release|Any CPU + {F061C879-063E-99DE-B301-E261DB12156F}.Release|x64.ActiveCfg = Release|Any CPU + {F061C879-063E-99DE-B301-E261DB12156F}.Release|x64.Build.0 = Release|Any CPU + {F061C879-063E-99DE-B301-E261DB12156F}.Release|x86.ActiveCfg = Release|Any CPU + {F061C879-063E-99DE-B301-E261DB12156F}.Release|x86.Build.0 = Release|Any CPU + {6E9C9582-67FA-2EB1-C6BA-AD4CD326E276}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6E9C9582-67FA-2EB1-C6BA-AD4CD326E276}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6E9C9582-67FA-2EB1-C6BA-AD4CD326E276}.Debug|x64.ActiveCfg = Debug|Any CPU + {6E9C9582-67FA-2EB1-C6BA-AD4CD326E276}.Debug|x64.Build.0 = Debug|Any CPU + {6E9C9582-67FA-2EB1-C6BA-AD4CD326E276}.Debug|x86.ActiveCfg = Debug|Any CPU + {6E9C9582-67FA-2EB1-C6BA-AD4CD326E276}.Debug|x86.Build.0 = Debug|Any CPU + {6E9C9582-67FA-2EB1-C6BA-AD4CD326E276}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6E9C9582-67FA-2EB1-C6BA-AD4CD326E276}.Release|Any CPU.Build.0 = Release|Any CPU + {6E9C9582-67FA-2EB1-C6BA-AD4CD326E276}.Release|x64.ActiveCfg = Release|Any CPU + {6E9C9582-67FA-2EB1-C6BA-AD4CD326E276}.Release|x64.Build.0 = Release|Any CPU + {6E9C9582-67FA-2EB1-C6BA-AD4CD326E276}.Release|x86.ActiveCfg = Release|Any CPU + {6E9C9582-67FA-2EB1-C6BA-AD4CD326E276}.Release|x86.Build.0 = Release|Any CPU + {FCF711C2-1090-7204-5E38-4BEFBE265A61}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FCF711C2-1090-7204-5E38-4BEFBE265A61}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FCF711C2-1090-7204-5E38-4BEFBE265A61}.Debug|x64.ActiveCfg = Debug|Any CPU + {FCF711C2-1090-7204-5E38-4BEFBE265A61}.Debug|x64.Build.0 = Debug|Any CPU + {FCF711C2-1090-7204-5E38-4BEFBE265A61}.Debug|x86.ActiveCfg = Debug|Any CPU + {FCF711C2-1090-7204-5E38-4BEFBE265A61}.Debug|x86.Build.0 = Debug|Any CPU + {FCF711C2-1090-7204-5E38-4BEFBE265A61}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FCF711C2-1090-7204-5E38-4BEFBE265A61}.Release|Any CPU.Build.0 = Release|Any CPU + {FCF711C2-1090-7204-5E38-4BEFBE265A61}.Release|x64.ActiveCfg = Release|Any CPU + {FCF711C2-1090-7204-5E38-4BEFBE265A61}.Release|x64.Build.0 = Release|Any CPU + {FCF711C2-1090-7204-5E38-4BEFBE265A61}.Release|x86.ActiveCfg = Release|Any CPU + {FCF711C2-1090-7204-5E38-4BEFBE265A61}.Release|x86.Build.0 = Release|Any CPU + {3A4AAE04-FA0C-2AF8-6548-AC22E5A41312}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3A4AAE04-FA0C-2AF8-6548-AC22E5A41312}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3A4AAE04-FA0C-2AF8-6548-AC22E5A41312}.Debug|x64.ActiveCfg = Debug|Any CPU + {3A4AAE04-FA0C-2AF8-6548-AC22E5A41312}.Debug|x64.Build.0 = Debug|Any CPU + {3A4AAE04-FA0C-2AF8-6548-AC22E5A41312}.Debug|x86.ActiveCfg = Debug|Any CPU + {3A4AAE04-FA0C-2AF8-6548-AC22E5A41312}.Debug|x86.Build.0 = Debug|Any CPU + {3A4AAE04-FA0C-2AF8-6548-AC22E5A41312}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3A4AAE04-FA0C-2AF8-6548-AC22E5A41312}.Release|Any CPU.Build.0 = Release|Any CPU + {3A4AAE04-FA0C-2AF8-6548-AC22E5A41312}.Release|x64.ActiveCfg = Release|Any CPU + {3A4AAE04-FA0C-2AF8-6548-AC22E5A41312}.Release|x64.Build.0 = Release|Any CPU + {3A4AAE04-FA0C-2AF8-6548-AC22E5A41312}.Release|x86.ActiveCfg = Release|Any CPU + {3A4AAE04-FA0C-2AF8-6548-AC22E5A41312}.Release|x86.Build.0 = Release|Any CPU + {66F8F288-C387-40E0-5F83-938671335703}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {66F8F288-C387-40E0-5F83-938671335703}.Debug|Any CPU.Build.0 = Debug|Any CPU + {66F8F288-C387-40E0-5F83-938671335703}.Debug|x64.ActiveCfg = Debug|Any CPU + {66F8F288-C387-40E0-5F83-938671335703}.Debug|x64.Build.0 = Debug|Any CPU + {66F8F288-C387-40E0-5F83-938671335703}.Debug|x86.ActiveCfg = Debug|Any CPU + {66F8F288-C387-40E0-5F83-938671335703}.Debug|x86.Build.0 = Debug|Any CPU + {66F8F288-C387-40E0-5F83-938671335703}.Release|Any CPU.ActiveCfg = Release|Any CPU + {66F8F288-C387-40E0-5F83-938671335703}.Release|Any CPU.Build.0 = Release|Any CPU + {66F8F288-C387-40E0-5F83-938671335703}.Release|x64.ActiveCfg = Release|Any CPU + {66F8F288-C387-40E0-5F83-938671335703}.Release|x64.Build.0 = Release|Any CPU + {66F8F288-C387-40E0-5F83-938671335703}.Release|x86.ActiveCfg = Release|Any CPU + {66F8F288-C387-40E0-5F83-938671335703}.Release|x86.Build.0 = Release|Any CPU + {7B3BDB83-918F-6760-3853-BDD70CD71B42}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7B3BDB83-918F-6760-3853-BDD70CD71B42}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7B3BDB83-918F-6760-3853-BDD70CD71B42}.Debug|x64.ActiveCfg = Debug|Any CPU + {7B3BDB83-918F-6760-3853-BDD70CD71B42}.Debug|x64.Build.0 = Debug|Any CPU + {7B3BDB83-918F-6760-3853-BDD70CD71B42}.Debug|x86.ActiveCfg = Debug|Any CPU + {7B3BDB83-918F-6760-3853-BDD70CD71B42}.Debug|x86.Build.0 = Debug|Any CPU + {7B3BDB83-918F-6760-3853-BDD70CD71B42}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7B3BDB83-918F-6760-3853-BDD70CD71B42}.Release|Any CPU.Build.0 = Release|Any CPU + {7B3BDB83-918F-6760-3853-BDD70CD71B42}.Release|x64.ActiveCfg = Release|Any CPU + {7B3BDB83-918F-6760-3853-BDD70CD71B42}.Release|x64.Build.0 = Release|Any CPU + {7B3BDB83-918F-6760-3853-BDD70CD71B42}.Release|x86.ActiveCfg = Release|Any CPU + {7B3BDB83-918F-6760-3853-BDD70CD71B42}.Release|x86.Build.0 = Release|Any CPU + {2669C700-5CFF-0186-F65E-8D26BE06E934}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2669C700-5CFF-0186-F65E-8D26BE06E934}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2669C700-5CFF-0186-F65E-8D26BE06E934}.Debug|x64.ActiveCfg = Debug|Any CPU + {2669C700-5CFF-0186-F65E-8D26BE06E934}.Debug|x64.Build.0 = Debug|Any CPU + {2669C700-5CFF-0186-F65E-8D26BE06E934}.Debug|x86.ActiveCfg = Debug|Any CPU + {2669C700-5CFF-0186-F65E-8D26BE06E934}.Debug|x86.Build.0 = Debug|Any CPU + {2669C700-5CFF-0186-F65E-8D26BE06E934}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2669C700-5CFF-0186-F65E-8D26BE06E934}.Release|Any CPU.Build.0 = Release|Any CPU + {2669C700-5CFF-0186-F65E-8D26BE06E934}.Release|x64.ActiveCfg = Release|Any CPU + {2669C700-5CFF-0186-F65E-8D26BE06E934}.Release|x64.Build.0 = Release|Any CPU + {2669C700-5CFF-0186-F65E-8D26BE06E934}.Release|x86.ActiveCfg = Release|Any CPU + {2669C700-5CFF-0186-F65E-8D26BE06E934}.Release|x86.Build.0 = Release|Any CPU + {0560BD84-CDBC-A79A-C665-55F6D62825EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0560BD84-CDBC-A79A-C665-55F6D62825EA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0560BD84-CDBC-A79A-C665-55F6D62825EA}.Debug|x64.ActiveCfg = Debug|Any CPU + {0560BD84-CDBC-A79A-C665-55F6D62825EA}.Debug|x64.Build.0 = Debug|Any CPU + {0560BD84-CDBC-A79A-C665-55F6D62825EA}.Debug|x86.ActiveCfg = Debug|Any CPU + {0560BD84-CDBC-A79A-C665-55F6D62825EA}.Debug|x86.Build.0 = Debug|Any CPU + {0560BD84-CDBC-A79A-C665-55F6D62825EA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0560BD84-CDBC-A79A-C665-55F6D62825EA}.Release|Any CPU.Build.0 = Release|Any CPU + {0560BD84-CDBC-A79A-C665-55F6D62825EA}.Release|x64.ActiveCfg = Release|Any CPU + {0560BD84-CDBC-A79A-C665-55F6D62825EA}.Release|x64.Build.0 = Release|Any CPU + {0560BD84-CDBC-A79A-C665-55F6D62825EA}.Release|x86.ActiveCfg = Release|Any CPU + {0560BD84-CDBC-A79A-C665-55F6D62825EA}.Release|x86.Build.0 = Release|Any CPU + {783A67C9-3381-6E4C-3752-423F0FC6F6F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {783A67C9-3381-6E4C-3752-423F0FC6F6F9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {783A67C9-3381-6E4C-3752-423F0FC6F6F9}.Debug|x64.ActiveCfg = Debug|Any CPU + {783A67C9-3381-6E4C-3752-423F0FC6F6F9}.Debug|x64.Build.0 = Debug|Any CPU + {783A67C9-3381-6E4C-3752-423F0FC6F6F9}.Debug|x86.ActiveCfg = Debug|Any CPU + {783A67C9-3381-6E4C-3752-423F0FC6F6F9}.Debug|x86.Build.0 = Debug|Any CPU + {783A67C9-3381-6E4C-3752-423F0FC6F6F9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {783A67C9-3381-6E4C-3752-423F0FC6F6F9}.Release|Any CPU.Build.0 = Release|Any CPU + {783A67C9-3381-6E4C-3752-423F0FC6F6F9}.Release|x64.ActiveCfg = Release|Any CPU + {783A67C9-3381-6E4C-3752-423F0FC6F6F9}.Release|x64.Build.0 = Release|Any CPU + {783A67C9-3381-6E4C-3752-423F0FC6F6F9}.Release|x86.ActiveCfg = Release|Any CPU + {783A67C9-3381-6E4C-3752-423F0FC6F6F9}.Release|x86.Build.0 = Release|Any CPU + {F890BD12-6CF5-4F80-9099-B7FE9A908432}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F890BD12-6CF5-4F80-9099-B7FE9A908432}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F890BD12-6CF5-4F80-9099-B7FE9A908432}.Debug|x64.ActiveCfg = Debug|Any CPU + {F890BD12-6CF5-4F80-9099-B7FE9A908432}.Debug|x64.Build.0 = Debug|Any CPU + {F890BD12-6CF5-4F80-9099-B7FE9A908432}.Debug|x86.ActiveCfg = Debug|Any CPU + {F890BD12-6CF5-4F80-9099-B7FE9A908432}.Debug|x86.Build.0 = Debug|Any CPU + {F890BD12-6CF5-4F80-9099-B7FE9A908432}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F890BD12-6CF5-4F80-9099-B7FE9A908432}.Release|Any CPU.Build.0 = Release|Any CPU + {F890BD12-6CF5-4F80-9099-B7FE9A908432}.Release|x64.ActiveCfg = Release|Any CPU + {F890BD12-6CF5-4F80-9099-B7FE9A908432}.Release|x64.Build.0 = Release|Any CPU + {F890BD12-6CF5-4F80-9099-B7FE9A908432}.Release|x86.ActiveCfg = Release|Any CPU + {F890BD12-6CF5-4F80-9099-B7FE9A908432}.Release|x86.Build.0 = Release|Any CPU + {505C6840-5113-26EC-CEDB-D07EEABEF94B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {505C6840-5113-26EC-CEDB-D07EEABEF94B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {505C6840-5113-26EC-CEDB-D07EEABEF94B}.Debug|x64.ActiveCfg = Debug|Any CPU + {505C6840-5113-26EC-CEDB-D07EEABEF94B}.Debug|x64.Build.0 = Debug|Any CPU + {505C6840-5113-26EC-CEDB-D07EEABEF94B}.Debug|x86.ActiveCfg = Debug|Any CPU + {505C6840-5113-26EC-CEDB-D07EEABEF94B}.Debug|x86.Build.0 = Debug|Any CPU + {505C6840-5113-26EC-CEDB-D07EEABEF94B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {505C6840-5113-26EC-CEDB-D07EEABEF94B}.Release|Any CPU.Build.0 = Release|Any CPU + {505C6840-5113-26EC-CEDB-D07EEABEF94B}.Release|x64.ActiveCfg = Release|Any CPU + {505C6840-5113-26EC-CEDB-D07EEABEF94B}.Release|x64.Build.0 = Release|Any CPU + {505C6840-5113-26EC-CEDB-D07EEABEF94B}.Release|x86.ActiveCfg = Release|Any CPU + {505C6840-5113-26EC-CEDB-D07EEABEF94B}.Release|x86.Build.0 = Release|Any CPU + {A79CBC0C-5313-4ECF-A24E-27CE236BCF2C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A79CBC0C-5313-4ECF-A24E-27CE236BCF2C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A79CBC0C-5313-4ECF-A24E-27CE236BCF2C}.Debug|x64.ActiveCfg = Debug|Any CPU + {A79CBC0C-5313-4ECF-A24E-27CE236BCF2C}.Debug|x64.Build.0 = Debug|Any CPU + {A79CBC0C-5313-4ECF-A24E-27CE236BCF2C}.Debug|x86.ActiveCfg = Debug|Any CPU + {A79CBC0C-5313-4ECF-A24E-27CE236BCF2C}.Debug|x86.Build.0 = Debug|Any CPU + {A79CBC0C-5313-4ECF-A24E-27CE236BCF2C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A79CBC0C-5313-4ECF-A24E-27CE236BCF2C}.Release|Any CPU.Build.0 = Release|Any CPU + {A79CBC0C-5313-4ECF-A24E-27CE236BCF2C}.Release|x64.ActiveCfg = Release|Any CPU + {A79CBC0C-5313-4ECF-A24E-27CE236BCF2C}.Release|x64.Build.0 = Release|Any CPU + {A79CBC0C-5313-4ECF-A24E-27CE236BCF2C}.Release|x86.ActiveCfg = Release|Any CPU + {A79CBC0C-5313-4ECF-A24E-27CE236BCF2C}.Release|x86.Build.0 = Release|Any CPU + {0AF13355-173C-3128-5AFC-D32E540DA3EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0AF13355-173C-3128-5AFC-D32E540DA3EF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0AF13355-173C-3128-5AFC-D32E540DA3EF}.Debug|x64.ActiveCfg = Debug|Any CPU + {0AF13355-173C-3128-5AFC-D32E540DA3EF}.Debug|x64.Build.0 = Debug|Any CPU + {0AF13355-173C-3128-5AFC-D32E540DA3EF}.Debug|x86.ActiveCfg = Debug|Any CPU + {0AF13355-173C-3128-5AFC-D32E540DA3EF}.Debug|x86.Build.0 = Debug|Any CPU + {0AF13355-173C-3128-5AFC-D32E540DA3EF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0AF13355-173C-3128-5AFC-D32E540DA3EF}.Release|Any CPU.Build.0 = Release|Any CPU + {0AF13355-173C-3128-5AFC-D32E540DA3EF}.Release|x64.ActiveCfg = Release|Any CPU + {0AF13355-173C-3128-5AFC-D32E540DA3EF}.Release|x64.Build.0 = Release|Any CPU + {0AF13355-173C-3128-5AFC-D32E540DA3EF}.Release|x86.ActiveCfg = Release|Any CPU + {0AF13355-173C-3128-5AFC-D32E540DA3EF}.Release|x86.Build.0 = Release|Any CPU + {AF043113-CCE3-59C1-DF71-9804155F26A8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AF043113-CCE3-59C1-DF71-9804155F26A8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AF043113-CCE3-59C1-DF71-9804155F26A8}.Debug|x64.ActiveCfg = Debug|Any CPU + {AF043113-CCE3-59C1-DF71-9804155F26A8}.Debug|x64.Build.0 = Debug|Any CPU + {AF043113-CCE3-59C1-DF71-9804155F26A8}.Debug|x86.ActiveCfg = Debug|Any CPU + {AF043113-CCE3-59C1-DF71-9804155F26A8}.Debug|x86.Build.0 = Debug|Any CPU + {AF043113-CCE3-59C1-DF71-9804155F26A8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AF043113-CCE3-59C1-DF71-9804155F26A8}.Release|Any CPU.Build.0 = Release|Any CPU + {AF043113-CCE3-59C1-DF71-9804155F26A8}.Release|x64.ActiveCfg = Release|Any CPU + {AF043113-CCE3-59C1-DF71-9804155F26A8}.Release|x64.Build.0 = Release|Any CPU + {AF043113-CCE3-59C1-DF71-9804155F26A8}.Release|x86.ActiveCfg = Release|Any CPU + {AF043113-CCE3-59C1-DF71-9804155F26A8}.Release|x86.Build.0 = Release|Any CPU + {15602821-2ABA-14BB-738D-1A53E1976E07}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {15602821-2ABA-14BB-738D-1A53E1976E07}.Debug|Any CPU.Build.0 = Debug|Any CPU + {15602821-2ABA-14BB-738D-1A53E1976E07}.Debug|x64.ActiveCfg = Debug|Any CPU + {15602821-2ABA-14BB-738D-1A53E1976E07}.Debug|x64.Build.0 = Debug|Any CPU + {15602821-2ABA-14BB-738D-1A53E1976E07}.Debug|x86.ActiveCfg = Debug|Any CPU + {15602821-2ABA-14BB-738D-1A53E1976E07}.Debug|x86.Build.0 = Debug|Any CPU + {15602821-2ABA-14BB-738D-1A53E1976E07}.Release|Any CPU.ActiveCfg = Release|Any CPU + {15602821-2ABA-14BB-738D-1A53E1976E07}.Release|Any CPU.Build.0 = Release|Any CPU + {15602821-2ABA-14BB-738D-1A53E1976E07}.Release|x64.ActiveCfg = Release|Any CPU + {15602821-2ABA-14BB-738D-1A53E1976E07}.Release|x64.Build.0 = Release|Any CPU + {15602821-2ABA-14BB-738D-1A53E1976E07}.Release|x86.ActiveCfg = Release|Any CPU + {15602821-2ABA-14BB-738D-1A53E1976E07}.Release|x86.Build.0 = Release|Any CPU + {1D761F8B-921C-53BF-DCF5-5ABD329EEB0C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1D761F8B-921C-53BF-DCF5-5ABD329EEB0C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1D761F8B-921C-53BF-DCF5-5ABD329EEB0C}.Debug|x64.ActiveCfg = Debug|Any CPU + {1D761F8B-921C-53BF-DCF5-5ABD329EEB0C}.Debug|x64.Build.0 = Debug|Any CPU + {1D761F8B-921C-53BF-DCF5-5ABD329EEB0C}.Debug|x86.ActiveCfg = Debug|Any CPU + {1D761F8B-921C-53BF-DCF5-5ABD329EEB0C}.Debug|x86.Build.0 = Debug|Any CPU + {1D761F8B-921C-53BF-DCF5-5ABD329EEB0C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1D761F8B-921C-53BF-DCF5-5ABD329EEB0C}.Release|Any CPU.Build.0 = Release|Any CPU + {1D761F8B-921C-53BF-DCF5-5ABD329EEB0C}.Release|x64.ActiveCfg = Release|Any CPU + {1D761F8B-921C-53BF-DCF5-5ABD329EEB0C}.Release|x64.Build.0 = Release|Any CPU + {1D761F8B-921C-53BF-DCF5-5ABD329EEB0C}.Release|x86.ActiveCfg = Release|Any CPU + {1D761F8B-921C-53BF-DCF5-5ABD329EEB0C}.Release|x86.Build.0 = Release|Any CPU + {DA7634C2-9156-9B79-7A1D-90D8E605DC8A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DA7634C2-9156-9B79-7A1D-90D8E605DC8A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DA7634C2-9156-9B79-7A1D-90D8E605DC8A}.Debug|x64.ActiveCfg = Debug|Any CPU + {DA7634C2-9156-9B79-7A1D-90D8E605DC8A}.Debug|x64.Build.0 = Debug|Any CPU + {DA7634C2-9156-9B79-7A1D-90D8E605DC8A}.Debug|x86.ActiveCfg = Debug|Any CPU + {DA7634C2-9156-9B79-7A1D-90D8E605DC8A}.Debug|x86.Build.0 = Debug|Any CPU + {DA7634C2-9156-9B79-7A1D-90D8E605DC8A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DA7634C2-9156-9B79-7A1D-90D8E605DC8A}.Release|Any CPU.Build.0 = Release|Any CPU + {DA7634C2-9156-9B79-7A1D-90D8E605DC8A}.Release|x64.ActiveCfg = Release|Any CPU + {DA7634C2-9156-9B79-7A1D-90D8E605DC8A}.Release|x64.Build.0 = Release|Any CPU + {DA7634C2-9156-9B79-7A1D-90D8E605DC8A}.Release|x86.ActiveCfg = Release|Any CPU + {DA7634C2-9156-9B79-7A1D-90D8E605DC8A}.Release|x86.Build.0 = Release|Any CPU + {5C4EF841-B039-4899-BF6F-32DC4FDB7AE5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5C4EF841-B039-4899-BF6F-32DC4FDB7AE5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5C4EF841-B039-4899-BF6F-32DC4FDB7AE5}.Debug|x64.ActiveCfg = Debug|Any CPU + {5C4EF841-B039-4899-BF6F-32DC4FDB7AE5}.Debug|x64.Build.0 = Debug|Any CPU + {5C4EF841-B039-4899-BF6F-32DC4FDB7AE5}.Debug|x86.ActiveCfg = Debug|Any CPU + {5C4EF841-B039-4899-BF6F-32DC4FDB7AE5}.Debug|x86.Build.0 = Debug|Any CPU + {5C4EF841-B039-4899-BF6F-32DC4FDB7AE5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5C4EF841-B039-4899-BF6F-32DC4FDB7AE5}.Release|Any CPU.Build.0 = Release|Any CPU + {5C4EF841-B039-4899-BF6F-32DC4FDB7AE5}.Release|x64.ActiveCfg = Release|Any CPU + {5C4EF841-B039-4899-BF6F-32DC4FDB7AE5}.Release|x64.Build.0 = Release|Any CPU + {5C4EF841-B039-4899-BF6F-32DC4FDB7AE5}.Release|x86.ActiveCfg = Release|Any CPU + {5C4EF841-B039-4899-BF6F-32DC4FDB7AE5}.Release|x86.Build.0 = Release|Any CPU + {188D45EF-BD46-40F6-BC4A-1B708E0C7169}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {188D45EF-BD46-40F6-BC4A-1B708E0C7169}.Debug|Any CPU.Build.0 = Debug|Any CPU + {188D45EF-BD46-40F6-BC4A-1B708E0C7169}.Debug|x64.ActiveCfg = Debug|Any CPU + {188D45EF-BD46-40F6-BC4A-1B708E0C7169}.Debug|x64.Build.0 = Debug|Any CPU + {188D45EF-BD46-40F6-BC4A-1B708E0C7169}.Debug|x86.ActiveCfg = Debug|Any CPU + {188D45EF-BD46-40F6-BC4A-1B708E0C7169}.Debug|x86.Build.0 = Debug|Any CPU + {188D45EF-BD46-40F6-BC4A-1B708E0C7169}.Release|Any CPU.ActiveCfg = Release|Any CPU + {188D45EF-BD46-40F6-BC4A-1B708E0C7169}.Release|Any CPU.Build.0 = Release|Any CPU + {188D45EF-BD46-40F6-BC4A-1B708E0C7169}.Release|x64.ActiveCfg = Release|Any CPU + {188D45EF-BD46-40F6-BC4A-1B708E0C7169}.Release|x64.Build.0 = Release|Any CPU + {188D45EF-BD46-40F6-BC4A-1B708E0C7169}.Release|x86.ActiveCfg = Release|Any CPU + {188D45EF-BD46-40F6-BC4A-1B708E0C7169}.Release|x86.Build.0 = Release|Any CPU + {4D73CAD5-0D7A-4FCA-AE0B-06D07BFA1851}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4D73CAD5-0D7A-4FCA-AE0B-06D07BFA1851}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4D73CAD5-0D7A-4FCA-AE0B-06D07BFA1851}.Debug|x64.ActiveCfg = Debug|Any CPU + {4D73CAD5-0D7A-4FCA-AE0B-06D07BFA1851}.Debug|x64.Build.0 = Debug|Any CPU + {4D73CAD5-0D7A-4FCA-AE0B-06D07BFA1851}.Debug|x86.ActiveCfg = Debug|Any CPU + {4D73CAD5-0D7A-4FCA-AE0B-06D07BFA1851}.Debug|x86.Build.0 = Debug|Any CPU + {4D73CAD5-0D7A-4FCA-AE0B-06D07BFA1851}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4D73CAD5-0D7A-4FCA-AE0B-06D07BFA1851}.Release|Any CPU.Build.0 = Release|Any CPU + {4D73CAD5-0D7A-4FCA-AE0B-06D07BFA1851}.Release|x64.ActiveCfg = Release|Any CPU + {4D73CAD5-0D7A-4FCA-AE0B-06D07BFA1851}.Release|x64.Build.0 = Release|Any CPU + {4D73CAD5-0D7A-4FCA-AE0B-06D07BFA1851}.Release|x86.ActiveCfg = Release|Any CPU + {4D73CAD5-0D7A-4FCA-AE0B-06D07BFA1851}.Release|x86.Build.0 = Release|Any CPU + {4297C94F-8AB6-46BB-AAF0-27BE050F2FA7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4297C94F-8AB6-46BB-AAF0-27BE050F2FA7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4297C94F-8AB6-46BB-AAF0-27BE050F2FA7}.Debug|x64.ActiveCfg = Debug|Any CPU + {4297C94F-8AB6-46BB-AAF0-27BE050F2FA7}.Debug|x64.Build.0 = Debug|Any CPU + {4297C94F-8AB6-46BB-AAF0-27BE050F2FA7}.Debug|x86.ActiveCfg = Debug|Any CPU + {4297C94F-8AB6-46BB-AAF0-27BE050F2FA7}.Debug|x86.Build.0 = Debug|Any CPU + {4297C94F-8AB6-46BB-AAF0-27BE050F2FA7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4297C94F-8AB6-46BB-AAF0-27BE050F2FA7}.Release|Any CPU.Build.0 = Release|Any CPU + {4297C94F-8AB6-46BB-AAF0-27BE050F2FA7}.Release|x64.ActiveCfg = Release|Any CPU + {4297C94F-8AB6-46BB-AAF0-27BE050F2FA7}.Release|x64.Build.0 = Release|Any CPU + {4297C94F-8AB6-46BB-AAF0-27BE050F2FA7}.Release|x86.ActiveCfg = Release|Any CPU + {4297C94F-8AB6-46BB-AAF0-27BE050F2FA7}.Release|x86.Build.0 = Release|Any CPU + {0EB3E7C7-4DBA-48E2-BC3C-E98F30799A0C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0EB3E7C7-4DBA-48E2-BC3C-E98F30799A0C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0EB3E7C7-4DBA-48E2-BC3C-E98F30799A0C}.Debug|x64.ActiveCfg = Debug|Any CPU + {0EB3E7C7-4DBA-48E2-BC3C-E98F30799A0C}.Debug|x64.Build.0 = Debug|Any CPU + {0EB3E7C7-4DBA-48E2-BC3C-E98F30799A0C}.Debug|x86.ActiveCfg = Debug|Any CPU + {0EB3E7C7-4DBA-48E2-BC3C-E98F30799A0C}.Debug|x86.Build.0 = Debug|Any CPU + {0EB3E7C7-4DBA-48E2-BC3C-E98F30799A0C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0EB3E7C7-4DBA-48E2-BC3C-E98F30799A0C}.Release|Any CPU.Build.0 = Release|Any CPU + {0EB3E7C7-4DBA-48E2-BC3C-E98F30799A0C}.Release|x64.ActiveCfg = Release|Any CPU + {0EB3E7C7-4DBA-48E2-BC3C-E98F30799A0C}.Release|x64.Build.0 = Release|Any CPU + {0EB3E7C7-4DBA-48E2-BC3C-E98F30799A0C}.Release|x86.ActiveCfg = Release|Any CPU + {0EB3E7C7-4DBA-48E2-BC3C-E98F30799A0C}.Release|x86.Build.0 = Release|Any CPU + {AB8B0F8E-D92D-410E-8EB7-A766522DF520}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AB8B0F8E-D92D-410E-8EB7-A766522DF520}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AB8B0F8E-D92D-410E-8EB7-A766522DF520}.Debug|x64.ActiveCfg = Debug|Any CPU + {AB8B0F8E-D92D-410E-8EB7-A766522DF520}.Debug|x64.Build.0 = Debug|Any CPU + {AB8B0F8E-D92D-410E-8EB7-A766522DF520}.Debug|x86.ActiveCfg = Debug|Any CPU + {AB8B0F8E-D92D-410E-8EB7-A766522DF520}.Debug|x86.Build.0 = Debug|Any CPU + {AB8B0F8E-D92D-410E-8EB7-A766522DF520}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AB8B0F8E-D92D-410E-8EB7-A766522DF520}.Release|Any CPU.Build.0 = Release|Any CPU + {AB8B0F8E-D92D-410E-8EB7-A766522DF520}.Release|x64.ActiveCfg = Release|Any CPU + {AB8B0F8E-D92D-410E-8EB7-A766522DF520}.Release|x64.Build.0 = Release|Any CPU + {AB8B0F8E-D92D-410E-8EB7-A766522DF520}.Release|x86.ActiveCfg = Release|Any CPU + {AB8B0F8E-D92D-410E-8EB7-A766522DF520}.Release|x86.Build.0 = Release|Any CPU + {46C131C6-64D9-4C72-9C11-AABCAD340DE3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {46C131C6-64D9-4C72-9C11-AABCAD340DE3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {46C131C6-64D9-4C72-9C11-AABCAD340DE3}.Debug|x64.ActiveCfg = Debug|Any CPU + {46C131C6-64D9-4C72-9C11-AABCAD340DE3}.Debug|x64.Build.0 = Debug|Any CPU + {46C131C6-64D9-4C72-9C11-AABCAD340DE3}.Debug|x86.ActiveCfg = Debug|Any CPU + {46C131C6-64D9-4C72-9C11-AABCAD340DE3}.Debug|x86.Build.0 = Debug|Any CPU + {46C131C6-64D9-4C72-9C11-AABCAD340DE3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {46C131C6-64D9-4C72-9C11-AABCAD340DE3}.Release|Any CPU.Build.0 = Release|Any CPU + {46C131C6-64D9-4C72-9C11-AABCAD340DE3}.Release|x64.ActiveCfg = Release|Any CPU + {46C131C6-64D9-4C72-9C11-AABCAD340DE3}.Release|x64.Build.0 = Release|Any CPU + {46C131C6-64D9-4C72-9C11-AABCAD340DE3}.Release|x86.ActiveCfg = Release|Any CPU + {46C131C6-64D9-4C72-9C11-AABCAD340DE3}.Release|x86.Build.0 = Release|Any CPU + {E0016C1D-F312-4EB5-8EFE-96DE87D97B43}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E0016C1D-F312-4EB5-8EFE-96DE87D97B43}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E0016C1D-F312-4EB5-8EFE-96DE87D97B43}.Debug|x64.ActiveCfg = Debug|Any CPU + {E0016C1D-F312-4EB5-8EFE-96DE87D97B43}.Debug|x64.Build.0 = Debug|Any CPU + {E0016C1D-F312-4EB5-8EFE-96DE87D97B43}.Debug|x86.ActiveCfg = Debug|Any CPU + {E0016C1D-F312-4EB5-8EFE-96DE87D97B43}.Debug|x86.Build.0 = Debug|Any CPU + {E0016C1D-F312-4EB5-8EFE-96DE87D97B43}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E0016C1D-F312-4EB5-8EFE-96DE87D97B43}.Release|Any CPU.Build.0 = Release|Any CPU + {E0016C1D-F312-4EB5-8EFE-96DE87D97B43}.Release|x64.ActiveCfg = Release|Any CPU + {E0016C1D-F312-4EB5-8EFE-96DE87D97B43}.Release|x64.Build.0 = Release|Any CPU + {E0016C1D-F312-4EB5-8EFE-96DE87D97B43}.Release|x86.ActiveCfg = Release|Any CPU + {E0016C1D-F312-4EB5-8EFE-96DE87D97B43}.Release|x86.Build.0 = Release|Any CPU + {8B84E879-EB68-435A-8FAF-4539D8E2B359}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8B84E879-EB68-435A-8FAF-4539D8E2B359}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8B84E879-EB68-435A-8FAF-4539D8E2B359}.Debug|x64.ActiveCfg = Debug|Any CPU + {8B84E879-EB68-435A-8FAF-4539D8E2B359}.Debug|x64.Build.0 = Debug|Any CPU + {8B84E879-EB68-435A-8FAF-4539D8E2B359}.Debug|x86.ActiveCfg = Debug|Any CPU + {8B84E879-EB68-435A-8FAF-4539D8E2B359}.Debug|x86.Build.0 = Debug|Any CPU + {8B84E879-EB68-435A-8FAF-4539D8E2B359}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8B84E879-EB68-435A-8FAF-4539D8E2B359}.Release|Any CPU.Build.0 = Release|Any CPU + {8B84E879-EB68-435A-8FAF-4539D8E2B359}.Release|x64.ActiveCfg = Release|Any CPU + {8B84E879-EB68-435A-8FAF-4539D8E2B359}.Release|x64.Build.0 = Release|Any CPU + {8B84E879-EB68-435A-8FAF-4539D8E2B359}.Release|x86.ActiveCfg = Release|Any CPU + {8B84E879-EB68-435A-8FAF-4539D8E2B359}.Release|x86.Build.0 = Release|Any CPU + {EEB5F0FD-BCAC-47C6-8895-B414B8656070}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EEB5F0FD-BCAC-47C6-8895-B414B8656070}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EEB5F0FD-BCAC-47C6-8895-B414B8656070}.Debug|x64.ActiveCfg = Debug|Any CPU + {EEB5F0FD-BCAC-47C6-8895-B414B8656070}.Debug|x64.Build.0 = Debug|Any CPU + {EEB5F0FD-BCAC-47C6-8895-B414B8656070}.Debug|x86.ActiveCfg = Debug|Any CPU + {EEB5F0FD-BCAC-47C6-8895-B414B8656070}.Debug|x86.Build.0 = Debug|Any CPU + {EEB5F0FD-BCAC-47C6-8895-B414B8656070}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EEB5F0FD-BCAC-47C6-8895-B414B8656070}.Release|Any CPU.Build.0 = Release|Any CPU + {EEB5F0FD-BCAC-47C6-8895-B414B8656070}.Release|x64.ActiveCfg = Release|Any CPU + {EEB5F0FD-BCAC-47C6-8895-B414B8656070}.Release|x64.Build.0 = Release|Any CPU + {EEB5F0FD-BCAC-47C6-8895-B414B8656070}.Release|x86.ActiveCfg = Release|Any CPU + {EEB5F0FD-BCAC-47C6-8895-B414B8656070}.Release|x86.Build.0 = Release|Any CPU + {A9AE973D-8734-4C6E-9DD2-552478F6D5D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A9AE973D-8734-4C6E-9DD2-552478F6D5D5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A9AE973D-8734-4C6E-9DD2-552478F6D5D5}.Debug|x64.ActiveCfg = Debug|Any CPU + {A9AE973D-8734-4C6E-9DD2-552478F6D5D5}.Debug|x64.Build.0 = Debug|Any CPU + {A9AE973D-8734-4C6E-9DD2-552478F6D5D5}.Debug|x86.ActiveCfg = Debug|Any CPU + {A9AE973D-8734-4C6E-9DD2-552478F6D5D5}.Debug|x86.Build.0 = Debug|Any CPU + {A9AE973D-8734-4C6E-9DD2-552478F6D5D5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A9AE973D-8734-4C6E-9DD2-552478F6D5D5}.Release|Any CPU.Build.0 = Release|Any CPU + {A9AE973D-8734-4C6E-9DD2-552478F6D5D5}.Release|x64.ActiveCfg = Release|Any CPU + {A9AE973D-8734-4C6E-9DD2-552478F6D5D5}.Release|x64.Build.0 = Release|Any CPU + {A9AE973D-8734-4C6E-9DD2-552478F6D5D5}.Release|x86.ActiveCfg = Release|Any CPU + {A9AE973D-8734-4C6E-9DD2-552478F6D5D5}.Release|x86.Build.0 = Release|Any CPU + {9C74824C-0CED-4976-A910-CBAE529223D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9C74824C-0CED-4976-A910-CBAE529223D9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9C74824C-0CED-4976-A910-CBAE529223D9}.Debug|x64.ActiveCfg = Debug|Any CPU + {9C74824C-0CED-4976-A910-CBAE529223D9}.Debug|x64.Build.0 = Debug|Any CPU + {9C74824C-0CED-4976-A910-CBAE529223D9}.Debug|x86.ActiveCfg = Debug|Any CPU + {9C74824C-0CED-4976-A910-CBAE529223D9}.Debug|x86.Build.0 = Debug|Any CPU + {9C74824C-0CED-4976-A910-CBAE529223D9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9C74824C-0CED-4976-A910-CBAE529223D9}.Release|Any CPU.Build.0 = Release|Any CPU + {9C74824C-0CED-4976-A910-CBAE529223D9}.Release|x64.ActiveCfg = Release|Any CPU + {9C74824C-0CED-4976-A910-CBAE529223D9}.Release|x64.Build.0 = Release|Any CPU + {9C74824C-0CED-4976-A910-CBAE529223D9}.Release|x86.ActiveCfg = Release|Any CPU + {9C74824C-0CED-4976-A910-CBAE529223D9}.Release|x86.Build.0 = Release|Any CPU + {0608FA93-FE0B-410F-810E-C250CEB81AB9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0608FA93-FE0B-410F-810E-C250CEB81AB9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0608FA93-FE0B-410F-810E-C250CEB81AB9}.Debug|x64.ActiveCfg = Debug|Any CPU + {0608FA93-FE0B-410F-810E-C250CEB81AB9}.Debug|x64.Build.0 = Debug|Any CPU + {0608FA93-FE0B-410F-810E-C250CEB81AB9}.Debug|x86.ActiveCfg = Debug|Any CPU + {0608FA93-FE0B-410F-810E-C250CEB81AB9}.Debug|x86.Build.0 = Debug|Any CPU + {0608FA93-FE0B-410F-810E-C250CEB81AB9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0608FA93-FE0B-410F-810E-C250CEB81AB9}.Release|Any CPU.Build.0 = Release|Any CPU + {0608FA93-FE0B-410F-810E-C250CEB81AB9}.Release|x64.ActiveCfg = Release|Any CPU + {0608FA93-FE0B-410F-810E-C250CEB81AB9}.Release|x64.Build.0 = Release|Any CPU + {0608FA93-FE0B-410F-810E-C250CEB81AB9}.Release|x86.ActiveCfg = Release|Any CPU + {0608FA93-FE0B-410F-810E-C250CEB81AB9}.Release|x86.Build.0 = Release|Any CPU + {444A9AEC-4B40-4B6D-916A-15B9D195DF26}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {444A9AEC-4B40-4B6D-916A-15B9D195DF26}.Debug|Any CPU.Build.0 = Debug|Any CPU + {444A9AEC-4B40-4B6D-916A-15B9D195DF26}.Debug|x64.ActiveCfg = Debug|Any CPU + {444A9AEC-4B40-4B6D-916A-15B9D195DF26}.Debug|x64.Build.0 = Debug|Any CPU + {444A9AEC-4B40-4B6D-916A-15B9D195DF26}.Debug|x86.ActiveCfg = Debug|Any CPU + {444A9AEC-4B40-4B6D-916A-15B9D195DF26}.Debug|x86.Build.0 = Debug|Any CPU + {444A9AEC-4B40-4B6D-916A-15B9D195DF26}.Release|Any CPU.ActiveCfg = Release|Any CPU + {444A9AEC-4B40-4B6D-916A-15B9D195DF26}.Release|Any CPU.Build.0 = Release|Any CPU + {444A9AEC-4B40-4B6D-916A-15B9D195DF26}.Release|x64.ActiveCfg = Release|Any CPU + {444A9AEC-4B40-4B6D-916A-15B9D195DF26}.Release|x64.Build.0 = Release|Any CPU + {444A9AEC-4B40-4B6D-916A-15B9D195DF26}.Release|x86.ActiveCfg = Release|Any CPU + {444A9AEC-4B40-4B6D-916A-15B9D195DF26}.Release|x86.Build.0 = Release|Any CPU + {3A24D8FE-4A6F-4E03-85C9-17E46B647516}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3A24D8FE-4A6F-4E03-85C9-17E46B647516}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3A24D8FE-4A6F-4E03-85C9-17E46B647516}.Debug|x64.ActiveCfg = Debug|Any CPU + {3A24D8FE-4A6F-4E03-85C9-17E46B647516}.Debug|x64.Build.0 = Debug|Any CPU + {3A24D8FE-4A6F-4E03-85C9-17E46B647516}.Debug|x86.ActiveCfg = Debug|Any CPU + {3A24D8FE-4A6F-4E03-85C9-17E46B647516}.Debug|x86.Build.0 = Debug|Any CPU + {3A24D8FE-4A6F-4E03-85C9-17E46B647516}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3A24D8FE-4A6F-4E03-85C9-17E46B647516}.Release|Any CPU.Build.0 = Release|Any CPU + {3A24D8FE-4A6F-4E03-85C9-17E46B647516}.Release|x64.ActiveCfg = Release|Any CPU + {3A24D8FE-4A6F-4E03-85C9-17E46B647516}.Release|x64.Build.0 = Release|Any CPU + {3A24D8FE-4A6F-4E03-85C9-17E46B647516}.Release|x86.ActiveCfg = Release|Any CPU + {3A24D8FE-4A6F-4E03-85C9-17E46B647516}.Release|x86.Build.0 = Release|Any CPU + {C09B550A-314A-4719-88CF-DFBD549E0CA1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C09B550A-314A-4719-88CF-DFBD549E0CA1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C09B550A-314A-4719-88CF-DFBD549E0CA1}.Debug|x64.ActiveCfg = Debug|Any CPU + {C09B550A-314A-4719-88CF-DFBD549E0CA1}.Debug|x64.Build.0 = Debug|Any CPU + {C09B550A-314A-4719-88CF-DFBD549E0CA1}.Debug|x86.ActiveCfg = Debug|Any CPU + {C09B550A-314A-4719-88CF-DFBD549E0CA1}.Debug|x86.Build.0 = Debug|Any CPU + {C09B550A-314A-4719-88CF-DFBD549E0CA1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C09B550A-314A-4719-88CF-DFBD549E0CA1}.Release|Any CPU.Build.0 = Release|Any CPU + {C09B550A-314A-4719-88CF-DFBD549E0CA1}.Release|x64.ActiveCfg = Release|Any CPU + {C09B550A-314A-4719-88CF-DFBD549E0CA1}.Release|x64.Build.0 = Release|Any CPU + {C09B550A-314A-4719-88CF-DFBD549E0CA1}.Release|x86.ActiveCfg = Release|Any CPU + {C09B550A-314A-4719-88CF-DFBD549E0CA1}.Release|x86.Build.0 = Release|Any CPU + {6D3C9124-C29A-4BD9-B14D-B285E5F2481D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6D3C9124-C29A-4BD9-B14D-B285E5F2481D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6D3C9124-C29A-4BD9-B14D-B285E5F2481D}.Debug|x64.ActiveCfg = Debug|Any CPU + {6D3C9124-C29A-4BD9-B14D-B285E5F2481D}.Debug|x64.Build.0 = Debug|Any CPU + {6D3C9124-C29A-4BD9-B14D-B285E5F2481D}.Debug|x86.ActiveCfg = Debug|Any CPU + {6D3C9124-C29A-4BD9-B14D-B285E5F2481D}.Debug|x86.Build.0 = Debug|Any CPU + {6D3C9124-C29A-4BD9-B14D-B285E5F2481D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6D3C9124-C29A-4BD9-B14D-B285E5F2481D}.Release|Any CPU.Build.0 = Release|Any CPU + {6D3C9124-C29A-4BD9-B14D-B285E5F2481D}.Release|x64.ActiveCfg = Release|Any CPU + {6D3C9124-C29A-4BD9-B14D-B285E5F2481D}.Release|x64.Build.0 = Release|Any CPU + {6D3C9124-C29A-4BD9-B14D-B285E5F2481D}.Release|x86.ActiveCfg = Release|Any CPU + {6D3C9124-C29A-4BD9-B14D-B285E5F2481D}.Release|x86.Build.0 = Release|Any CPU + {08377B69-0AB0-471E-A743-A9C92436DC51}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {08377B69-0AB0-471E-A743-A9C92436DC51}.Debug|Any CPU.Build.0 = Debug|Any CPU + {08377B69-0AB0-471E-A743-A9C92436DC51}.Debug|x64.ActiveCfg = Debug|Any CPU + {08377B69-0AB0-471E-A743-A9C92436DC51}.Debug|x64.Build.0 = Debug|Any CPU + {08377B69-0AB0-471E-A743-A9C92436DC51}.Debug|x86.ActiveCfg = Debug|Any CPU + {08377B69-0AB0-471E-A743-A9C92436DC51}.Debug|x86.Build.0 = Debug|Any CPU + {08377B69-0AB0-471E-A743-A9C92436DC51}.Release|Any CPU.ActiveCfg = Release|Any CPU + {08377B69-0AB0-471E-A743-A9C92436DC51}.Release|Any CPU.Build.0 = Release|Any CPU + {08377B69-0AB0-471E-A743-A9C92436DC51}.Release|x64.ActiveCfg = Release|Any CPU + {08377B69-0AB0-471E-A743-A9C92436DC51}.Release|x64.Build.0 = Release|Any CPU + {08377B69-0AB0-471E-A743-A9C92436DC51}.Release|x86.ActiveCfg = Release|Any CPU + {08377B69-0AB0-471E-A743-A9C92436DC51}.Release|x86.Build.0 = Release|Any CPU + {1E4548D0-9272-41BD-9529-A40BE308A1BC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1E4548D0-9272-41BD-9529-A40BE308A1BC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1E4548D0-9272-41BD-9529-A40BE308A1BC}.Debug|x64.ActiveCfg = Debug|Any CPU + {1E4548D0-9272-41BD-9529-A40BE308A1BC}.Debug|x64.Build.0 = Debug|Any CPU + {1E4548D0-9272-41BD-9529-A40BE308A1BC}.Debug|x86.ActiveCfg = Debug|Any CPU + {1E4548D0-9272-41BD-9529-A40BE308A1BC}.Debug|x86.Build.0 = Debug|Any CPU + {1E4548D0-9272-41BD-9529-A40BE308A1BC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1E4548D0-9272-41BD-9529-A40BE308A1BC}.Release|Any CPU.Build.0 = Release|Any CPU + {1E4548D0-9272-41BD-9529-A40BE308A1BC}.Release|x64.ActiveCfg = Release|Any CPU + {1E4548D0-9272-41BD-9529-A40BE308A1BC}.Release|x64.Build.0 = Release|Any CPU + {1E4548D0-9272-41BD-9529-A40BE308A1BC}.Release|x86.ActiveCfg = Release|Any CPU + {1E4548D0-9272-41BD-9529-A40BE308A1BC}.Release|x86.Build.0 = Release|Any CPU + {4EC447F0-DE52-49E1-A798-4EA1CFF08036}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4EC447F0-DE52-49E1-A798-4EA1CFF08036}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4EC447F0-DE52-49E1-A798-4EA1CFF08036}.Debug|x64.ActiveCfg = Debug|Any CPU + {4EC447F0-DE52-49E1-A798-4EA1CFF08036}.Debug|x64.Build.0 = Debug|Any CPU + {4EC447F0-DE52-49E1-A798-4EA1CFF08036}.Debug|x86.ActiveCfg = Debug|Any CPU + {4EC447F0-DE52-49E1-A798-4EA1CFF08036}.Debug|x86.Build.0 = Debug|Any CPU + {4EC447F0-DE52-49E1-A798-4EA1CFF08036}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4EC447F0-DE52-49E1-A798-4EA1CFF08036}.Release|Any CPU.Build.0 = Release|Any CPU + {4EC447F0-DE52-49E1-A798-4EA1CFF08036}.Release|x64.ActiveCfg = Release|Any CPU + {4EC447F0-DE52-49E1-A798-4EA1CFF08036}.Release|x64.Build.0 = Release|Any CPU + {4EC447F0-DE52-49E1-A798-4EA1CFF08036}.Release|x86.ActiveCfg = Release|Any CPU + {4EC447F0-DE52-49E1-A798-4EA1CFF08036}.Release|x86.Build.0 = Release|Any CPU + {3AC27AD7-CBAC-422A-818F-669D0B63983B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3AC27AD7-CBAC-422A-818F-669D0B63983B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3AC27AD7-CBAC-422A-818F-669D0B63983B}.Debug|x64.ActiveCfg = Debug|Any CPU + {3AC27AD7-CBAC-422A-818F-669D0B63983B}.Debug|x64.Build.0 = Debug|Any CPU + {3AC27AD7-CBAC-422A-818F-669D0B63983B}.Debug|x86.ActiveCfg = Debug|Any CPU + {3AC27AD7-CBAC-422A-818F-669D0B63983B}.Debug|x86.Build.0 = Debug|Any CPU + {3AC27AD7-CBAC-422A-818F-669D0B63983B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3AC27AD7-CBAC-422A-818F-669D0B63983B}.Release|Any CPU.Build.0 = Release|Any CPU + {3AC27AD7-CBAC-422A-818F-669D0B63983B}.Release|x64.ActiveCfg = Release|Any CPU + {3AC27AD7-CBAC-422A-818F-669D0B63983B}.Release|x64.Build.0 = Release|Any CPU + {3AC27AD7-CBAC-422A-818F-669D0B63983B}.Release|x86.ActiveCfg = Release|Any CPU + {3AC27AD7-CBAC-422A-818F-669D0B63983B}.Release|x86.Build.0 = Release|Any CPU + {C3DDE3A6-217A-4DE6-B5E9-1E31AC4E0B35}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C3DDE3A6-217A-4DE6-B5E9-1E31AC4E0B35}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C3DDE3A6-217A-4DE6-B5E9-1E31AC4E0B35}.Debug|x64.ActiveCfg = Debug|Any CPU + {C3DDE3A6-217A-4DE6-B5E9-1E31AC4E0B35}.Debug|x64.Build.0 = Debug|Any CPU + {C3DDE3A6-217A-4DE6-B5E9-1E31AC4E0B35}.Debug|x86.ActiveCfg = Debug|Any CPU + {C3DDE3A6-217A-4DE6-B5E9-1E31AC4E0B35}.Debug|x86.Build.0 = Debug|Any CPU + {C3DDE3A6-217A-4DE6-B5E9-1E31AC4E0B35}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C3DDE3A6-217A-4DE6-B5E9-1E31AC4E0B35}.Release|Any CPU.Build.0 = Release|Any CPU + {C3DDE3A6-217A-4DE6-B5E9-1E31AC4E0B35}.Release|x64.ActiveCfg = Release|Any CPU + {C3DDE3A6-217A-4DE6-B5E9-1E31AC4E0B35}.Release|x64.Build.0 = Release|Any CPU + {C3DDE3A6-217A-4DE6-B5E9-1E31AC4E0B35}.Release|x86.ActiveCfg = Release|Any CPU + {C3DDE3A6-217A-4DE6-B5E9-1E31AC4E0B35}.Release|x86.Build.0 = Release|Any CPU + {E9BE6A86-1AF1-4291-A42E-21BDD384CBDA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E9BE6A86-1AF1-4291-A42E-21BDD384CBDA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E9BE6A86-1AF1-4291-A42E-21BDD384CBDA}.Debug|x64.ActiveCfg = Debug|Any CPU + {E9BE6A86-1AF1-4291-A42E-21BDD384CBDA}.Debug|x64.Build.0 = Debug|Any CPU + {E9BE6A86-1AF1-4291-A42E-21BDD384CBDA}.Debug|x86.ActiveCfg = Debug|Any CPU + {E9BE6A86-1AF1-4291-A42E-21BDD384CBDA}.Debug|x86.Build.0 = Debug|Any CPU + {E9BE6A86-1AF1-4291-A42E-21BDD384CBDA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E9BE6A86-1AF1-4291-A42E-21BDD384CBDA}.Release|Any CPU.Build.0 = Release|Any CPU + {E9BE6A86-1AF1-4291-A42E-21BDD384CBDA}.Release|x64.ActiveCfg = Release|Any CPU + {E9BE6A86-1AF1-4291-A42E-21BDD384CBDA}.Release|x64.Build.0 = Release|Any CPU + {E9BE6A86-1AF1-4291-A42E-21BDD384CBDA}.Release|x86.ActiveCfg = Release|Any CPU + {E9BE6A86-1AF1-4291-A42E-21BDD384CBDA}.Release|x86.Build.0 = Release|Any CPU + {31544218-76AF-4ADA-B779-9C793E9686D8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {31544218-76AF-4ADA-B779-9C793E9686D8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {31544218-76AF-4ADA-B779-9C793E9686D8}.Debug|x64.ActiveCfg = Debug|Any CPU + {31544218-76AF-4ADA-B779-9C793E9686D8}.Debug|x64.Build.0 = Debug|Any CPU + {31544218-76AF-4ADA-B779-9C793E9686D8}.Debug|x86.ActiveCfg = Debug|Any CPU + {31544218-76AF-4ADA-B779-9C793E9686D8}.Debug|x86.Build.0 = Debug|Any CPU + {31544218-76AF-4ADA-B779-9C793E9686D8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {31544218-76AF-4ADA-B779-9C793E9686D8}.Release|Any CPU.Build.0 = Release|Any CPU + {31544218-76AF-4ADA-B779-9C793E9686D8}.Release|x64.ActiveCfg = Release|Any CPU + {31544218-76AF-4ADA-B779-9C793E9686D8}.Release|x64.Build.0 = Release|Any CPU + {31544218-76AF-4ADA-B779-9C793E9686D8}.Release|x86.ActiveCfg = Release|Any CPU + {31544218-76AF-4ADA-B779-9C793E9686D8}.Release|x86.Build.0 = Release|Any CPU + {CF2A7FD7-E5F7-4810-A5E3-0D40269F8E1B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CF2A7FD7-E5F7-4810-A5E3-0D40269F8E1B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CF2A7FD7-E5F7-4810-A5E3-0D40269F8E1B}.Debug|x64.ActiveCfg = Debug|Any CPU + {CF2A7FD7-E5F7-4810-A5E3-0D40269F8E1B}.Debug|x64.Build.0 = Debug|Any CPU + {CF2A7FD7-E5F7-4810-A5E3-0D40269F8E1B}.Debug|x86.ActiveCfg = Debug|Any CPU + {CF2A7FD7-E5F7-4810-A5E3-0D40269F8E1B}.Debug|x86.Build.0 = Debug|Any CPU + {CF2A7FD7-E5F7-4810-A5E3-0D40269F8E1B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CF2A7FD7-E5F7-4810-A5E3-0D40269F8E1B}.Release|Any CPU.Build.0 = Release|Any CPU + {CF2A7FD7-E5F7-4810-A5E3-0D40269F8E1B}.Release|x64.ActiveCfg = Release|Any CPU + {CF2A7FD7-E5F7-4810-A5E3-0D40269F8E1B}.Release|x64.Build.0 = Release|Any CPU + {CF2A7FD7-E5F7-4810-A5E3-0D40269F8E1B}.Release|x86.ActiveCfg = Release|Any CPU + {CF2A7FD7-E5F7-4810-A5E3-0D40269F8E1B}.Release|x86.Build.0 = Release|Any CPU + {F19C3D33-FACE-4217-AC9B-519BE901CDF0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F19C3D33-FACE-4217-AC9B-519BE901CDF0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F19C3D33-FACE-4217-AC9B-519BE901CDF0}.Debug|x64.ActiveCfg = Debug|Any CPU + {F19C3D33-FACE-4217-AC9B-519BE901CDF0}.Debug|x64.Build.0 = Debug|Any CPU + {F19C3D33-FACE-4217-AC9B-519BE901CDF0}.Debug|x86.ActiveCfg = Debug|Any CPU + {F19C3D33-FACE-4217-AC9B-519BE901CDF0}.Debug|x86.Build.0 = Debug|Any CPU + {F19C3D33-FACE-4217-AC9B-519BE901CDF0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F19C3D33-FACE-4217-AC9B-519BE901CDF0}.Release|Any CPU.Build.0 = Release|Any CPU + {F19C3D33-FACE-4217-AC9B-519BE901CDF0}.Release|x64.ActiveCfg = Release|Any CPU + {F19C3D33-FACE-4217-AC9B-519BE901CDF0}.Release|x64.Build.0 = Release|Any CPU + {F19C3D33-FACE-4217-AC9B-519BE901CDF0}.Release|x86.ActiveCfg = Release|Any CPU + {F19C3D33-FACE-4217-AC9B-519BE901CDF0}.Release|x86.Build.0 = Release|Any CPU + {8D384B62-F15F-4BDF-BE33-17BDE81B3599}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8D384B62-F15F-4BDF-BE33-17BDE81B3599}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8D384B62-F15F-4BDF-BE33-17BDE81B3599}.Debug|x64.ActiveCfg = Debug|Any CPU + {8D384B62-F15F-4BDF-BE33-17BDE81B3599}.Debug|x64.Build.0 = Debug|Any CPU + {8D384B62-F15F-4BDF-BE33-17BDE81B3599}.Debug|x86.ActiveCfg = Debug|Any CPU + {8D384B62-F15F-4BDF-BE33-17BDE81B3599}.Debug|x86.Build.0 = Debug|Any CPU + {8D384B62-F15F-4BDF-BE33-17BDE81B3599}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8D384B62-F15F-4BDF-BE33-17BDE81B3599}.Release|Any CPU.Build.0 = Release|Any CPU + {8D384B62-F15F-4BDF-BE33-17BDE81B3599}.Release|x64.ActiveCfg = Release|Any CPU + {8D384B62-F15F-4BDF-BE33-17BDE81B3599}.Release|x64.Build.0 = Release|Any CPU + {8D384B62-F15F-4BDF-BE33-17BDE81B3599}.Release|x86.ActiveCfg = Release|Any CPU + {8D384B62-F15F-4BDF-BE33-17BDE81B3599}.Release|x86.Build.0 = Release|Any CPU + {3FDB9AC2-4CFC-48C7-99A5-8C24050886F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3FDB9AC2-4CFC-48C7-99A5-8C24050886F7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3FDB9AC2-4CFC-48C7-99A5-8C24050886F7}.Debug|x64.ActiveCfg = Debug|Any CPU + {3FDB9AC2-4CFC-48C7-99A5-8C24050886F7}.Debug|x64.Build.0 = Debug|Any CPU + {3FDB9AC2-4CFC-48C7-99A5-8C24050886F7}.Debug|x86.ActiveCfg = Debug|Any CPU + {3FDB9AC2-4CFC-48C7-99A5-8C24050886F7}.Debug|x86.Build.0 = Debug|Any CPU + {3FDB9AC2-4CFC-48C7-99A5-8C24050886F7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3FDB9AC2-4CFC-48C7-99A5-8C24050886F7}.Release|Any CPU.Build.0 = Release|Any CPU + {3FDB9AC2-4CFC-48C7-99A5-8C24050886F7}.Release|x64.ActiveCfg = Release|Any CPU + {3FDB9AC2-4CFC-48C7-99A5-8C24050886F7}.Release|x64.Build.0 = Release|Any CPU + {3FDB9AC2-4CFC-48C7-99A5-8C24050886F7}.Release|x86.ActiveCfg = Release|Any CPU + {3FDB9AC2-4CFC-48C7-99A5-8C24050886F7}.Release|x86.Build.0 = Release|Any CPU + {CF3DD636-CCC5-448C-A083-F2961B2FB3F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CF3DD636-CCC5-448C-A083-F2961B2FB3F6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CF3DD636-CCC5-448C-A083-F2961B2FB3F6}.Debug|x64.ActiveCfg = Debug|Any CPU + {CF3DD636-CCC5-448C-A083-F2961B2FB3F6}.Debug|x64.Build.0 = Debug|Any CPU + {CF3DD636-CCC5-448C-A083-F2961B2FB3F6}.Debug|x86.ActiveCfg = Debug|Any CPU + {CF3DD636-CCC5-448C-A083-F2961B2FB3F6}.Debug|x86.Build.0 = Debug|Any CPU + {CF3DD636-CCC5-448C-A083-F2961B2FB3F6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CF3DD636-CCC5-448C-A083-F2961B2FB3F6}.Release|Any CPU.Build.0 = Release|Any CPU + {CF3DD636-CCC5-448C-A083-F2961B2FB3F6}.Release|x64.ActiveCfg = Release|Any CPU + {CF3DD636-CCC5-448C-A083-F2961B2FB3F6}.Release|x64.Build.0 = Release|Any CPU + {CF3DD636-CCC5-448C-A083-F2961B2FB3F6}.Release|x86.ActiveCfg = Release|Any CPU + {CF3DD636-CCC5-448C-A083-F2961B2FB3F6}.Release|x86.Build.0 = Release|Any CPU + {44A3DE13-CC1A-4331-8551-30F52E67510C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {44A3DE13-CC1A-4331-8551-30F52E67510C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {44A3DE13-CC1A-4331-8551-30F52E67510C}.Debug|x64.ActiveCfg = Debug|Any CPU + {44A3DE13-CC1A-4331-8551-30F52E67510C}.Debug|x64.Build.0 = Debug|Any CPU + {44A3DE13-CC1A-4331-8551-30F52E67510C}.Debug|x86.ActiveCfg = Debug|Any CPU + {44A3DE13-CC1A-4331-8551-30F52E67510C}.Debug|x86.Build.0 = Debug|Any CPU + {44A3DE13-CC1A-4331-8551-30F52E67510C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {44A3DE13-CC1A-4331-8551-30F52E67510C}.Release|Any CPU.Build.0 = Release|Any CPU + {44A3DE13-CC1A-4331-8551-30F52E67510C}.Release|x64.ActiveCfg = Release|Any CPU + {44A3DE13-CC1A-4331-8551-30F52E67510C}.Release|x64.Build.0 = Release|Any CPU + {44A3DE13-CC1A-4331-8551-30F52E67510C}.Release|x86.ActiveCfg = Release|Any CPU + {44A3DE13-CC1A-4331-8551-30F52E67510C}.Release|x86.Build.0 = Release|Any CPU + {162D0F7E-3313-40B1-97AC-16198CB0F6BA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {162D0F7E-3313-40B1-97AC-16198CB0F6BA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {162D0F7E-3313-40B1-97AC-16198CB0F6BA}.Debug|x64.ActiveCfg = Debug|Any CPU + {162D0F7E-3313-40B1-97AC-16198CB0F6BA}.Debug|x64.Build.0 = Debug|Any CPU + {162D0F7E-3313-40B1-97AC-16198CB0F6BA}.Debug|x86.ActiveCfg = Debug|Any CPU + {162D0F7E-3313-40B1-97AC-16198CB0F6BA}.Debug|x86.Build.0 = Debug|Any CPU + {162D0F7E-3313-40B1-97AC-16198CB0F6BA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {162D0F7E-3313-40B1-97AC-16198CB0F6BA}.Release|Any CPU.Build.0 = Release|Any CPU + {162D0F7E-3313-40B1-97AC-16198CB0F6BA}.Release|x64.ActiveCfg = Release|Any CPU + {162D0F7E-3313-40B1-97AC-16198CB0F6BA}.Release|x64.Build.0 = Release|Any CPU + {162D0F7E-3313-40B1-97AC-16198CB0F6BA}.Release|x86.ActiveCfg = Release|Any CPU + {162D0F7E-3313-40B1-97AC-16198CB0F6BA}.Release|x86.Build.0 = Release|Any CPU + {7492C8D3-B033-45F8-A826-560B925EAFD9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7492C8D3-B033-45F8-A826-560B925EAFD9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7492C8D3-B033-45F8-A826-560B925EAFD9}.Debug|x64.ActiveCfg = Debug|Any CPU + {7492C8D3-B033-45F8-A826-560B925EAFD9}.Debug|x64.Build.0 = Debug|Any CPU + {7492C8D3-B033-45F8-A826-560B925EAFD9}.Debug|x86.ActiveCfg = Debug|Any CPU + {7492C8D3-B033-45F8-A826-560B925EAFD9}.Debug|x86.Build.0 = Debug|Any CPU + {7492C8D3-B033-45F8-A826-560B925EAFD9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7492C8D3-B033-45F8-A826-560B925EAFD9}.Release|Any CPU.Build.0 = Release|Any CPU + {7492C8D3-B033-45F8-A826-560B925EAFD9}.Release|x64.ActiveCfg = Release|Any CPU + {7492C8D3-B033-45F8-A826-560B925EAFD9}.Release|x64.Build.0 = Release|Any CPU + {7492C8D3-B033-45F8-A826-560B925EAFD9}.Release|x86.ActiveCfg = Release|Any CPU + {7492C8D3-B033-45F8-A826-560B925EAFD9}.Release|x86.Build.0 = Release|Any CPU + {41E9DD28-3F40-4288-B4CA-D2395BFA3B9E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {41E9DD28-3F40-4288-B4CA-D2395BFA3B9E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {41E9DD28-3F40-4288-B4CA-D2395BFA3B9E}.Debug|x64.ActiveCfg = Debug|Any CPU + {41E9DD28-3F40-4288-B4CA-D2395BFA3B9E}.Debug|x64.Build.0 = Debug|Any CPU + {41E9DD28-3F40-4288-B4CA-D2395BFA3B9E}.Debug|x86.ActiveCfg = Debug|Any CPU + {41E9DD28-3F40-4288-B4CA-D2395BFA3B9E}.Debug|x86.Build.0 = Debug|Any CPU + {41E9DD28-3F40-4288-B4CA-D2395BFA3B9E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {41E9DD28-3F40-4288-B4CA-D2395BFA3B9E}.Release|Any CPU.Build.0 = Release|Any CPU + {41E9DD28-3F40-4288-B4CA-D2395BFA3B9E}.Release|x64.ActiveCfg = Release|Any CPU + {41E9DD28-3F40-4288-B4CA-D2395BFA3B9E}.Release|x64.Build.0 = Release|Any CPU + {41E9DD28-3F40-4288-B4CA-D2395BFA3B9E}.Release|x86.ActiveCfg = Release|Any CPU + {41E9DD28-3F40-4288-B4CA-D2395BFA3B9E}.Release|x86.Build.0 = Release|Any CPU + {C6087B8C-3C57-4593-A340-A4D7BDCD8259}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C6087B8C-3C57-4593-A340-A4D7BDCD8259}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C6087B8C-3C57-4593-A340-A4D7BDCD8259}.Debug|x64.ActiveCfg = Debug|Any CPU + {C6087B8C-3C57-4593-A340-A4D7BDCD8259}.Debug|x64.Build.0 = Debug|Any CPU + {C6087B8C-3C57-4593-A340-A4D7BDCD8259}.Debug|x86.ActiveCfg = Debug|Any CPU + {C6087B8C-3C57-4593-A340-A4D7BDCD8259}.Debug|x86.Build.0 = Debug|Any CPU + {C6087B8C-3C57-4593-A340-A4D7BDCD8259}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C6087B8C-3C57-4593-A340-A4D7BDCD8259}.Release|Any CPU.Build.0 = Release|Any CPU + {C6087B8C-3C57-4593-A340-A4D7BDCD8259}.Release|x64.ActiveCfg = Release|Any CPU + {C6087B8C-3C57-4593-A340-A4D7BDCD8259}.Release|x64.Build.0 = Release|Any CPU + {C6087B8C-3C57-4593-A340-A4D7BDCD8259}.Release|x86.ActiveCfg = Release|Any CPU + {C6087B8C-3C57-4593-A340-A4D7BDCD8259}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {1B2C9807-0067-AAD6-69CD-7FD799689BDD} = {EB157E4F-3EA5-5CBF-9694-18E3FA265E53} + {4B16E448-1B2D-28B7-2417-D2D191FE524F} = {EB157E4F-3EA5-5CBF-9694-18E3FA265E53} + {9EAD58C8-4DCD-4933-B9F9-7D211C66D73F} = {EB157E4F-3EA5-5CBF-9694-18E3FA265E53} + {0E5FA5F8-5C99-7868-B1D0-1D38A9624795} = {EB157E4F-3EA5-5CBF-9694-18E3FA265E53} + {F310596E-88BB-9E54-885E-21C61971917E} = {5B52EF8A-3661-DCFF-797D-BC4D6AC60BDA} + {EA6E5683-3A20-2E52-1CE6-AE0D6D36AC4D} = {F310596E-88BB-9E54-885E-21C61971917E} + {D9492ED1-A812-924B-65E4-F518592B49BB} = {F310596E-88BB-9E54-885E-21C61971917E} + {3823DE1E-2ACE-C956-99E1-00DB786D9E1D} = {D9492ED1-A812-924B-65E4-F518592B49BB} + {03DFF14F-7321-1784-D4C7-4E99D4120F48} = {5B52EF8A-3661-DCFF-797D-BC4D6AC60BDA} + {BDD326D6-7616-84F0-B914-74743BFBA520} = {03DFF14F-7321-1784-D4C7-4E99D4120F48} + {EC506DBE-AB6D-492E-786E-8B176021BF2E} = {BDD326D6-7616-84F0-B914-74743BFBA520} + {5AC09D9A-F2A5-9CFA-B3C5-8D25F257651C} = {5B52EF8A-3661-DCFF-797D-BC4D6AC60BDA} + {33B1AE27-692A-1778-48C1-CCEC2B9BC78F} = {5AC09D9A-F2A5-9CFA-B3C5-8D25F257651C} + {018E0E11-1CCE-A2BE-641D-21EE14D2E90D} = {5AC09D9A-F2A5-9CFA-B3C5-8D25F257651C} + {5F27FB4E-CF09-3A6B-F5B4-BF5A709FA609} = {33B1AE27-692A-1778-48C1-CCEC2B9BC78F} + {AB67BDB9-D701-3AC9-9CDF-ECCDCCD8DB6D} = {5AC09D9A-F2A5-9CFA-B3C5-8D25F257651C} + {3F605548-87E2-8A1D-306D-0CE6960B8242} = {AB67BDB9-D701-3AC9-9CDF-ECCDCCD8DB6D} + {45F7FA87-7451-6970-7F6E-F8BAE45E081B} = {AB67BDB9-D701-3AC9-9CDF-ECCDCCD8DB6D} + {C1DCEFBD-12A5-EAAE-632E-8EEB9BE491B6} = {5B52EF8A-3661-DCFF-797D-BC4D6AC60BDA} + {A6928CBA-4D4D-AB2B-CA19-FEE6E73ECE70} = {C1DCEFBD-12A5-EAAE-632E-8EEB9BE491B6} + {F2E6CB0E-DF77-1FAA-582B-62B040DF3848} = {A6928CBA-4D4D-AB2B-CA19-FEE6E73ECE70} + {C494ECBE-DEA5-3576-D2AF-200FF12BC144} = {A6928CBA-4D4D-AB2B-CA19-FEE6E73ECE70} + {7E890DF9-B715-B6DF-2498-FD74DDA87D71} = {A6928CBA-4D4D-AB2B-CA19-FEE6E73ECE70} + {64689413-46D7-8499-68A6-B6367ACBC597} = {A6928CBA-4D4D-AB2B-CA19-FEE6E73ECE70} + {5827F4DE-0AA7-FC85-641D-09E3D890DB27} = {C1DCEFBD-12A5-EAAE-632E-8EEB9BE491B6} + {9BD75659-58CB-06D1-E198-C39007E82C6A} = {5827F4DE-0AA7-FC85-641D-09E3D890DB27} + {7BF13935-F1DD-D23B-8347-DB1550C69D69} = {5827F4DE-0AA7-FC85-641D-09E3D890DB27} + {2949F2E7-0E10-76D0-5672-8B1662588F74} = {5B52EF8A-3661-DCFF-797D-BC4D6AC60BDA} + {3ADE81EE-6F71-CB5C-016B-36E8AC854713} = {2949F2E7-0E10-76D0-5672-8B1662588F74} + {24D60BC6-6A93-C97D-1238-113DDB928700} = {3ADE81EE-6F71-CB5C-016B-36E8AC854713} + {CCF230F8-F75D-A766-7EAE-0C9FEF5AF6C2} = {3ADE81EE-6F71-CB5C-016B-36E8AC854713} + {066DF6C9-826C-F223-47D2-BDF53D59F6C3} = {3ADE81EE-6F71-CB5C-016B-36E8AC854713} + {61C7FDA3-83AA-3EE6-6321-1C1ACD1073DF} = {3ADE81EE-6F71-CB5C-016B-36E8AC854713} + {BF6C9274-4DBD-2FDE-B94C-1B208F6C53BC} = {3ADE81EE-6F71-CB5C-016B-36E8AC854713} + {4EAAC62E-EBD2-DFF1-7B37-7E131C75DEC3} = {3ADE81EE-6F71-CB5C-016B-36E8AC854713} + {157C3671-CA0B-69FA-A7C9-74A1FDA97B99} = {5B52EF8A-3661-DCFF-797D-BC4D6AC60BDA} + {F39E09D6-BF93-B64A-CFE7-2BA92815C0FE} = {157C3671-CA0B-69FA-A7C9-74A1FDA97B99} + {39EFDA5B-F5EE-8212-D5BA-90E1B82013E7} = {F39E09D6-BF93-B64A-CFE7-2BA92815C0FE} + {3B82DBF3-3DAE-EA97-85F4-6DCFA09940DF} = {F39E09D6-BF93-B64A-CFE7-2BA92815C0FE} + {6844B539-C2A3-9D4F-139D-9D533BCABADA} = {F39E09D6-BF93-B64A-CFE7-2BA92815C0FE} + {4263AA71-0335-3F44-9A9B-423C3A3D05E6} = {F39E09D6-BF93-B64A-CFE7-2BA92815C0FE} + {F1B1DB47-D2D7-59CB-679B-23E4928E8328} = {F39E09D6-BF93-B64A-CFE7-2BA92815C0FE} + {BC35DE94-4F04-3436-27A3-F11647FEDD5C} = {F39E09D6-BF93-B64A-CFE7-2BA92815C0FE} + {864C8B80-771A-0C15-30A5-558F99006E0D} = {F39E09D6-BF93-B64A-CFE7-2BA92815C0FE} + {603E7A23-1D6B-D3A9-B0E6-3E332B13ED5C} = {F39E09D6-BF93-B64A-CFE7-2BA92815C0FE} + {D2F7E58B-47D4-5205-D917-144CA1CFF4F1} = {F39E09D6-BF93-B64A-CFE7-2BA92815C0FE} + {1DCF4EBB-DBC4-752C-13D4-D1EECE4E8907} = {F39E09D6-BF93-B64A-CFE7-2BA92815C0FE} + {1B37A859-E733-60CB-4806-1A24B6F10E05} = {F39E09D6-BF93-B64A-CFE7-2BA92815C0FE} + {F2B58F4E-6F28-A25F-5BFB-CDEBAD6B9A3E} = {F39E09D6-BF93-B64A-CFE7-2BA92815C0FE} + {7D49FA52-6EA1-EAC8-4C5A-AC07188D6C57} = {5B52EF8A-3661-DCFF-797D-BC4D6AC60BDA} + {C9CF27FC-12DB-954F-863C-576BA8E309A5} = {7D49FA52-6EA1-EAC8-4C5A-AC07188D6C57} + {6DCAF6F3-717F-27A9-D96C-F2BFA5550347} = {C9CF27FC-12DB-954F-863C-576BA8E309A5} + {C4A90603-BE42-0044-CAB4-3EB910AD51A5} = {5B52EF8A-3661-DCFF-797D-BC4D6AC60BDA} + {054761F9-16D3-B2F8-6F4D-EFC2248805CD} = {C4A90603-BE42-0044-CAB4-3EB910AD51A5} + {B54CE64C-4167-1DD1-B7D6-2FD7A5AEF715} = {C4A90603-BE42-0044-CAB4-3EB910AD51A5} + {D2162FEA-AFA4-2A88-6444-2F6D845260BB} = {5B52EF8A-3661-DCFF-797D-BC4D6AC60BDA} + {63EAEA3B-ADC9-631D-774E-7AA04490EDDD} = {D2162FEA-AFA4-2A88-6444-2F6D845260BB} + {B0F64757-F7A7-1A11-8DEC-BAC72EB5EC29} = {63EAEA3B-ADC9-631D-774E-7AA04490EDDD} + {8E6B774C-CC4E-CE7C-AD4B-8AF7C92889A6} = {5B52EF8A-3661-DCFF-797D-BC4D6AC60BDA} + {BC12ED55-6015-7C8B-8384-B39CE93C76D6} = {8E6B774C-CC4E-CE7C-AD4B-8AF7C92889A6} + {FF70543D-AFF9-1D38-4950-4F8EE18D60BB} = {8E6B774C-CC4E-CE7C-AD4B-8AF7C92889A6} + {831265B0-8896-9C95-3488-E12FD9F6DC53} = {FF70543D-AFF9-1D38-4950-4F8EE18D60BB} + {316BBD0A-04D2-85C9-52EA-7993CC6C8930} = {5B52EF8A-3661-DCFF-797D-BC4D6AC60BDA} + {9D6AB85A-85EA-D85A-5566-A121D34016E6} = {316BBD0A-04D2-85C9-52EA-7993CC6C8930} + {FC018E5B-1E2F-DE19-1E97-0C845058C469} = {5B52EF8A-3661-DCFF-797D-BC4D6AC60BDA} + {1BE5B76C-B486-560B-6CB2-44C6537249AA} = {FC018E5B-1E2F-DE19-1E97-0C845058C469} + {F4F1CBE2-1CDD-CAA4-41F0-266DB4677C05} = {1BE5B76C-B486-560B-6CB2-44C6537249AA} + {3DE1DCDC-C845-4AC7-7B66-34B0A9E8626B} = {1BE5B76C-B486-560B-6CB2-44C6537249AA} + {6FA01E92-606B-0CB8-8583-6F693A903CFC} = {1BE5B76C-B486-560B-6CB2-44C6537249AA} + {A5994E92-7E0E-89FE-5628-DE1A0176B8BA} = {1BE5B76C-B486-560B-6CB2-44C6537249AA} + {54C11B29-4C54-7255-AB44-BEB63AF9BD1F} = {1BE5B76C-B486-560B-6CB2-44C6537249AA} + {AD65DDE7-9FEA-7380-8C10-FA165F745354} = {5B52EF8A-3661-DCFF-797D-BC4D6AC60BDA} + {076B8074-5735-5367-1EEA-CA16A5B8ABD7} = {AD65DDE7-9FEA-7380-8C10-FA165F745354} + {3247EE0D-B3E9-9C11-B0AE-FE719410390B} = {5B52EF8A-3661-DCFF-797D-BC4D6AC60BDA} + {CD7C09DA-FEC8-2CC5-D00C-E525638DFF4A} = {3247EE0D-B3E9-9C11-B0AE-FE719410390B} + {79B10804-91E9-972E-1913-EE0F0B11663E} = {CD7C09DA-FEC8-2CC5-D00C-E525638DFF4A} + {A4E2971A-7DCF-D72F-631D-98564D1D1E5D} = {5B52EF8A-3661-DCFF-797D-BC4D6AC60BDA} + {01B2B5BA-58CD-9920-5A1E-6B801BF36685} = {A4E2971A-7DCF-D72F-631D-98564D1D1E5D} + {3C8A19A6-5578-51E9-E592-6DABBE5739E5} = {01B2B5BA-58CD-9920-5A1E-6B801BF36685} + {8403C362-DC2B-AF41-C9D8-E4F2D892DF8E} = {5B52EF8A-3661-DCFF-797D-BC4D6AC60BDA} + {B75836D4-A244-20B8-4707-64E17C725DAA} = {8403C362-DC2B-AF41-C9D8-E4F2D892DF8E} + {0910C958-24C8-947F-359A-218ED1199AAE} = {B75836D4-A244-20B8-4707-64E17C725DAA} + {1345DD29-BB3A-FB5F-4B3D-E29F6045A27A} = {5B52EF8A-3661-DCFF-797D-BC4D6AC60BDA} + {9C2DD234-FA33-FDB6-86F0-EF9B75A13450} = {1345DD29-BB3A-FB5F-4B3D-E29F6045A27A} + {79E122F4-2325-3E92-438E-5825A307B594} = {1345DD29-BB3A-FB5F-4B3D-E29F6045A27A} + {538E2D98-5325-3F54-BE74-EFE5FC1ECBD8} = {1345DD29-BB3A-FB5F-4B3D-E29F6045A27A} + {66557252-B5C4-664B-D807-07018C627474} = {1345DD29-BB3A-FB5F-4B3D-E29F6045A27A} + {7203223D-FF02-7BEB-2798-D1639ACC01C4} = {1345DD29-BB3A-FB5F-4B3D-E29F6045A27A} + {5AC9EE40-1881-5F8A-46A2-2C303950D3C8} = {1345DD29-BB3A-FB5F-4B3D-E29F6045A27A} + {927E3CD3-4C20-4DE5-A395-D0977152A8D3} = {1345DD29-BB3A-FB5F-4B3D-E29F6045A27A} + {3C69853C-90E3-D889-1960-3B9229882590} = {1345DD29-BB3A-FB5F-4B3D-E29F6045A27A} + {9FB0DDD7-7A77-8DA4-F9E2-A94E60ED8FC7} = {1345DD29-BB3A-FB5F-4B3D-E29F6045A27A} + {643E4D4C-BC96-A37F-E0EC-488127F0B127} = {1345DD29-BB3A-FB5F-4B3D-E29F6045A27A} + {6F2CA7F5-3E7C-C61B-94E6-E7DD1227B5B1} = {1345DD29-BB3A-FB5F-4B3D-E29F6045A27A} + {F04B7DBB-77A5-C978-B2DE-8C189A32AA72} = {1345DD29-BB3A-FB5F-4B3D-E29F6045A27A} + {7C72F22A-20FF-DF5B-9191-6DFD0D497DB2} = {1345DD29-BB3A-FB5F-4B3D-E29F6045A27A} + {C896CC0A-F5E6-9AA4-C582-E691441F8D32} = {1345DD29-BB3A-FB5F-4B3D-E29F6045A27A} + {0AA3A418-AB45-CCA4-46D4-EEBFE011FECA} = {1345DD29-BB3A-FB5F-4B3D-E29F6045A27A} + {225D9926-4AE8-E539-70AD-8698E688F271} = {1345DD29-BB3A-FB5F-4B3D-E29F6045A27A} + {D6E8E69C-F721-BBCB-8C39-9716D53D72AD} = {1345DD29-BB3A-FB5F-4B3D-E29F6045A27A} + {9529EE99-D6A5-B570-EB1F-15BD2D57DFE2} = {1345DD29-BB3A-FB5F-4B3D-E29F6045A27A} + {589A43FD-8213-E9E3-6CFF-9CBA72D53E98} = {1345DD29-BB3A-FB5F-4B3D-E29F6045A27A} + {2BACF7E3-1278-FE99-8343-8221E6FBA9DE} = {1345DD29-BB3A-FB5F-4B3D-E29F6045A27A} + {75E47125-E4D7-8482-F1A4-726564970864} = {1345DD29-BB3A-FB5F-4B3D-E29F6045A27A} + {FCD529E0-DD17-6587-B29C-12D425C0AD0C} = {1345DD29-BB3A-FB5F-4B3D-E29F6045A27A} + {61B23570-4F2D-B060-BE1F-37995682E494} = {1345DD29-BB3A-FB5F-4B3D-E29F6045A27A} + {1182764D-2143-EEF0-9270-3DCE392F5D06} = {1345DD29-BB3A-FB5F-4B3D-E29F6045A27A} + {772B02B5-6280-E1D4-3E2E-248D0455C2FB} = {1345DD29-BB3A-FB5F-4B3D-E29F6045A27A} + {48F90289-938C-CCA7-B60F-D2143E7C9A69} = {1345DD29-BB3A-FB5F-4B3D-E29F6045A27A} + {E69FA1A0-6D1B-A6E4-2DC0-8F4C5F21BF04} = {1345DD29-BB3A-FB5F-4B3D-E29F6045A27A} + {083067CF-CE89-EF39-9BD3-4741919E26F3} = {1345DD29-BB3A-FB5F-4B3D-E29F6045A27A} + {8380A20C-A5B8-EE91-1A58-270323688CB9} = {1345DD29-BB3A-FB5F-4B3D-E29F6045A27A} + {A7542386-71EB-4F34-E1CE-27D399325955} = {1345DD29-BB3A-FB5F-4B3D-E29F6045A27A} + {90659617-4DF7-809A-4E5B-29BB5A98E8E1} = {5B52EF8A-3661-DCFF-797D-BC4D6AC60BDA} + {AB8B269C-5A2A-A4B8-0488-B5F81E55B4D9} = {90659617-4DF7-809A-4E5B-29BB5A98E8E1} + {CEDC2447-F717-3C95-7E08-F214D575A7B7} = {AB8B269C-5A2A-A4B8-0488-B5F81E55B4D9} + {2F4EB1B2-4185-C535-85ED-53EA2D2C0573} = {A5C98087-E847-D2C4-2143-20869479839D} + {79987C33-9903-4F75-1045-284F537155FE} = {A5C98087-E847-D2C4-2143-20869479839D} + {730E1137-BA40-3899-17CB-93922F4DA4CA} = {A5C98087-E847-D2C4-2143-20869479839D} + {EEF284CB-436C-439C-9606-E4DD1FD5F6D3} = {A5C98087-E847-D2C4-2143-20869479839D} + {891DD979-9F3C-981D-FB41-EB66B7C61938} = {A5C98087-E847-D2C4-2143-20869479839D} + {E5B2A8AA-8162-7E98-9D31-3DA40BB8EDC2} = {A5C98087-E847-D2C4-2143-20869479839D} + {29E594C5-9DE1-D06F-4F19-4166C3935CAE} = {A5C98087-E847-D2C4-2143-20869479839D} + {BE6FF48D-FDD8-A0C3-69E0-CAE30D3FC13F} = {A5C98087-E847-D2C4-2143-20869479839D} + {8FF79C15-5E56-BB59-A473-0E42F8395C89} = {A5C98087-E847-D2C4-2143-20869479839D} + {1003D3C6-91C9-D165-8325-20F3A16D9A5B} = {A5C98087-E847-D2C4-2143-20869479839D} + {278C2043-B1CE-CC57-7F08-9C4B69C7A65D} = {A5C98087-E847-D2C4-2143-20869479839D} + {F783A1A9-4991-592A-57C3-E18C41986545} = {A5C98087-E847-D2C4-2143-20869479839D} + {9646F3EA-8DFC-F221-D415-D489C166889B} = {A5C98087-E847-D2C4-2143-20869479839D} + {079CEB57-D0A3-20A1-131E-37D0CA4C0410} = {A5C98087-E847-D2C4-2143-20869479839D} + {49ECEC98-A019-AEB6-1632-D05B0DCB6EEA} = {A5C98087-E847-D2C4-2143-20869479839D} + {7C05D5D7-330C-204C-430C-8B2B27AB9995} = {A5C98087-E847-D2C4-2143-20869479839D} + {AA9E4361-01CE-AC2F-E20D-0CA5C1D438CC} = {A5C98087-E847-D2C4-2143-20869479839D} + {3C0D80C5-B033-CE52-8868-614826F45D72} = {A5C98087-E847-D2C4-2143-20869479839D} + {C28FED05-DEDA-E1BA-F538-4BCAA8DAF2FE} = {A5C98087-E847-D2C4-2143-20869479839D} + {53094E1B-EF1A-C3F8-6EB5-45A43B0B8A9C} = {A5C98087-E847-D2C4-2143-20869479839D} + {9AA450C7-BBED-3722-DB4B-4DA97D885E00} = {A5C98087-E847-D2C4-2143-20869479839D} + {31219219-DCBE-766A-A54F-4975DA7BBB80} = {A5C98087-E847-D2C4-2143-20869479839D} + {0CBB3D0E-CC76-CDE9-4A2D-CE5F73B59420} = {A5C98087-E847-D2C4-2143-20869479839D} + {66B51CD4-BCE3-757B-BEFC-0F2BF96C7D52} = {A5C98087-E847-D2C4-2143-20869479839D} + {3C13055F-5B62-4AF8-F8BB-83CFDDEEDDCF} = {A5C98087-E847-D2C4-2143-20869479839D} + {4DAC6B77-D825-3085-4263-3226D14F61AC} = {A5C98087-E847-D2C4-2143-20869479839D} + {12AF40C6-8379-4786-4DF4-B8CCCC54E2A8} = {A5C98087-E847-D2C4-2143-20869479839D} + {160F000D-9DA4-3AAA-8370-15C6BF460E1B} = {A5C98087-E847-D2C4-2143-20869479839D} + {5F222E3F-1E9A-331A-F5BC-7C22CC4DC27B} = {A5C98087-E847-D2C4-2143-20869479839D} + {7E234D3A-B714-8683-EF17-12E573F8796B} = {A5C98087-E847-D2C4-2143-20869479839D} + {A92B2733-D971-E795-C42E-763B8D28BE6A} = {A5C98087-E847-D2C4-2143-20869479839D} + {56F1436F-34A3-0E62-F76D-7D33B0F6CF9A} = {A5C98087-E847-D2C4-2143-20869479839D} + {A5F6CD9E-7F26-36C4-0785-86FECE0484FE} = {A5C98087-E847-D2C4-2143-20869479839D} + {340ACF7A-5050-5BEF-8E3E-56EFD1EA2CFF} = {A5C98087-E847-D2C4-2143-20869479839D} + {6C13C210-5443-5941-7273-A830E6A0F2FD} = {A5C98087-E847-D2C4-2143-20869479839D} + {7CC450E2-1730-BE2C-9AE2-AF8567982B8E} = {A5C98087-E847-D2C4-2143-20869479839D} + {680B3990-2960-BCCC-7C3B-2C4BAB9B8134} = {A5C98087-E847-D2C4-2143-20869479839D} + {20D1D291-07F1-1389-74B4-F82B49847CED} = {A5C98087-E847-D2C4-2143-20869479839D} + {7A7A0652-CB76-9B73-1FD1-D67F5510DB1F} = {A5C98087-E847-D2C4-2143-20869479839D} + {4BE45036-7A8F-9072-5160-3BB53505A68F} = {A5C98087-E847-D2C4-2143-20869479839D} + {C484B2AA-2582-1395-840E-61DE9FED5313} = {A5C98087-E847-D2C4-2143-20869479839D} + {A81B4B21-F490-7DE9-024B-4959F9172735} = {A5C98087-E847-D2C4-2143-20869479839D} + {68C21EB9-A635-48B1-98E8-A0DD36C545F5} = {A5C98087-E847-D2C4-2143-20869479839D} + {7A608DF5-E8F5-38DA-4E15-2A7A2B6C508F} = {A5C98087-E847-D2C4-2143-20869479839D} + {0D24FEF1-02AE-8265-6524-64DE307CB4CE} = {A5C98087-E847-D2C4-2143-20869479839D} + {7DB7DF6E-FCAA-E569-29D8-C0C4FBFC5625} = {A5C98087-E847-D2C4-2143-20869479839D} + {D655B564-92E5-1E96-7A72-112F368418C4} = {A5C98087-E847-D2C4-2143-20869479839D} + {BAF3E8BB-263A-8E31-3576-D18BE2B46A22} = {A5C98087-E847-D2C4-2143-20869479839D} + {DB888E44-BB55-C4A2-CDFF-7B04F0E2D6E5} = {A5C98087-E847-D2C4-2143-20869479839D} + {56026A9E-B2DA-48EC-60C3-3193857A238C} = {A5C98087-E847-D2C4-2143-20869479839D} + {3B2531C6-790E-BD6E-AE00-3F5F95EFE5F9} = {BB76B5A5-14BA-E317-828D-110B711D71F5} + {75E6454F-DC5F-C6E5-CB1D-47AA2AB72FDF} = {BB76B5A5-14BA-E317-828D-110B711D71F5} + {E49112A1-06DA-43F4-4A8D-5E805A6466FC} = {BB76B5A5-14BA-E317-828D-110B711D71F5} + {446F1BC4-7609-555E-1FA7-901560438951} = {BB76B5A5-14BA-E317-828D-110B711D71F5} + {327615A7-CCA8-CAAA-A279-54A2EB3C8D8C} = {BB76B5A5-14BA-E317-828D-110B711D71F5} + {1006432F-63A0-8569-EEC7-4393A1918480} = {BB76B5A5-14BA-E317-828D-110B711D71F5} + {4B6B949F-4BF5-2817-4818-71E26BF57749} = {BB76B5A5-14BA-E317-828D-110B711D71F5} + {FBBE5FEA-8CD5-8521-DC93-06C239F7876D} = {BB76B5A5-14BA-E317-828D-110B711D71F5} + {1F48E1F9-072B-1F1D-7E58-9C065D0F4ED1} = {BB76B5A5-14BA-E317-828D-110B711D71F5} + {5CC00E13-DAA8-7285-CDB0-D482D750FFBD} = {BB76B5A5-14BA-E317-828D-110B711D71F5} + {D2BC4D25-D54D-1778-798D-6547A38EEB05} = {BB76B5A5-14BA-E317-828D-110B711D71F5} + {7AC29AE8-7BDA-4243-5869-D89EF3921DC3} = {BB76B5A5-14BA-E317-828D-110B711D71F5} + {6E200246-2354-ECCC-0D81-F58C27F7551C} = {BB76B5A5-14BA-E317-828D-110B711D71F5} + {2F13F88C-7820-12E7-1FEF-4DB0DDE34DAB} = {BB76B5A5-14BA-E317-828D-110B711D71F5} + {ED480238-AEBE-3C29-8D80-5C0AB45E13D3} = {BB76B5A5-14BA-E317-828D-110B711D71F5} + {633ED6DB-FFB9-F767-070D-09C342DB4B74} = {BB76B5A5-14BA-E317-828D-110B711D71F5} + {235A45A7-331C-25BE-DB53-C2C328987E22} = {BB76B5A5-14BA-E317-828D-110B711D71F5} + {63E6AEF3-80BE-0FCE-E677-D01F65E1424F} = {BB76B5A5-14BA-E317-828D-110B711D71F5} + {D17D4B09-569F-CD45-70A8-2E4DDDE53E69} = {BB76B5A5-14BA-E317-828D-110B711D71F5} + {C1767CE0-3470-0BE7-DF63-57D1C7455A8A} = {BB76B5A5-14BA-E317-828D-110B711D71F5} + {4E63ED70-BA5F-324B-9E64-B04E64B5C0C7} = {BB76B5A5-14BA-E317-828D-110B711D71F5} + {F381910D-309A-2065-DD77-7494C2FD23BF} = {BB76B5A5-14BA-E317-828D-110B711D71F5} + {51A857B4-DBDA-1B5A-0F80-83580CA223B4} = {BB76B5A5-14BA-E317-828D-110B711D71F5} + {63B739C4-3FFB-AD71-3B39-8EA5128D8127} = {BB76B5A5-14BA-E317-828D-110B711D71F5} + {4DCC3C48-6209-AF26-3864-EAB021E65AD5} = {BB76B5A5-14BA-E317-828D-110B711D71F5} + {7C0B72FC-8CBC-D831-9614-5C4CAE8197D3} = {BB76B5A5-14BA-E317-828D-110B711D71F5} + {1AD4003A-C2C9-E202-16B4-973BE4ECFEB0} = {BB76B5A5-14BA-E317-828D-110B711D71F5} + {A684BCCD-046B-5314-B492-13A65C7AAC2A} = {BB76B5A5-14BA-E317-828D-110B711D71F5} + {7A54278E-939F-447A-17F4-15DDB3F6B403} = {BB76B5A5-14BA-E317-828D-110B711D71F5} + {AF719B05-6089-CA3F-96A7-8F59902FF781} = {BB76B5A5-14BA-E317-828D-110B711D71F5} + {D6EFF950-E1CE-F003-26E6-CCD4CE93C144} = {BB76B5A5-14BA-E317-828D-110B711D71F5} + {44F1D673-6AB1-2635-567A-4512FEA53D32} = {BB76B5A5-14BA-E317-828D-110B711D71F5} + {4B435085-030A-E80A-F018-FAEE801E2FB4} = {BB76B5A5-14BA-E317-828D-110B711D71F5} + {5E698EC5-3B12-9266-8F90-ED9B6C841C6A} = {BB76B5A5-14BA-E317-828D-110B711D71F5} + {D767CC98-7156-89E4-C89E-0B4624D83584} = {BB76B5A5-14BA-E317-828D-110B711D71F5} + {DE5FDAC5-0355-823F-552F-5A37661C9FE5} = {BB76B5A5-14BA-E317-828D-110B711D71F5} + {0BA16F05-266F-4BCC-04A6-25E7D566E769} = {BB76B5A5-14BA-E317-828D-110B711D71F5} + {540911AA-2FA7-DF51-2203-B6109B580346} = {BB76B5A5-14BA-E317-828D-110B711D71F5} + {B4801F0F-50AB-5613-AB78-5225D80E0421} = {BB76B5A5-14BA-E317-828D-110B711D71F5} + {42729022-D163-60E6-B0BA-6FA7E84DA52F} = {BB76B5A5-14BA-E317-828D-110B711D71F5} + {6A2FEC90-D817-C92B-89B8-8B2E2DD18FF1} = {BB76B5A5-14BA-E317-828D-110B711D71F5} + {EA3E79FB-BEC9-7590-EC7C-64D2CF8E8782} = {BB76B5A5-14BA-E317-828D-110B711D71F5} + {D835F664-32BC-4CA8-2ABB-97D30953F05C} = {BB76B5A5-14BA-E317-828D-110B711D71F5} + {BC4C56DA-A389-5A58-7CB0-D7AA2AE44430} = {BB76B5A5-14BA-E317-828D-110B711D71F5} + {AF8D5B15-8ADF-FA1F-342A-73D095FD5B21} = {BB76B5A5-14BA-E317-828D-110B711D71F5} + {A27B1BB8-9415-4C14-CA41-828B66357E0B} = {BB76B5A5-14BA-E317-828D-110B711D71F5} + {69D6403C-9C1F-04C3-BADF-8CFFBD3848E7} = {BB76B5A5-14BA-E317-828D-110B711D71F5} + {C26F680C-684A-ECC6-BB6C-EBD19DC43B4C} = {BB76B5A5-14BA-E317-828D-110B711D71F5} + {22B129C7-C609-3B90-AD56-64C746A1505E} = {EA6E5683-3A20-2E52-1CE6-AE0D6D36AC4D} + {AD31623A-BC43-52C2-D906-AC1D8784A541} = {3823DE1E-2ACE-C956-99E1-00DB786D9E1D} + {776E2142-804F-03B9-C804-D061D64C6092} = {EC506DBE-AB6D-492E-786E-8B176021BF2E} + {5B4DF41E-C8CC-2606-FA2D-967118BD3C59} = {5F27FB4E-CF09-3A6B-F5B4-BF5A709FA609} + {3D8C5A6C-462D-7487-5BD0-A3EF6B657EB6} = {018E0E11-1CCE-A2BE-641D-21EE14D2E90D} + {2609BC1A-6765-29BE-78CC-C0F1D2814F10} = {3F605548-87E2-8A1D-306D-0CE6960B8242} + {C6822231-A4F4-9E69-6CE2-4FDB3E81C728} = {45F7FA87-7451-6970-7F6E-F8BAE45E081B} + {55D9B653-FB76-FCE8-1A3C-67B1BEDEC214} = {F2E6CB0E-DF77-1FAA-582B-62B040DF3848} + {DE5BF139-1E5C-D6EA-4FAA-661EF353A194} = {C494ECBE-DEA5-3576-D2AF-200FF12BC144} + {335E62C0-9E69-A952-680B-753B1B17C6D0} = {9C2DD234-FA33-FDB6-86F0-EF9B75A13450} + {ECA25786-A3A8-92C4-4AA3-D4A73C69FDCA} = {7E890DF9-B715-B6DF-2498-FD74DDA87D71} + {5A6CD890-8142-F920-3734-D67CA3E65F61} = {9BD75659-58CB-06D1-E198-C39007E82C6A} + {A260E14F-DBA4-862E-53CD-18D3B92ADA3D} = {7BF13935-F1DD-D23B-8347-DB1550C69D69} + {97F94029-5419-6187-5A63-5C8FD9232FAE} = {64689413-46D7-8499-68A6-B6367ACBC597} + {03DF5914-2390-A82D-7464-642D0B95E068} = {24D60BC6-6A93-C97D-1238-113DDB928700} + {CF633BDA-9F2E-D0C8-702F-BC9D27363B4B} = {CCF230F8-F75D-A766-7EAE-0C9FEF5AF6C2} + {73DE9C04-CEFE-53BA-A527-3A36D478DEFE} = {066DF6C9-826C-F223-47D2-BDF53D59F6C3} + {B7DC1B0A-EBD8-B1E8-28C8-9D5F19E118AD} = {61C7FDA3-83AA-3EE6-6321-1C1ACD1073DF} + {0B56708E-B56C-E058-DE31-FCDFF30031F7} = {BF6C9274-4DBD-2FDE-B94C-1B208F6C53BC} + {78FAD457-CE1B-D78E-A602-510EAD85E0AF} = {4EAAC62E-EBD2-DFF1-7B37-7E131C75DEC3} + {AF9E7F02-25AD-3540-18D7-F6A4F8BA5A60} = {79E122F4-2325-3E92-438E-5825A307B594} + {AB6AE2B6-8D6B-2D9F-2A88-7C596C59F4FC} = {39EFDA5B-F5EE-8212-D5BA-90E1B82013E7} + {375F5AD0-F7EE-1782-7B34-E181CDB61B9F} = {3B82DBF3-3DAE-EA97-85F4-6DCFA09940DF} + {BA45605A-1CCE-6B0C-489D-C113915B243F} = {6844B539-C2A3-9D4F-139D-9D533BCABADA} + {9D31FC8A-2A69-B78A-D3E5-4F867B16D971} = {4263AA71-0335-3F44-9A9B-423C3A3D05E6} + {92268008-FBB0-C7AD-ECC2-7B75BED9F5E1} = {F1B1DB47-D2D7-59CB-679B-23E4928E8328} + {8DCCAF70-D364-4C8B-4E90-AF65091DE0C5} = {BC35DE94-4F04-3436-27A3-F11647FEDD5C} + {7828C164-DD01-2809-CCB3-364486834F60} = {864C8B80-771A-0C15-30A5-558F99006E0D} + {DE95E7B2-0937-A980-441F-829E023BC43E} = {603E7A23-1D6B-D3A9-B0E6-3E332B13ED5C} + {91D69463-23E2-E2C7-AA7E-A78B13CED620} = {D2F7E58B-47D4-5205-D917-144CA1CFF4F1} + {34EFF636-81A7-8DF6-7CC9-4DA784BAC7F3} = {1DCF4EBB-DBC4-752C-13D4-D1EECE4E8907} + {5DCF16A8-97C6-2CB4-6A63-0370239039EB} = {1B37A859-E733-60CB-4806-1A24B6F10E05} + {EB093C48-CDAC-106B-1196-AE34809B34C0} = {F2B58F4E-6F28-A25F-5BFB-CDEBAD6B9A3E} + {92C62F7B-8028-6EE1-B71B-F45F459B8E97} = {538E2D98-5325-3F54-BE74-EFE5FC1ECBD8} + {F664A948-E352-5808-E780-77A03F19E93E} = {66557252-B5C4-664B-D807-07018C627474} + {FA83F778-5252-0B80-5555-E69F790322EA} = {7203223D-FF02-7BEB-2798-D1639ACC01C4} + {F3A27846-6DE0-3448-222C-25A273E86B2E} = {5AC9EE40-1881-5F8A-46A2-2C303950D3C8} + {166F4DEC-9886-92D5-6496-085664E9F08F} = {927E3CD3-4C20-4DE5-A395-D0977152A8D3} + {C53E0895-879A-D9E6-0A43-24AD17A2F270} = {3C69853C-90E3-D889-1960-3B9229882590} + {246FCC7C-1437-742D-BAE5-E77A24164F08} = {9FB0DDD7-7A77-8DA4-F9E2-A94E60ED8FC7} + {0AED303F-69E6-238F-EF80-81985080EDB7} = {643E4D4C-BC96-A37F-E0EC-488127F0B127} + {2904D288-CE64-A565-2C46-C2E85A96A1EE} = {6F2CA7F5-3E7C-C61B-94E6-E7DD1227B5B1} + {A6667CC3-B77F-023E-3A67-05F99E9FF46A} = {F04B7DBB-77A5-C978-B2DE-8C189A32AA72} + {A26E2816-F787-F76B-1D6C-E086DD3E19CE} = {7C72F22A-20FF-DF5B-9191-6DFD0D497DB2} + {B3DEC619-67AC-1B5A-4F3E-A1F24C3F6877} = {C896CC0A-F5E6-9AA4-C582-E691441F8D32} + {90DB65B4-8F6E-FB8E-0281-505AD8BC6BA6} = {0AA3A418-AB45-CCA4-46D4-EEBFE011FECA} + {059FBB86-DEE6-8207-3F23-2A1A3EC00DEA} = {225D9926-4AE8-E539-70AD-8698E688F271} + {8BBA3159-C4CC-F685-A28C-7FE6CBD3D2A1} = {D6E8E69C-F721-BBCB-8C39-9716D53D72AD} + {EA0974E3-CD2B-5792-EF1E-9B5B7CCBDF00} = {9529EE99-D6A5-B570-EB1F-15BD2D57DFE2} + {632A1F0D-1BA5-C84B-B716-2BE638A92780} = {589A43FD-8213-E9E3-6CFF-9CBA72D53E98} + {9DE7852B-7E2D-257E-B0F1-45D2687854ED} = {2BACF7E3-1278-FE99-8343-8221E6FBA9DE} + {DC2AFC89-C3C8-4E9B-13A7-027EB6386EFA} = {75E47125-E4D7-8482-F1A4-726564970864} + {9151601C-8784-01A6-C2E7-A5C0FAAB0AEF} = {6DCAF6F3-717F-27A9-D96C-F2BFA5550347} + {CB296A20-2732-77C1-7F23-27D5BAEDD0C7} = {054761F9-16D3-B2F8-6F4D-EFC2248805CD} + {0DBEC9BA-FE1D-3898-B2C6-E4357DC23E0F} = {B54CE64C-4167-1DD1-B7D6-2FD7A5AEF715} + {A63897D9-9531-989B-7309-E384BCFC2BB9} = {FCD529E0-DD17-6587-B29C-12D425C0AD0C} + {8C594D82-3463-3367-4F06-900AC707753D} = {61B23570-4F2D-B060-BE1F-37995682E494} + {52F400CD-D473-7A1F-7986-89011CD2A887} = {CEDC2447-F717-3C95-7E08-F214D575A7B7} + {9588FBF9-C37E-D16E-2E8F-CFA226EAC01D} = {1182764D-2143-EEF0-9270-3DCE392F5D06} + {97998C88-E6E1-D5E2-B632-537B58E00CBF} = {F4F1CBE2-1CDD-CAA4-41F0-266DB4677C05} + {BAD08D96-A80A-D27F-5D9C-656AEEB3D568} = {3DE1DCDC-C845-4AC7-7B66-34B0A9E8626B} + {F63694F1-B56D-6E72-3F5D-5D38B1541F0F} = {6FA01E92-606B-0CB8-8583-6F693A903CFC} + {20D1569C-2A47-38B8-075E-47225B674394} = {B0F64757-F7A7-1A11-8DEC-BAC72EB5EC29} + {38A9EE9B-6FC8-93BC-0D43-2A906E678D66} = {772B02B5-6280-E1D4-3E2E-248D0455C2FB} + {19868E2D-7163-2108-1094-F13887C4F070} = {831265B0-8896-9C95-3488-E12FD9F6DC53} + {CC319FC5-F4B1-C3DD-7310-4DAD343E0125} = {BC12ED55-6015-7C8B-8384-B39CE93C76D6} + {84F711C2-C210-28D2-F0D9-B13733FEE23D} = {48F90289-938C-CCA7-B60F-D2143E7C9A69} + {CBB14B90-27F9-8DD6-DFC4-3507DBD1FBC6} = {E69FA1A0-6D1B-A6E4-2DC0-8F4C5F21BF04} + {A78EBC0F-C62C-8F56-95C0-330E376242A2} = {9D6AB85A-85EA-D85A-5566-A121D34016E6} + {6D26FB21-7E48-024B-E5D4-E3F0F31976BB} = {083067CF-CE89-EF39-9BD3-4741919E26F3} + {79104479-B087-E5D0-5523-F1803282A246} = {A5994E92-7E0E-89FE-5628-DE1A0176B8BA} + {F17A6F0B-3120-2BA9-84D8-5F8BA0B9705D} = {54C11B29-4C54-7255-AB44-BEB63AF9BD1F} + {FBD908D6-AF93-CC62-C09D-F0BB3E0CEA7F} = {2F4EB1B2-4185-C535-85ED-53EA2D2C0573} + {37F9B25E-81CF-95C5-0311-EA6DA191E415} = {3B2531C6-790E-BD6E-AE00-3F5F95EFE5F9} + {28D91816-206C-576E-1A83-FD98E08C2E3C} = {79987C33-9903-4F75-1045-284F537155FE} + {5EFEC79C-A9F1-96A4-692C-733566107170} = {730E1137-BA40-3899-17CB-93922F4DA4CA} + {F4E7E32B-D78B-5A08-F7B5-E8D4C7ED20D3} = {75E6454F-DC5F-C6E5-CB1D-47AA2AB72FDF} + {3A1CFB24-6EAA-9A87-7783-BFC56B0E5394} = {EEF284CB-436C-439C-9606-E4DD1FD5F6D3} + {B1969736-DE03-ADEB-2659-55B2B82B38A8} = {1B2C9807-0067-AAD6-69CD-7FD799689BDD} + {D166FCF0-F220-A013-133A-620521740411} = {E49112A1-06DA-43F4-4A8D-5E805A6466FC} + {F638D731-2DB2-2278-D9F8-019418A264F2} = {891DD979-9F3C-981D-FB41-EB66B7C61938} + {CAEB1FEB-B3F1-4B9D-7FEC-1983BCA60D81} = {446F1BC4-7609-555E-1FA7-901560438951} + {B07074FE-3D4E-5957-5F81-B75B5D25BD1B} = {E5B2A8AA-8162-7E98-9D31-3DA40BB8EDC2} + {91B8E22B-C90B-AEBD-707E-57BBD549BA32} = {327615A7-CCA8-CAAA-A279-54A2EB3C8D8C} + {B7B5D764-C3A0-1743-0739-29966F993626} = {29E594C5-9DE1-D06F-4F19-4166C3935CAE} + {E9039B92-DAFC-F20B-22D6-78F8E4EB7CF1} = {1006432F-63A0-8569-EEC7-4393A1918480} + {C4EDBBAF-875C-4839-05A8-F6F12A5ED52D} = {BE6FF48D-FDD8-A0C3-69E0-CAE30D3FC13F} + {04444789-CEE4-3F3A-6EFA-18416E620B2A} = {4B6B949F-4BF5-2817-4818-71E26BF57749} + {AD1B6448-2DC9-2F9A-D143-F09BBDF6F01F} = {FBBE5FEA-8CD5-8521-DC93-06C239F7876D} + {0EAC8F64-9588-1EF0-C33A-67590CF27590} = {8FF79C15-5E56-BB59-A473-0E42F8395C89} + {761CAD6D-98CB-1936-9065-BF1A756671FF} = {4B16E448-1B2D-28B7-2417-D2D191FE524F} + {7974C4F0-BC89-2775-8943-2DF909F3B08B} = {1F48E1F9-072B-1F1D-7E58-9C065D0F4ED1} + {B1B31937-CCC8-D97A-F66D-1849734B780B} = {1003D3C6-91C9-D165-8325-20F3A16D9A5B} + {9A566B3A-E281-09AF-EC1B-BD4FDB3248CE} = {5CC00E13-DAA8-7285-CDB0-D482D750FFBD} + {A345E5AC-BDDB-A817-3C92-08C8865D1EF9} = {278C2043-B1CE-CC57-7F08-9C4B69C7A65D} + {905DD8ED-3D10-7C2B-B199-B98E85267BB8} = {D2BC4D25-D54D-1778-798D-6547A38EEB05} + {C2D3B3C7-E556-9B72-024A-FF5EDEAF4CB5} = {F783A1A9-4991-592A-57C3-E18C41986545} + {31AC6B88-D6C8-E2EF-39AF-1B21AFD76C89} = {9EAD58C8-4DCD-4933-B9F9-7D211C66D73F} + {90B84537-F992-234C-C998-91C6AD65AB12} = {7AC29AE8-7BDA-4243-5869-D89EF3921DC3} + {F22333B6-7E27-679B-8475-B4B9AB1CB186} = {9646F3EA-8DFC-F221-D415-D489C166889B} + {CE042F3A-6851-FAAB-9E9C-AD905B4AAC8D} = {83CDC626-3D1B-02BE-1DE9-82175D01430B} + {D6B56A54-4057-9F76-BC7E-56E896E5D276} = {6E200246-2354-ECCC-0D81-F58C27F7551C} + {9258E4F2-762C-C780-F118-2CABD0281CC9} = {079CEB57-D0A3-20A1-131E-37D0CA4C0410} + {D6752A7E-0FD2-6626-80E3-CE4D4816D2B0} = {49ECEC98-A019-AEB6-1632-D05B0DCB6EEA} + {AF85AC87-521A-2F0E-5F10-836E416EC716} = {7C05D5D7-330C-204C-430C-8B2B27AB9995} + {FB946C57-55B3-08C6-18AE-1672D46C5308} = {AA9E4361-01CE-AC2F-E20D-0CA5C1D438CC} + {99A47EAA-44B8-8E06-DA0E-05B225009FDF} = {2F13F88C-7820-12E7-1FEF-4DB0DDE34DAB} + {4F0EF830-4308-347B-A31D-270A9812D15E} = {3C0D80C5-B033-CE52-8868-614826F45D72} + {B7EE2F70-A634-8B4D-93F4-EA1EEFD9E5E8} = {ED480238-AEBE-3C29-8D80-5C0AB45E13D3} + {A5298720-984E-6574-D41B-CFE7CA408182} = {C28FED05-DEDA-E1BA-F538-4BCAA8DAF2FE} + {CB033CB6-F90B-E201-BA86-C867544E7247} = {633ED6DB-FFB9-F767-070D-09C342DB4B74} + {E9F5BFF2-0D0E-7B41-9AF0-83384F4B8825} = {53094E1B-EF1A-C3F8-6EB5-45A43B0B8A9C} + {668466AC-CD66-BAA0-0322-148549E373CB} = {235A45A7-331C-25BE-DB53-C2C328987E22} + {07EBBFA6-798E-76A3-CAF0-67828B00B58E} = {9AA450C7-BBED-3722-DB4B-4DA97D885E00} + {181ED0FE-FE20-069F-7CCF-86FF5449D7F5} = {63E6AEF3-80BE-0FCE-E677-D01F65E1424F} + {5E683B7C-B584-0E56-C8D6-D29050DE70FB} = {31219219-DCBE-766A-A54F-4975DA7BBB80} + {4163E755-1563-6A72-60E7-BB2B69F5ABA2} = {D17D4B09-569F-CD45-70A8-2E4DDDE53E69} + {AE6F3DA7-2993-6926-323E-A29295D55C36} = {0CBB3D0E-CC76-CDE9-4A2D-CE5F73B59420} + {D013641A-8457-6215-05A1-74BB57B58409} = {C1767CE0-3470-0BE7-DF63-57D1C7455A8A} + {4FC29140-9C5F-8DD5-B405-6982BD1DA5A3} = {66B51CD4-BCE3-757B-BEFC-0F2BF96C7D52} + {B9C9A1E4-3BB8-C8BE-7819-660A582D2952} = {3C13055F-5B62-4AF8-F8BB-83CFDDEEDDCF} + {2BBAB3B4-2E18-F945-F7AB-6207D7F72714} = {4E63ED70-BA5F-324B-9E64-B04E64B5C0C7} + {BA492274-A505-BCD5-3DA5-EE0C94DD5748} = {4DAC6B77-D825-3085-4263-3226D14F61AC} + {029F8300-57F5-9CCD-505E-708937686679} = {F381910D-309A-2065-DD77-7494C2FD23BF} + {A5D2DB78-8045-29AC-E4B1-66E72F2C7FF0} = {12AF40C6-8379-4786-4DF4-B8CCCC54E2A8} + {294792C0-DC28-3C5D-2D59-33DC99CD6C61} = {51A857B4-DBDA-1B5A-0F80-83580CA223B4} + {58D8630F-C0F4-B772-8572-BCC98FF0F0D8} = {160F000D-9DA4-3AAA-8370-15C6BF460E1B} + {2B1B4954-1241-8F2E-75B6-2146D15D037B} = {63B739C4-3FFB-AD71-3B39-8EA5128D8127} + {97A9C869-F385-6711-6B76-F3859C86DCAC} = {5F222E3F-1E9A-331A-F5BC-7C22CC4DC27B} + {201CE292-0186-2A38-55D7-69890B5817DF} = {4DCC3C48-6209-AF26-3864-EAB021E65AD5} + {17A00031-9FF7-4F73-5319-23FA5817625F} = {7E234D3A-B714-8683-EF17-12E573F8796B} + {11E0E129-091F-BEEB-A1B2-9BAEF5BE9FBC} = {7C0B72FC-8CBC-D831-9614-5C4CAE8197D3} + {AEF63403-4889-5396-CDEA-3B713CEF2ED7} = {1AD4003A-C2C9-E202-16B4-973BE4ECFEB0} + {D24E7862-3930-A4F6-1DFA-DA88C759546C} = {A92B2733-D971-E795-C42E-763B8D28BE6A} + {6DC62619-949E-92E6-F4F1-5A0320959929} = {A684BCCD-046B-5314-B492-13A65C7AAC2A} + {37F1D83D-073C-C165-4C53-664AD87628E6} = {56F1436F-34A3-0E62-F76D-7D33B0F6CF9A} + {CDC236E8-6881-46C4-EE95-3C386AF009D0} = {7A54278E-939F-447A-17F4-15DDB3F6B403} + {ACC2785F-F4B9-13E4-EED2-C5D067242175} = {A5F6CD9E-7F26-36C4-0785-86FECE0484FE} + {7F4A3CA3-C3DA-A698-5CBC-54F28D1C7DFB} = {AF719B05-6089-CA3F-96A7-8F59902FF781} + {DAA1F516-DEC3-7FF2-3A63-78DD97BA062C} = {D6EFF950-E1CE-F003-26E6-CCD4CE93C144} + {11EF0DE9-2648-F711-6194-70B5C40B3F3F} = {340ACF7A-5050-5BEF-8E3E-56EFD1EA2CFF} + {01A21B47-07C5-6039-1B48-C5EACA3DBA2D} = {6C13C210-5443-5941-7273-A830E6A0F2FD} + {7CB7FEA8-8A12-A5D6-0057-AA65DB328617} = {7CC450E2-1730-BE2C-9AE2-AF8567982B8E} + {0484DB46-3E40-1A10-131C-524AF1233EA7} = {44F1D673-6AB1-2635-567A-4512FEA53D32} + {64E1D9B1-B944-8AA3-799F-02E7DD33FB78} = {680B3990-2960-BCCC-7C3B-2C4BAB9B8134} + {D37991E1-585F-FF1B-9772-07477E40AF78} = {4B435085-030A-E80A-F018-FAEE801E2FB4} + {35A06F00-71AB-8A31-7D60-EBF41EA730CA} = {20D1D291-07F1-1389-74B4-F82B49847CED} + {56120A54-1D4D-F07B-63B4-B15525C2ADD9} = {5E698EC5-3B12-9266-8F90-ED9B6C841C6A} + {BE47FB74-D163-0B1F-5293-0962EA7E8585} = {D767CC98-7156-89E4-C89E-0B4624D83584} + {9AD932E9-0986-654C-B454-34E654C80697} = {7A7A0652-CB76-9B73-1FD1-D67F5510DB1F} + {00BE2B68-FC96-23F8-F61D-EE53B7AE06A1} = {DE5FDAC5-0355-823F-552F-5A37661C9FE5} + {570BA050-81A7-46EB-3DDD-422027EE2CA2} = {513870EC-4723-146F-A8F5-D6A7981C32B5} + {6C43FD78-3478-F245-3EE4-E410D1E7D7C5} = {0BA16F05-266F-4BCC-04A6-25E7D566E769} + {7F0FFA06-EAC8-CC9A-3386-389638F12B59} = {4BE45036-7A8F-9072-5160-3BB53505A68F} + {03B9D4BE-348B-9AD6-6FE9-6C40AE22053D} = {540911AA-2FA7-DF51-2203-B6109B580346} + {35CF4CF2-8A84-378D-32F0-572F4AA900A3} = {C484B2AA-2582-1395-840E-61DE9FED5313} + {13E03C69-0634-3330-26D9-DCF7DD136BC5} = {0E5FA5F8-5C99-7868-B1D0-1D38A9624795} + {A80D212B-7E80-4251-16C0-60FA3670A5B4} = {A81B4B21-F490-7DE9-024B-4959F9172735} + {2F4ACEB8-76C7-D5A2-6DD1-2EF713D9A197} = {B4801F0F-50AB-5613-AB78-5225D80E0421} + {C146A9AF-6C13-B9DC-F555-37182A54430F} = {42729022-D163-60E6-B0BA-6FA7E84DA52F} + {E5025FCF-78CB-4B5B-3377-AE008B1BE9D2} = {68C21EB9-A635-48B1-98E8-A0DD36C545F5} + {52698305-D6F8-C13C-0882-48FC37726404} = {7A608DF5-E8F5-38DA-4E15-2A7A2B6C508F} + {DE10AF97-E790-9D19-2399-70940A9B83A7} = {6A2FEC90-D817-C92B-89B8-8B2E2DD18FF1} + {5567139C-0365-B6A0-5DD0-978A09B9F176} = {0D24FEF1-02AE-8265-6524-64DE307CB4CE} + {A56C1F0E-3E18-DBEE-7F97-B5FCBF23D1D6} = {EA3E79FB-BEC9-7590-EC7C-64D2CF8E8782} + {256D269B-35EA-F833-2F1D-8E0058908DEE} = {7DB7DF6E-FCAA-E569-29D8-C0C4FBFC5625} + {F02B63CD-2C69-61F7-7F96-930122D4D4D7} = {D835F664-32BC-4CA8-2ABB-97D30953F05C} + {F061C879-063E-99DE-B301-E261DB12156F} = {BC4C56DA-A389-5A58-7CB0-D7AA2AE44430} + {6E9C9582-67FA-2EB1-C6BA-AD4CD326E276} = {D655B564-92E5-1E96-7A72-112F368418C4} + {FCF711C2-1090-7204-5E38-4BEFBE265A61} = {AF8D5B15-8ADF-FA1F-342A-73D095FD5B21} + {3A4AAE04-FA0C-2AF8-6548-AC22E5A41312} = {BAF3E8BB-263A-8E31-3576-D18BE2B46A22} + {66F8F288-C387-40E0-5F83-938671335703} = {A27B1BB8-9415-4C14-CA41-828B66357E0B} + {7B3BDB83-918F-6760-3853-BDD70CD71B42} = {DB888E44-BB55-C4A2-CDFF-7B04F0E2D6E5} + {2669C700-5CFF-0186-F65E-8D26BE06E934} = {56026A9E-B2DA-48EC-60C3-3193857A238C} + {0560BD84-CDBC-A79A-C665-55F6D62825EA} = {BE1D16DA-78A9-22D8-F49D-94719ECB5132} + {783A67C9-3381-6E4C-3752-423F0FC6F6F9} = {69D6403C-9C1F-04C3-BADF-8CFFBD3848E7} + {F890BD12-6CF5-4F80-9099-B7FE9A908432} = {C35AAD13-5F80-85D6-1702-E3E0C55D6A99} + {505C6840-5113-26EC-CEDB-D07EEABEF94B} = {C26F680C-684A-ECC6-BB6C-EBD19DC43B4C} + {A79CBC0C-5313-4ECF-A24E-27CE236BCF2C} = {076B8074-5735-5367-1EEA-CA16A5B8ABD7} + {0AF13355-173C-3128-5AFC-D32E540DA3EF} = {79B10804-91E9-972E-1913-EE0F0B11663E} + {AF043113-CCE3-59C1-DF71-9804155F26A8} = {8380A20C-A5B8-EE91-1A58-270323688CB9} + {15602821-2ABA-14BB-738D-1A53E1976E07} = {3C8A19A6-5578-51E9-E592-6DABBE5739E5} + {1D761F8B-921C-53BF-DCF5-5ABD329EEB0C} = {A7542386-71EB-4F34-E1CE-27D399325955} + {DA7634C2-9156-9B79-7A1D-90D8E605DC8A} = {0910C958-24C8-947F-359A-218ED1199AAE} + {5C4EF841-B039-4899-BF6F-32DC4FDB7AE5} = {BB76B5A5-14BA-E317-828D-110B711D71F5} + {44A3DE13-CC1A-4331-8551-30F52E67510C} = {A5C98087-E847-D2C4-2143-20869479839D} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {C9C08EA6-E174-0E6C-3FFC-FC856E9A6EC2} + EndGlobalSection +EndGlobal diff --git a/src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Deno/Internal/Runtime/DenoRuntimeShim.cs b/src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Deno/Internal/Runtime/DenoRuntimeShim.cs index a319c18e7..aa8c07d32 100644 --- a/src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Deno/Internal/Runtime/DenoRuntimeShim.cs +++ b/src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Deno/Internal/Runtime/DenoRuntimeShim.cs @@ -426,8 +426,13 @@ function flush() { const sorted = events.sort((a, b) => { const at = String(a.ts); const bt = String(b.ts); - if (at === bt) return String(a.type).localeCompare(String(b.type)); - return at.localeCompare(bt); + if (at === bt) { + const atype = String(a.type); + const btype = String(b.type); + if (atype === btype) return 0; + return atype < btype ? -1 : 1; + } + return at < bt ? -1 : 1; }); const data = sorted.map((e) => JSON.stringify(e)).join("\\n"); diff --git a/src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Deno/Internal/Runtime/DenoRuntimeTraceRecorder.cs b/src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Deno/Internal/Runtime/DenoRuntimeTraceRecorder.cs index ab2cd0fd7..b468aa466 100644 --- a/src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Deno/Internal/Runtime/DenoRuntimeTraceRecorder.cs +++ b/src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Deno/Internal/Runtime/DenoRuntimeTraceRecorder.cs @@ -11,11 +11,12 @@ internal sealed class DenoRuntimeTraceRecorder private readonly string _rootPath; private readonly TimeProvider _timeProvider; - public DenoRuntimeTraceRecorder(string rootPath, TimeProvider? timeProvider = null) + public DenoRuntimeTraceRecorder(string rootPath, TimeProvider timeProvider) { ArgumentException.ThrowIfNullOrWhiteSpace(rootPath); + ArgumentNullException.ThrowIfNull(timeProvider); _rootPath = Path.GetFullPath(rootPath); - _timeProvider = timeProvider ?? TimeProvider.System; + _timeProvider = timeProvider; } public void AddModuleLoad(string absoluteModulePath, string reason, IEnumerable permissions, string? origin = null, DateTimeOffset? timestamp = null) diff --git a/src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Deno/Internal/Runtime/DenoRuntimeTraceRunner.cs b/src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Deno/Internal/Runtime/DenoRuntimeTraceRunner.cs index c5646a428..753f9d397 100644 --- a/src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Deno/Internal/Runtime/DenoRuntimeTraceRunner.cs +++ b/src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Deno/Internal/Runtime/DenoRuntimeTraceRunner.cs @@ -13,6 +13,12 @@ internal static class DenoRuntimeTraceRunner private const string EntrypointEnvVar = "STELLA_DENO_ENTRYPOINT"; private const string BinaryEnvVar = "STELLA_DENO_BINARY"; private const string RuntimeFileName = "deno-runtime.ndjson"; + private static readonly HashSet AllowedBinaryNames = new(StringComparer.OrdinalIgnoreCase) + { + "deno", + "deno.exe", + "deno.cmd" + }; public static async Task TryExecuteAsync( LanguageAnalyzerContext context, @@ -28,29 +34,28 @@ internal static class DenoRuntimeTraceRunner return false; } - var entrypointPath = Path.GetFullPath(Path.Combine(context.RootPath, entrypoint)); - if (!File.Exists(entrypointPath)) + var rootPath = Path.GetFullPath(context.RootPath); + if (!TryResolveEntrypointPath(rootPath, entrypoint, logger, out var entrypointPath)) { - logger?.LogWarning("Deno runtime trace skipped: entrypoint '{Entrypoint}' missing", entrypointPath); return false; } - var shimPath = Path.Combine(context.RootPath, DenoRuntimeShim.FileName); - if (!File.Exists(shimPath)) - { - await DenoRuntimeShim.WriteAsync(context.RootPath, cancellationToken).ConfigureAwait(false); - } - - var binary = Environment.GetEnvironmentVariable(BinaryEnvVar); + var binary = ResolveBinary(rootPath, Environment.GetEnvironmentVariable(BinaryEnvVar), logger); if (string.IsNullOrWhiteSpace(binary)) { - binary = "deno"; + return false; + } + + var shimPath = Path.Combine(rootPath, DenoRuntimeShim.FileName); + if (!File.Exists(shimPath)) + { + await DenoRuntimeShim.WriteAsync(rootPath, cancellationToken).ConfigureAwait(false); } var startInfo = new ProcessStartInfo { FileName = binary, - WorkingDirectory = context.RootPath, + WorkingDirectory = rootPath, RedirectStandardError = true, RedirectStandardOutput = true, UseShellExecute = false, @@ -58,7 +63,7 @@ internal static class DenoRuntimeTraceRunner startInfo.ArgumentList.Add("run"); startInfo.ArgumentList.Add("--cached-only"); - startInfo.ArgumentList.Add("--allow-read"); + startInfo.ArgumentList.Add(BuildAllowReadArgument(rootPath, logger)); startInfo.ArgumentList.Add("--allow-env"); startInfo.ArgumentList.Add("--quiet"); startInfo.ArgumentList.Add(shimPath); @@ -96,7 +101,7 @@ internal static class DenoRuntimeTraceRunner return false; } - var runtimePath = Path.Combine(context.RootPath, RuntimeFileName); + var runtimePath = Path.Combine(rootPath, RuntimeFileName); if (!File.Exists(runtimePath)) { logger?.LogWarning( @@ -108,6 +113,122 @@ internal static class DenoRuntimeTraceRunner return true; } + private static bool TryResolveEntrypointPath( + string rootPath, + string entrypoint, + ILogger? logger, + out string entrypointPath) + { + entrypointPath = string.Empty; + if (string.IsNullOrWhiteSpace(entrypoint)) + { + logger?.LogWarning("Deno runtime trace skipped: entrypoint was empty"); + return false; + } + + try + { + var candidate = Path.GetFullPath(Path.Combine(rootPath, entrypoint)); + if (!IsWithinRoot(rootPath, candidate)) + { + logger?.LogWarning("Deno runtime trace skipped: entrypoint '{Entrypoint}' not under root", entrypoint); + return false; + } + + if (!File.Exists(candidate)) + { + logger?.LogWarning("Deno runtime trace skipped: entrypoint '{Entrypoint}' missing", candidate); + return false; + } + + entrypointPath = candidate; + return true; + } + catch (Exception ex) when (ex is ArgumentException or IOException or NotSupportedException or PathTooLongException or UnauthorizedAccessException) + { + logger?.LogWarning(ex, "Deno runtime trace skipped: entrypoint '{Entrypoint}' invalid", entrypoint); + return false; + } + } + + private static string? ResolveBinary(string rootPath, string? binary, ILogger? logger) + { + if (string.IsNullOrWhiteSpace(binary)) + { + return "deno"; + } + + var trimmed = binary.Trim(); + var fileName = Path.GetFileName(trimmed); + if (string.IsNullOrWhiteSpace(fileName) || !AllowedBinaryNames.Contains(fileName)) + { + logger?.LogWarning("Deno runtime trace skipped: binary '{Binary}' not allowlisted", trimmed); + return null; + } + + var isPath = trimmed.Contains(Path.DirectorySeparatorChar) || + trimmed.Contains(Path.AltDirectorySeparatorChar) || + Path.IsPathRooted(trimmed); + if (!isPath) + { + return trimmed; + } + + try + { + var candidate = Path.GetFullPath(Path.IsPathRooted(trimmed) + ? trimmed + : Path.Combine(rootPath, trimmed)); + if (!IsWithinRoot(rootPath, candidate)) + { + logger?.LogWarning("Deno runtime trace skipped: binary '{Binary}' not under root", trimmed); + return null; + } + + if (!File.Exists(candidate)) + { + logger?.LogWarning("Deno runtime trace skipped: binary '{Binary}' missing", candidate); + return null; + } + + return candidate; + } + catch (Exception ex) when (ex is ArgumentException or IOException or NotSupportedException or PathTooLongException or UnauthorizedAccessException) + { + logger?.LogWarning(ex, "Deno runtime trace skipped: binary '{Binary}' invalid", trimmed); + return null; + } + } + + private static string BuildAllowReadArgument(string rootPath, ILogger? logger) + { + var comparison = OperatingSystem.IsWindows() ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal; + var allowed = new HashSet(comparison) { rootPath }; + var denoDir = Environment.GetEnvironmentVariable("DENO_DIR"); + if (!string.IsNullOrWhiteSpace(denoDir)) + { + try + { + var denoDirPath = Path.GetFullPath(denoDir, rootPath); + allowed.Add(denoDirPath); + } + catch (Exception ex) when (ex is ArgumentException or IOException or NotSupportedException or PathTooLongException or UnauthorizedAccessException) + { + logger?.LogWarning(ex, "Deno runtime trace: invalid DENO_DIR '{DenoDir}'", denoDir); + } + } + + var ordered = allowed.OrderBy(path => path, comparison); + return $"--allow-read={string.Join(",", ordered)}"; + } + + private static bool IsWithinRoot(string rootPath, string candidatePath) + { + var comparison = OperatingSystem.IsWindows() ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal; + var normalizedRoot = rootPath.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + Path.DirectorySeparatorChar; + return candidatePath.StartsWith(normalizedRoot, comparison); + } + private static string Truncate(string? value, int maxLength = 400) { if (string.IsNullOrEmpty(value)) diff --git a/src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Deno/Internal/Runtime/DenoRuntimeTraceSerializer.cs b/src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Deno/Internal/Runtime/DenoRuntimeTraceSerializer.cs index eb1cd8f64..285df39da 100644 --- a/src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Deno/Internal/Runtime/DenoRuntimeTraceSerializer.cs +++ b/src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Deno/Internal/Runtime/DenoRuntimeTraceSerializer.cs @@ -1,5 +1,6 @@ using System.Security.Cryptography; using System.Text; +using System.Text.Encodings.Web; using System.Text.Json; namespace StellaOps.Scanner.Analyzers.Lang.Deno.Internal.Runtime; @@ -8,7 +9,7 @@ internal static class DenoRuntimeTraceSerializer { private static readonly JsonWriterOptions WriterOptions = new() { - Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + Encoder = JavaScriptEncoder.Default, Indented = false }; diff --git a/src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.DotNet/Internal/Bundling/DotNetBundlingSignalCollector.cs b/src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.DotNet/Internal/Bundling/DotNetBundlingSignalCollector.cs index bd8b89a20..fa5878f06 100644 --- a/src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.DotNet/Internal/Bundling/DotNetBundlingSignalCollector.cs +++ b/src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.DotNet/Internal/Bundling/DotNetBundlingSignalCollector.cs @@ -1,4 +1,5 @@ using System.Collections.Immutable; +using System.Globalization; using StellaOps.Scanner.Analyzers.Lang.DotNet.Internal.Bundling; namespace StellaOps.Scanner.Analyzers.Lang.DotNet.Internal; @@ -278,7 +279,7 @@ internal sealed record BundlingSignal( yield return new("bundle.detected", "true"); yield return new("bundle.filePath", FilePath); yield return new("bundle.kind", Kind.ToString().ToLowerInvariant()); - yield return new("bundle.sizeBytes", SizeBytes.ToString()); + yield return new("bundle.sizeBytes", SizeBytes.ToString(CultureInfo.InvariantCulture)); if (IsSkipped) { @@ -292,7 +293,7 @@ internal sealed record BundlingSignal( { if (EstimatedBundledAssemblies > 0) { - yield return new("bundle.estimatedAssemblies", EstimatedBundledAssemblies.ToString()); + yield return new("bundle.estimatedAssemblies", EstimatedBundledAssemblies.ToString(CultureInfo.InvariantCulture)); } for (var i = 0; i < Indicators.Length; i++) diff --git a/src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.DotNet/Internal/Callgraph/DotNetCallgraphBuilder.cs b/src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.DotNet/Internal/Callgraph/DotNetCallgraphBuilder.cs index 28ea4befa..e6a639d10 100644 --- a/src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.DotNet/Internal/Callgraph/DotNetCallgraphBuilder.cs +++ b/src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.DotNet/Internal/Callgraph/DotNetCallgraphBuilder.cs @@ -23,10 +23,10 @@ internal sealed class DotNetCallgraphBuilder private int _assemblyCount; private int _typeCount; - public DotNetCallgraphBuilder(string contextDigest, TimeProvider? timeProvider = null) + public DotNetCallgraphBuilder(string contextDigest, TimeProvider timeProvider) { _contextDigest = contextDigest; - _timeProvider = timeProvider ?? TimeProvider.System; + _timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider)); } /// diff --git a/src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.DotNet/Internal/DotNetFileCaches.cs b/src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.DotNet/Internal/DotNetFileCaches.cs index b3a1b9ad5..de267cbf2 100644 --- a/src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.DotNet/Internal/DotNetFileCaches.cs +++ b/src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.DotNet/Internal/DotNetFileCaches.cs @@ -232,6 +232,7 @@ internal static class DotNetLicenseCache DtdProcessing = DtdProcessing.Ignore, IgnoreComments = true, IgnoreWhitespace = true, + XmlResolver = null, }); var expressions = new SortedSet(StringComparer.OrdinalIgnoreCase); diff --git a/src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.DotNet/TASKS.md b/src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.DotNet/TASKS.md new file mode 100644 index 000000000..adf2811c1 --- /dev/null +++ b/src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.DotNet/TASKS.md @@ -0,0 +1,10 @@ +# Scanner .NET Analyzer Task Board + +This board mirrors active sprint tasks for this module. +Source of truth: `docs/implplan/SPRINT_20260112_003_BE_csproj_audit_pending_apply.md` and `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229_049_BE_csproj_audit_maint_tests.md`. + +| Task ID | Status | Notes | +| --- | --- | --- | +| AUDIT-HOTLIST-SCANNER-LANG-DOTNET-0001 | DONE | Applied hotlist fixes and tests. | +| AUDIT-0644-A | DONE | Audit tracker updated for DotNet analyzer apply. | +| AUDIT-0698-A | DONE | Test project apply completed (warnings, deterministic fixtures). | diff --git a/src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Native/Internal/Callgraph/NativeCallgraphBuilder.cs b/src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Native/Internal/Callgraph/NativeCallgraphBuilder.cs index fbe241220..401a39b8f 100644 --- a/src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Native/Internal/Callgraph/NativeCallgraphBuilder.cs +++ b/src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Native/Internal/Callgraph/NativeCallgraphBuilder.cs @@ -18,10 +18,11 @@ internal sealed class NativeCallgraphBuilder private readonly TimeProvider _timeProvider; private int _binaryCount; - public NativeCallgraphBuilder(string layerDigest, TimeProvider? timeProvider = null) + public NativeCallgraphBuilder(string layerDigest, TimeProvider timeProvider) { _layerDigest = layerDigest; - _timeProvider = timeProvider ?? TimeProvider.System; + ArgumentNullException.ThrowIfNull(timeProvider); + _timeProvider = timeProvider; } /// diff --git a/src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Native/NativeReachabilityAnalyzer.cs b/src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Native/NativeReachabilityAnalyzer.cs index 5f7e6512f..ffcb16aa4 100644 --- a/src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Native/NativeReachabilityAnalyzer.cs +++ b/src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Native/NativeReachabilityAnalyzer.cs @@ -16,6 +16,14 @@ namespace StellaOps.Scanner.Analyzers.Native; /// public sealed class NativeReachabilityAnalyzer { + private readonly TimeProvider _timeProvider; + + public NativeReachabilityAnalyzer(TimeProvider timeProvider) + { + ArgumentNullException.ThrowIfNull(timeProvider); + _timeProvider = timeProvider; + } + /// /// Analyzes a directory of ELF binaries and produces a reachability graph. /// @@ -31,7 +39,7 @@ public sealed class NativeReachabilityAnalyzer ArgumentException.ThrowIfNullOrEmpty(layerPath); ArgumentException.ThrowIfNullOrEmpty(layerDigest); - var builder = new NativeCallgraphBuilder(layerDigest); + var builder = new NativeCallgraphBuilder(layerDigest, _timeProvider); // Find all potential ELF files in the layer await foreach (var filePath in FindElfFilesAsync(layerPath, cancellationToken)) @@ -73,7 +81,7 @@ public sealed class NativeReachabilityAnalyzer ArgumentException.ThrowIfNullOrEmpty(filePath); ArgumentException.ThrowIfNullOrEmpty(layerDigest); - var builder = new NativeCallgraphBuilder(layerDigest); + var builder = new NativeCallgraphBuilder(layerDigest, _timeProvider); await using var stream = File.OpenRead(filePath); var elf = ElfReader.Parse(stream, filePath, layerDigest); @@ -98,7 +106,7 @@ public sealed class NativeReachabilityAnalyzer ArgumentException.ThrowIfNullOrEmpty(filePath); ArgumentException.ThrowIfNullOrEmpty(layerDigest); - var builder = new NativeCallgraphBuilder(layerDigest); + var builder = new NativeCallgraphBuilder(layerDigest, _timeProvider); var elf = ElfReader.Parse(stream, filePath, layerDigest); if (elf is not null) diff --git a/src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Native/RuntimeCapture/Timeline/TimelineBuilder.cs b/src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Native/RuntimeCapture/Timeline/TimelineBuilder.cs index 98a45a051..d50ac5dec 100644 --- a/src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Native/RuntimeCapture/Timeline/TimelineBuilder.cs +++ b/src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Native/RuntimeCapture/Timeline/TimelineBuilder.cs @@ -1,3 +1,5 @@ +using System.Globalization; + namespace StellaOps.Scanner.Analyzers.Native.RuntimeCapture.Timeline; public interface ITimelineBuilder @@ -108,7 +110,7 @@ public sealed class TimelineBuilder : ITimelineBuilder Details = new Dictionary { ["path"] = obs.Path ?? "", - ["process_id"] = obs.ProcessId.ToString() + ["process_id"] = obs.ProcessId.ToString(CultureInfo.InvariantCulture) } }); } diff --git a/src/Scanner/__Libraries/StellaOps.Scanner.Contracts/CallGraphModels.cs b/src/Scanner/__Libraries/StellaOps.Scanner.Contracts/CallGraphModels.cs index f40cbd48c..3dcecf46b 100644 --- a/src/Scanner/__Libraries/StellaOps.Scanner.Contracts/CallGraphModels.cs +++ b/src/Scanner/__Libraries/StellaOps.Scanner.Contracts/CallGraphModels.cs @@ -312,7 +312,7 @@ public static class CallGraphDigests { private static readonly JsonWriterOptions CanonicalJsonOptions = new() { - Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + Encoder = System.Text.Encodings.Web.JavaScriptEncoder.Default, Indented = false, SkipValidation = false }; diff --git a/src/Scanner/__Libraries/StellaOps.Scanner.Contracts/TASKS.md b/src/Scanner/__Libraries/StellaOps.Scanner.Contracts/TASKS.md new file mode 100644 index 000000000..2d3e395fa --- /dev/null +++ b/src/Scanner/__Libraries/StellaOps.Scanner.Contracts/TASKS.md @@ -0,0 +1,9 @@ +# Scanner Contracts Task Board + +This board mirrors active sprint tasks for this module. +Source of truth: `docs/implplan/SPRINT_20260112_003_BE_csproj_audit_pending_apply.md` and `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229_049_BE_csproj_audit_maint_tests.md`. + +| Task ID | Status | Notes | +| --- | --- | --- | +| AUDIT-HOTLIST-SCANNER-CONTRACTS-0001 | DONE | Applied safe JSON encoder and test coverage update. | +| AUDIT-0946-A | DONE | Audit tracker updated for Scanner.Contracts apply. | diff --git a/src/Scanner/__Libraries/StellaOps.Scanner.Reachability/Cache/PrReachabilityGate.cs b/src/Scanner/__Libraries/StellaOps.Scanner.Reachability/Cache/PrReachabilityGate.cs index 216056041..c15a5bd45 100644 --- a/src/Scanner/__Libraries/StellaOps.Scanner.Reachability/Cache/PrReachabilityGate.cs +++ b/src/Scanner/__Libraries/StellaOps.Scanner.Reachability/Cache/PrReachabilityGate.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Text; using Microsoft.Extensions.Logging; @@ -414,23 +415,23 @@ public sealed class PrReachabilityGate : IPrReachabilityGate var sb = new StringBuilder(); sb.AppendLine(passed - ? "## ✅ Reachability Gate Passed" - : "## ❌ Reachability Gate Blocked"); + ? "## [OK] Reachability Gate Passed" + : "## [BLOCKED] Reachability Gate Blocked"); sb.AppendLine(); sb.AppendLine("| Metric | Value |"); sb.AppendLine("|--------|-------|"); - sb.AppendLine($"| New reachable paths | {decision.NewReachableCount} |"); + sb.AppendLine($"| New reachable paths | {decision.NewReachableCount.ToString(CultureInfo.InvariantCulture)} |"); if (options.IncludeMitigatedInSummary) { - sb.AppendLine($"| Mitigated paths | {decision.MitigatedCount} |"); - sb.AppendLine($"| Net change | {decision.NetChange:+#;-#;0} |"); + sb.AppendLine($"| Mitigated paths | {decision.MitigatedCount.ToString(CultureInfo.InvariantCulture)} |"); + sb.AppendLine($"| Net change | {decision.NetChange.ToString("+#;-#;0", CultureInfo.InvariantCulture)} |"); } sb.AppendLine($"| Analysis type | {(decision.WasIncremental ? "Incremental" : "Full")} |"); - sb.AppendLine($"| Cache savings | {decision.SavingsRatio:P0} |"); - sb.AppendLine($"| Duration | {decision.Duration.TotalMilliseconds:F0}ms |"); + sb.AppendLine($"| Cache savings | {decision.SavingsRatio.ToString("P0", CultureInfo.InvariantCulture)} |"); + sb.AppendLine($"| Duration | {decision.Duration.TotalMilliseconds.ToString("F0", CultureInfo.InvariantCulture)}ms |"); if (!passed && decision.BlockingFlips.Count > 0) { @@ -440,12 +441,13 @@ public sealed class PrReachabilityGate : IPrReachabilityGate foreach (var flip in decision.BlockingFlips.Take(10)) { - sb.AppendLine($"- `{flip.EntryMethodKey}` -> `{flip.SinkMethodKey}` (confidence: {flip.Confidence:P0})"); + sb.AppendLine($"- `{flip.EntryMethodKey}` -> `{flip.SinkMethodKey}` (confidence: {flip.Confidence.ToString("P0", CultureInfo.InvariantCulture)})"); } if (decision.BlockingFlips.Count > 10) { - sb.AppendLine($"- ... and {decision.BlockingFlips.Count - 10} more"); + var remaining = decision.BlockingFlips.Count - 10; + sb.AppendLine($"- ... and {remaining.ToString(CultureInfo.InvariantCulture)} more"); } } diff --git a/src/Scanner/__Libraries/StellaOps.Scanner.Reachability/Explanation/PathExplanationService.cs b/src/Scanner/__Libraries/StellaOps.Scanner.Reachability/Explanation/PathExplanationService.cs index d67a8c072..f718b22b3 100644 --- a/src/Scanner/__Libraries/StellaOps.Scanner.Reachability/Explanation/PathExplanationService.cs +++ b/src/Scanner/__Libraries/StellaOps.Scanner.Reachability/Explanation/PathExplanationService.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -124,7 +125,7 @@ public sealed class PathExplanationService : IPathExplanationService } /// - public Task ExplainPathAsync( + public async Task ExplainPathAsync( RichGraph graph, string pathId, CancellationToken cancellationToken = default) @@ -145,20 +146,22 @@ public sealed class PathExplanationService : IPathExplanationService MaxPaths = 100 }; - var resultTask = ExplainAsync(graph, query, cancellationToken); - return resultTask.ContinueWith(t => + var result = await ExplainAsync(graph, query, cancellationToken).ConfigureAwait(false); + if (result.Paths.Count == 0) { - if (t.Result.Paths.Count == 0) - return null; + return null; + } - // If path index specified, return that specific one - if (parts.Length >= 3 && int.TryParse(parts[2], out var idx) && idx < t.Result.Paths.Count) - { - return t.Result.Paths[idx]; - } + // If path index specified, return that specific one + if (parts.Length >= 3 && + int.TryParse(parts[2], NumberStyles.Integer, CultureInfo.InvariantCulture, out var idx) && + idx >= 0 && + idx < result.Paths.Count) + { + return result.Paths[idx]; + } - return t.Result.Paths[0]; - }, cancellationToken); + return result.Paths[0]; } private static Dictionary> BuildEdgeLookup(RichGraph graph) diff --git a/src/Scanner/__Libraries/StellaOps.Scanner.Reachability/Jobs/ReachabilityEvidenceJobExecutor.cs b/src/Scanner/__Libraries/StellaOps.Scanner.Reachability/Jobs/ReachabilityEvidenceJobExecutor.cs index 12c653aec..4599024a7 100644 --- a/src/Scanner/__Libraries/StellaOps.Scanner.Reachability/Jobs/ReachabilityEvidenceJobExecutor.cs +++ b/src/Scanner/__Libraries/StellaOps.Scanner.Reachability/Jobs/ReachabilityEvidenceJobExecutor.cs @@ -4,6 +4,7 @@ using System.Collections.Immutable; using System.Diagnostics; +using System.Globalization; using Microsoft.Extensions.Logging; using StellaOps.Scanner.CallGraph; using StellaOps.Scanner.Explainability.Assumptions; @@ -11,6 +12,7 @@ using StellaOps.Scanner.Reachability.Binary; using StellaOps.Scanner.Reachability.Runtime; using StellaOps.Scanner.Reachability.Services; using StellaOps.Scanner.Reachability.Stack; +using StellaOps.Determinism; // Aliases to disambiguate types with same name in different namespaces using StackEntrypointType = StellaOps.Scanner.Reachability.Stack.EntrypointType; @@ -33,6 +35,7 @@ public sealed class ReachabilityEvidenceJobExecutor : IReachabilityEvidenceJobEx private readonly IRuntimeReachabilityCollector? _runtimeCollector; private readonly ILogger _logger; private readonly TimeProvider _timeProvider; + private readonly IGuidProvider _guidProvider; public ReachabilityEvidenceJobExecutor( ICveSymbolMappingService cveSymbolService, @@ -42,6 +45,7 @@ public sealed class ReachabilityEvidenceJobExecutor : IReachabilityEvidenceJobEx ILogger logger, IBinaryPatchVerifier? binaryPatchVerifier = null, IRuntimeReachabilityCollector? runtimeCollector = null, + IGuidProvider? guidProvider = null, TimeProvider? timeProvider = null) { _cveSymbolService = cveSymbolService ?? throw new ArgumentNullException(nameof(cveSymbolService)); @@ -51,6 +55,7 @@ public sealed class ReachabilityEvidenceJobExecutor : IReachabilityEvidenceJobEx _binaryPatchVerifier = binaryPatchVerifier; _runtimeCollector = runtimeCollector; _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _guidProvider = guidProvider ?? SystemGuidProvider.Instance; _timeProvider = timeProvider ?? TimeProvider.System; } @@ -409,7 +414,7 @@ public sealed class ReachabilityEvidenceJobExecutor : IReachabilityEvidenceJobEx // Create a minimal stack with Unknown verdict var unknownStack = new ReachabilityStack { - Id = Guid.NewGuid().ToString("N"), + Id = _guidProvider.NewGuid().ToString("N", CultureInfo.InvariantCulture), FindingId = $"{job.CveId}:{job.Purl}", Symbol = new StackVulnerableSymbol( Name: "unknown", diff --git a/src/Scanner/__Libraries/StellaOps.Scanner.Reachability/ReachabilityUnionWriter.cs b/src/Scanner/__Libraries/StellaOps.Scanner.Reachability/ReachabilityUnionWriter.cs index 8f77cc56c..37a09d5ad 100644 --- a/src/Scanner/__Libraries/StellaOps.Scanner.Reachability/ReachabilityUnionWriter.cs +++ b/src/Scanner/__Libraries/StellaOps.Scanner.Reachability/ReachabilityUnionWriter.cs @@ -6,6 +6,7 @@ using System.IO; using System.Linq; using System.Security.Cryptography; using System.Text; +using System.Text.Encodings.Web; using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -22,7 +23,7 @@ public sealed class ReachabilityUnionWriter private static readonly JsonWriterOptions JsonOptions = new() { - Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + Encoder = JavaScriptEncoder.Default, Indented = false, SkipValidation = false }; diff --git a/src/Scanner/__Libraries/StellaOps.Scanner.Reachability/RichGraphSemanticExtensions.cs b/src/Scanner/__Libraries/StellaOps.Scanner.Reachability/RichGraphSemanticExtensions.cs index 2ca3536ff..ba8b77851 100644 --- a/src/Scanner/__Libraries/StellaOps.Scanner.Reachability/RichGraphSemanticExtensions.cs +++ b/src/Scanner/__Libraries/StellaOps.Scanner.Reachability/RichGraphSemanticExtensions.cs @@ -1,4 +1,5 @@ using System.Collections.Immutable; +using System.Globalization; using System.Text.Json; using System.Text.Json.Serialization; @@ -94,7 +95,7 @@ public static class RichGraphSemanticExtensions return null; } - return double.TryParse(value, out var score) ? score : null; + return double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var score) ? score : null; } /// Gets the confidence score. @@ -106,7 +107,7 @@ public static class RichGraphSemanticExtensions return null; } - return double.TryParse(value, out var score) ? score : null; + return double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var score) ? score : null; } /// Checks if this node is an entrypoint. @@ -190,13 +191,13 @@ public sealed class RichGraphNodeSemanticBuilder public RichGraphNodeSemanticBuilder WithRiskScore(double score) { - _attributes[RichGraphSemanticAttributes.RiskScore] = score.ToString("F3"); + _attributes[RichGraphSemanticAttributes.RiskScore] = score.ToString("F3", CultureInfo.InvariantCulture); return this; } public RichGraphNodeSemanticBuilder WithConfidence(double score, string tier) { - _attributes[RichGraphSemanticAttributes.Confidence] = score.ToString("F3"); + _attributes[RichGraphSemanticAttributes.Confidence] = score.ToString("F3", CultureInfo.InvariantCulture); _attributes[RichGraphSemanticAttributes.ConfidenceTier] = tier; return this; } @@ -225,7 +226,7 @@ public sealed class RichGraphNodeSemanticBuilder public RichGraphNodeSemanticBuilder WithCweId(int cweId) { - _attributes[RichGraphSemanticAttributes.CweId] = cweId.ToString(); + _attributes[RichGraphSemanticAttributes.CweId] = cweId.ToString(CultureInfo.InvariantCulture); return this; } diff --git a/src/Scanner/__Libraries/StellaOps.Scanner.Reachability/RichGraphWriter.cs b/src/Scanner/__Libraries/StellaOps.Scanner.Reachability/RichGraphWriter.cs index 14421e534..7a7065d34 100644 --- a/src/Scanner/__Libraries/StellaOps.Scanner.Reachability/RichGraphWriter.cs +++ b/src/Scanner/__Libraries/StellaOps.Scanner.Reachability/RichGraphWriter.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Text.Encodings.Web; using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -22,7 +23,7 @@ public sealed class RichGraphWriter private static readonly JsonWriterOptions JsonOptions = new() { - Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + Encoder = JavaScriptEncoder.Default, Indented = false, SkipValidation = false }; diff --git a/src/Scanner/__Libraries/StellaOps.Scanner.Reachability/Slices/InMemorySliceCache.cs b/src/Scanner/__Libraries/StellaOps.Scanner.Reachability/Slices/InMemorySliceCache.cs index 9c553303e..c2ccc5cb9 100644 --- a/src/Scanner/__Libraries/StellaOps.Scanner.Reachability/Slices/InMemorySliceCache.cs +++ b/src/Scanner/__Libraries/StellaOps.Scanner.Reachability/Slices/InMemorySliceCache.cs @@ -11,6 +11,7 @@ public sealed class InMemorySliceCache : ISliceCache, IDisposable private readonly ConcurrentDictionary _cache = new(); private readonly ILogger _logger; private readonly TimeProvider _timeProvider; + private readonly CancellationTokenSource _evictionCts = new(); private readonly Timer _evictionTimer; private readonly SemaphoreSlim _evictionLock = new(1, 1); @@ -26,7 +27,7 @@ public sealed class InMemorySliceCache : ISliceCache, IDisposable _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _timeProvider = timeProvider ?? TimeProvider.System; _evictionTimer = new Timer( - _ => _ = EvictExpiredEntriesAsync(CancellationToken.None), + _ => _ = EvictExpiredEntriesAsync(_evictionCts.Token), null, TimeSpan.FromSeconds(EvictionIntervalSeconds), TimeSpan.FromSeconds(EvictionIntervalSeconds)); @@ -116,7 +117,19 @@ public sealed class InMemorySliceCache : ISliceCache, IDisposable private async Task EvictExpiredEntriesAsync(CancellationToken cancellationToken) { - if (!await _evictionLock.WaitAsync(0, cancellationToken).ConfigureAwait(false)) + if (cancellationToken.IsCancellationRequested) + { + return; + } + + try + { + if (!await _evictionLock.WaitAsync(0, cancellationToken).ConfigureAwait(false)) + { + return; + } + } + catch (OperationCanceledException) { return; } @@ -199,8 +212,14 @@ public sealed class InMemorySliceCache : ISliceCache, IDisposable public void Dispose() { + if (!_evictionCts.IsCancellationRequested) + { + _evictionCts.Cancel(); + } + _evictionTimer?.Dispose(); _evictionLock?.Dispose(); + _evictionCts.Dispose(); } private sealed record CacheEntry( diff --git a/src/Scanner/__Libraries/StellaOps.Scanner.Reachability/Stack/ReachabilityStackEvaluator.cs b/src/Scanner/__Libraries/StellaOps.Scanner.Reachability/Stack/ReachabilityStackEvaluator.cs index b07dac195..008df21a8 100644 --- a/src/Scanner/__Libraries/StellaOps.Scanner.Reachability/Stack/ReachabilityStackEvaluator.cs +++ b/src/Scanner/__Libraries/StellaOps.Scanner.Reachability/Stack/ReachabilityStackEvaluator.cs @@ -1,7 +1,9 @@ // SPDX-License-Identifier: AGPL-3.0-or-later // Copyright (c) StellaOps +using System.Globalization; using System.Text; +using StellaOps.Determinism; using StellaOps.Scanner.Explainability.Assumptions; namespace StellaOps.Scanner.Reachability.Stack; @@ -48,6 +50,18 @@ public interface IReachabilityStackEvaluator /// public sealed class ReachabilityStackEvaluator : IReachabilityStackEvaluator { + private readonly IGuidProvider _guidProvider; + + public ReachabilityStackEvaluator() + : this(SystemGuidProvider.Instance) + { + } + + public ReachabilityStackEvaluator(IGuidProvider guidProvider) + { + _guidProvider = guidProvider ?? throw new ArgumentNullException(nameof(guidProvider)); + } + /// public ReachabilityStack Evaluate( string findingId, @@ -63,7 +77,7 @@ public sealed class ReachabilityStackEvaluator : IReachabilityStackEvaluator return new ReachabilityStack { - Id = Guid.NewGuid().ToString("N"), + Id = _guidProvider.NewGuid().ToString("N", CultureInfo.InvariantCulture), FindingId = findingId, Symbol = symbol, StaticCallGraph = layer1, diff --git a/src/Scanner/__Libraries/StellaOps.Scanner.Reachability/StellaOps.Scanner.Reachability.csproj b/src/Scanner/__Libraries/StellaOps.Scanner.Reachability/StellaOps.Scanner.Reachability.csproj index 26e3ac866..0581df393 100644 --- a/src/Scanner/__Libraries/StellaOps.Scanner.Reachability/StellaOps.Scanner.Reachability.csproj +++ b/src/Scanner/__Libraries/StellaOps.Scanner.Reachability/StellaOps.Scanner.Reachability.csproj @@ -26,6 +26,8 @@ + + diff --git a/src/Scanner/__Libraries/StellaOps.Scanner.Reachability/SubgraphExtractor.cs b/src/Scanner/__Libraries/StellaOps.Scanner.Reachability/SubgraphExtractor.cs index cc588c98a..90106f227 100644 --- a/src/Scanner/__Libraries/StellaOps.Scanner.Reachability/SubgraphExtractor.cs +++ b/src/Scanner/__Libraries/StellaOps.Scanner.Reachability/SubgraphExtractor.cs @@ -1,8 +1,10 @@ // Copyright (c) StellaOps. Licensed under AGPL-3.0-or-later. using System.Collections.Concurrent; +using System.Globalization; using Microsoft.Extensions.Logging; using StellaOps.Attestor; +using StellaOps.Determinism; namespace StellaOps.Scanner.Reachability; @@ -16,17 +18,29 @@ public class SubgraphExtractor : IReachabilityResolver private readonly IEntryPointResolver _entryPointResolver; private readonly IVulnSurfaceService _vulnSurfaceService; private readonly ILogger _logger; + private readonly IGuidProvider _guidProvider; public SubgraphExtractor( IRichGraphStore graphStore, IEntryPointResolver entryPointResolver, IVulnSurfaceService vulnSurfaceService, ILogger logger) + : this(graphStore, entryPointResolver, vulnSurfaceService, logger, SystemGuidProvider.Instance) + { + } + + public SubgraphExtractor( + IRichGraphStore graphStore, + IEntryPointResolver entryPointResolver, + IVulnSurfaceService vulnSurfaceService, + ILogger logger, + IGuidProvider guidProvider) { _graphStore = graphStore ?? throw new ArgumentNullException(nameof(graphStore)); _entryPointResolver = entryPointResolver ?? throw new ArgumentNullException(nameof(entryPointResolver)); _vulnSurfaceService = vulnSurfaceService ?? throw new ArgumentNullException(nameof(vulnSurfaceService)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _guidProvider = guidProvider ?? throw new ArgumentNullException(nameof(guidProvider)); } public async Task ResolveAsync( @@ -206,7 +220,7 @@ public class SubgraphExtractor : IReachabilityResolver if (sinkSet.Contains(current)) { paths.Add(new CallPath( - PathId: Guid.NewGuid().ToString(), + PathId: _guidProvider.NewGuid().ToString("N", CultureInfo.InvariantCulture), Nodes: path.ToList(), Edges: ExtractEdgesFromPath(path, graph), Length: path.Count - 1, diff --git a/src/Scanner/__Libraries/StellaOps.Scanner.Reachability/Witnesses/SuppressionDsseSigner.cs b/src/Scanner/__Libraries/StellaOps.Scanner.Reachability/Witnesses/SuppressionDsseSigner.cs index 6586adad2..262b2627a 100644 --- a/src/Scanner/__Libraries/StellaOps.Scanner.Reachability/Witnesses/SuppressionDsseSigner.cs +++ b/src/Scanner/__Libraries/StellaOps.Scanner.Reachability/Witnesses/SuppressionDsseSigner.cs @@ -1,6 +1,8 @@ -using System.Text; +using System.Text.Encodings.Web; using System.Text.Json; +using System.Text.Json.Serialization; using StellaOps.Attestor.Envelope; +using StellaOps.Canonical.Json; namespace StellaOps.Scanner.Reachability.Witnesses; @@ -11,11 +13,12 @@ namespace StellaOps.Scanner.Reachability.Witnesses; public sealed class SuppressionDsseSigner : ISuppressionDsseSigner { private readonly EnvelopeSignatureService _signatureService; - private static readonly JsonSerializerOptions CanonicalJsonOptions = new(JsonSerializerDefaults.Web) + private static readonly JsonSerializerOptions CanonicalJsonOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, WriteIndented = false, - DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + Encoder = JavaScriptEncoder.Default }; /// @@ -44,13 +47,10 @@ public sealed class SuppressionDsseSigner : ISuppressionDsseSigner try { // Serialize witness to canonical JSON bytes - var payloadBytes = JsonSerializer.SerializeToUtf8Bytes(witness, CanonicalJsonOptions); + var payloadBytes = CanonJson.Canonicalize(witness, CanonicalJsonOptions); - // Build the PAE (Pre-Authentication Encoding) for DSSE - var pae = BuildPae(SuppressionWitnessSchema.DssePayloadType, payloadBytes); - - // Sign the PAE - var signResult = _signatureService.Sign(pae, signingKey, cancellationToken); + // Sign DSSE payload using the shared PAE helper + var signResult = _signatureService.SignDsse(SuppressionWitnessSchema.DssePayloadType, payloadBytes, signingKey, cancellationToken); if (!signResult.IsSuccess) { return SuppressionDsseResult.Failure($"Signing failed: {signResult.Error?.Message}"); @@ -114,12 +114,10 @@ public sealed class SuppressionDsseSigner : ISuppressionDsseSigner return SuppressionVerifyResult.Failure($"No signature found for key ID: {publicKey.KeyId}"); } - // Build PAE and verify signature - var pae = BuildPae(envelope.PayloadType, envelope.Payload.ToArray()); var signatureBytes = Convert.FromBase64String(matchingSignature.Signature); var envelopeSignature = new EnvelopeSignature(publicKey.KeyId, publicKey.AlgorithmId, signatureBytes); - var verifyResult = _signatureService.Verify(pae, envelopeSignature, publicKey, cancellationToken); + var verifyResult = _signatureService.VerifyDsse(envelope.PayloadType, envelope.Payload.Span, envelopeSignature, publicKey, cancellationToken); if (!verifyResult.IsSuccess) { return SuppressionVerifyResult.Failure($"Signature verification failed: {verifyResult.Error?.Message}"); @@ -133,43 +131,6 @@ public sealed class SuppressionDsseSigner : ISuppressionDsseSigner } } - /// - /// Builds the DSSE Pre-Authentication Encoding (PAE) for a payload. - /// PAE = "DSSEv1" SP len(type) SP type SP len(payload) SP payload - /// - private static byte[] BuildPae(string payloadType, byte[] payload) - { - var typeBytes = Encoding.UTF8.GetBytes(payloadType); - - using var stream = new MemoryStream(); - using var writer = new BinaryWriter(stream, Encoding.UTF8, leaveOpen: true); - - // Write "DSSEv1 " - writer.Write(Encoding.UTF8.GetBytes("DSSEv1 ")); - - // Write len(type) as ASCII decimal string followed by space - WriteLengthAndSpace(writer, typeBytes.Length); - - // Write type followed by space - writer.Write(typeBytes); - writer.Write((byte)' '); - - // Write len(payload) as ASCII decimal string followed by space - WriteLengthAndSpace(writer, payload.Length); - - // Write payload - writer.Write(payload); - - writer.Flush(); - return stream.ToArray(); - } - - private static void WriteLengthAndSpace(BinaryWriter writer, int length) - { - // Write length as ASCII decimal string - writer.Write(Encoding.UTF8.GetBytes(length.ToString())); - writer.Write((byte)' '); - } } /// diff --git a/src/Scanner/__Libraries/StellaOps.Scanner.Reachability/Witnesses/WitnessDsseSigner.cs b/src/Scanner/__Libraries/StellaOps.Scanner.Reachability/Witnesses/WitnessDsseSigner.cs index c030c1940..45a83abfb 100644 --- a/src/Scanner/__Libraries/StellaOps.Scanner.Reachability/Witnesses/WitnessDsseSigner.cs +++ b/src/Scanner/__Libraries/StellaOps.Scanner.Reachability/Witnesses/WitnessDsseSigner.cs @@ -1,6 +1,8 @@ -using System.Text; +using System.Text.Encodings.Web; using System.Text.Json; +using System.Text.Json.Serialization; using StellaOps.Attestor.Envelope; +using StellaOps.Canonical.Json; namespace StellaOps.Scanner.Reachability.Witnesses; @@ -11,11 +13,12 @@ namespace StellaOps.Scanner.Reachability.Witnesses; public sealed class WitnessDsseSigner : IWitnessDsseSigner { private readonly EnvelopeSignatureService _signatureService; - private static readonly JsonSerializerOptions CanonicalJsonOptions = new(JsonSerializerDefaults.Web) + private static readonly JsonSerializerOptions CanonicalJsonOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, WriteIndented = false, - DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + Encoder = JavaScriptEncoder.Default }; /// @@ -44,13 +47,10 @@ public sealed class WitnessDsseSigner : IWitnessDsseSigner try { // Serialize witness to canonical JSON bytes - var payloadBytes = JsonSerializer.SerializeToUtf8Bytes(witness, CanonicalJsonOptions); + var payloadBytes = CanonJson.Canonicalize(witness, CanonicalJsonOptions); - // Build the PAE (Pre-Authentication Encoding) for DSSE - var pae = BuildPae(WitnessSchema.DssePayloadType, payloadBytes); - - // Sign the PAE - var signResult = _signatureService.Sign(pae, signingKey, cancellationToken); + // Sign DSSE payload using the shared PAE helper + var signResult = _signatureService.SignDsse(WitnessSchema.DssePayloadType, payloadBytes, signingKey, cancellationToken); if (!signResult.IsSuccess) { return WitnessDsseResult.Failure($"Signing failed: {signResult.Error?.Message}"); @@ -114,12 +114,10 @@ public sealed class WitnessDsseSigner : IWitnessDsseSigner return WitnessVerifyResult.Failure($"No signature found for key ID: {publicKey.KeyId}"); } - // Build PAE and verify signature - var pae = BuildPae(envelope.PayloadType, envelope.Payload.ToArray()); var signatureBytes = Convert.FromBase64String(matchingSignature.Signature); var envelopeSignature = new EnvelopeSignature(publicKey.KeyId, publicKey.AlgorithmId, signatureBytes); - var verifyResult = _signatureService.Verify(pae, envelopeSignature, publicKey, cancellationToken); + var verifyResult = _signatureService.VerifyDsse(envelope.PayloadType, envelope.Payload.Span, envelopeSignature, publicKey, cancellationToken); if (!verifyResult.IsSuccess) { return WitnessVerifyResult.Failure($"Signature verification failed: {verifyResult.Error?.Message}"); @@ -133,43 +131,6 @@ public sealed class WitnessDsseSigner : IWitnessDsseSigner } } - /// - /// Builds the DSSE Pre-Authentication Encoding (PAE) for a payload. - /// PAE = "DSSEv1" SP len(type) SP type SP len(payload) SP payload - /// - private static byte[] BuildPae(string payloadType, byte[] payload) - { - var typeBytes = Encoding.UTF8.GetBytes(payloadType); - - using var stream = new MemoryStream(); - using var writer = new BinaryWriter(stream, Encoding.UTF8, leaveOpen: true); - - // Write "DSSEv1 " - writer.Write(Encoding.UTF8.GetBytes("DSSEv1 ")); - - // Write len(type) as little-endian 8-byte integer followed by space - WriteLengthAndSpace(writer, typeBytes.Length); - - // Write type followed by space - writer.Write(typeBytes); - writer.Write((byte)' '); - - // Write len(payload) as little-endian 8-byte integer followed by space - WriteLengthAndSpace(writer, payload.Length); - - // Write payload - writer.Write(payload); - - writer.Flush(); - return stream.ToArray(); - } - - private static void WriteLengthAndSpace(BinaryWriter writer, int length) - { - // Write length as ASCII decimal string - writer.Write(Encoding.UTF8.GetBytes(length.ToString())); - writer.Write((byte)' '); - } } /// diff --git a/src/Scanner/__Libraries/StellaOps.Scanner.Surface.Secrets/ISurfaceSecretProvider.cs b/src/Scanner/__Libraries/StellaOps.Scanner.Surface.Secrets/ISurfaceSecretProvider.cs index f6ba45f6b..c775b7f3d 100644 --- a/src/Scanner/__Libraries/StellaOps.Scanner.Surface.Secrets/ISurfaceSecretProvider.cs +++ b/src/Scanner/__Libraries/StellaOps.Scanner.Surface.Secrets/ISurfaceSecretProvider.cs @@ -4,6 +4,8 @@ namespace StellaOps.Scanner.Surface.Secrets; public interface ISurfaceSecretProvider { + SurfaceSecretHandle Get(SurfaceSecretRequest request); + ValueTask GetAsync( SurfaceSecretRequest request, CancellationToken cancellationToken = default); diff --git a/src/Scanner/__Libraries/StellaOps.Scanner.Surface.Secrets/Providers/AuditingSurfaceSecretProvider.cs b/src/Scanner/__Libraries/StellaOps.Scanner.Surface.Secrets/Providers/AuditingSurfaceSecretProvider.cs index e3989dc8f..0efde7eab 100644 --- a/src/Scanner/__Libraries/StellaOps.Scanner.Surface.Secrets/Providers/AuditingSurfaceSecretProvider.cs +++ b/src/Scanner/__Libraries/StellaOps.Scanner.Surface.Secrets/Providers/AuditingSurfaceSecretProvider.cs @@ -29,6 +29,50 @@ internal sealed class AuditingSurfaceSecretProvider : ISurfaceSecretProvider _componentName = componentName ?? throw new ArgumentNullException(nameof(componentName)); } + public SurfaceSecretHandle Get(SurfaceSecretRequest request) + { + var startTime = _timeProvider.GetUtcNow(); + + try + { + var handle = _inner.Get(request); + + var elapsed = _timeProvider.GetUtcNow() - startTime; + LogAuditEvent( + request, + handle.Metadata, + success: true, + elapsed, + error: null); + + return handle; + } + catch (SurfaceSecretNotFoundException) + { + var elapsed = _timeProvider.GetUtcNow() - startTime; + LogAuditEvent( + request, + metadata: null, + success: false, + elapsed, + error: "NotFound"); + + throw; + } + catch (Exception ex) + { + var elapsed = _timeProvider.GetUtcNow() - startTime; + LogAuditEvent( + request, + metadata: null, + success: false, + elapsed, + error: ex.GetType().Name); + + throw; + } + } + public async ValueTask GetAsync( SurfaceSecretRequest request, CancellationToken cancellationToken = default) diff --git a/src/Scanner/__Libraries/StellaOps.Scanner.Surface.Secrets/Providers/CachingSurfaceSecretProvider.cs b/src/Scanner/__Libraries/StellaOps.Scanner.Surface.Secrets/Providers/CachingSurfaceSecretProvider.cs index a79de22df..3ed4455a8 100644 --- a/src/Scanner/__Libraries/StellaOps.Scanner.Surface.Secrets/Providers/CachingSurfaceSecretProvider.cs +++ b/src/Scanner/__Libraries/StellaOps.Scanner.Surface.Secrets/Providers/CachingSurfaceSecretProvider.cs @@ -35,6 +35,25 @@ internal sealed class CachingSurfaceSecretProvider : ISurfaceSecretProvider public TimeSpan CacheTtl => _ttl; + public SurfaceSecretHandle Get(SurfaceSecretRequest request) + { + var key = BuildCacheKey(request); + var now = _timeProvider.GetUtcNow(); + + if (_cache.TryGetValue(key, out var entry) && entry.ExpiresAt > now) + { + _logger.LogDebug("Surface secret cache hit for {Key}.", key); + return entry.Handle; + } + + var handle = _inner.Get(request); + var newEntry = new CacheEntry(handle, now.Add(_ttl)); + _cache[key] = newEntry; + + _logger.LogDebug("Surface secret cached for {Key}, expires at {ExpiresAt}.", key, newEntry.ExpiresAt); + return handle; + } + public async ValueTask GetAsync( SurfaceSecretRequest request, CancellationToken cancellationToken = default) diff --git a/src/Scanner/__Libraries/StellaOps.Scanner.Surface.Secrets/Providers/CompositeSurfaceSecretProvider.cs b/src/Scanner/__Libraries/StellaOps.Scanner.Surface.Secrets/Providers/CompositeSurfaceSecretProvider.cs index 5ce7cca6c..0990a8f3c 100644 --- a/src/Scanner/__Libraries/StellaOps.Scanner.Surface.Secrets/Providers/CompositeSurfaceSecretProvider.cs +++ b/src/Scanner/__Libraries/StellaOps.Scanner.Surface.Secrets/Providers/CompositeSurfaceSecretProvider.cs @@ -18,6 +18,23 @@ internal sealed class CompositeSurfaceSecretProvider : ISurfaceSecretProvider } } + public SurfaceSecretHandle Get(SurfaceSecretRequest request) + { + foreach (var provider in _providers) + { + try + { + return provider.Get(request); + } + catch (SurfaceSecretNotFoundException) + { + // try next provider + } + } + + throw new SurfaceSecretNotFoundException(request); + } + public async ValueTask GetAsync( SurfaceSecretRequest request, CancellationToken cancellationToken = default) diff --git a/src/Scanner/__Libraries/StellaOps.Scanner.Surface.Secrets/Providers/FileSurfaceSecretProvider.cs b/src/Scanner/__Libraries/StellaOps.Scanner.Surface.Secrets/Providers/FileSurfaceSecretProvider.cs index af8740883..a7021623e 100644 --- a/src/Scanner/__Libraries/StellaOps.Scanner.Surface.Secrets/Providers/FileSurfaceSecretProvider.cs +++ b/src/Scanner/__Libraries/StellaOps.Scanner.Surface.Secrets/Providers/FileSurfaceSecretProvider.cs @@ -20,6 +20,35 @@ internal sealed class FileSurfaceSecretProvider : ISurfaceSecretProvider _root = root; } + public SurfaceSecretHandle Get(SurfaceSecretRequest request) + { + if (request is null) + { + throw new ArgumentNullException(nameof(request)); + } + + var path = ResolvePath(request); + if (!File.Exists(path)) + { + throw new SurfaceSecretNotFoundException(request); + } + + var json = File.ReadAllText(path); + var descriptor = JsonSerializer.Deserialize(json); + if (descriptor is null) + { + throw new SurfaceSecretNotFoundException(request); + } + + if (string.IsNullOrWhiteSpace(descriptor.Payload)) + { + return SurfaceSecretHandle.Empty; + } + + var bytes = Convert.FromBase64String(descriptor.Payload); + return SurfaceSecretHandle.FromBytes(bytes, descriptor.Metadata); + } + public async ValueTask GetAsync( SurfaceSecretRequest request, CancellationToken cancellationToken = default) diff --git a/src/Scanner/__Libraries/StellaOps.Scanner.Surface.Secrets/Providers/InMemorySurfaceSecretProvider.cs b/src/Scanner/__Libraries/StellaOps.Scanner.Surface.Secrets/Providers/InMemorySurfaceSecretProvider.cs index b78a63184..560b358da 100644 --- a/src/Scanner/__Libraries/StellaOps.Scanner.Surface.Secrets/Providers/InMemorySurfaceSecretProvider.cs +++ b/src/Scanner/__Libraries/StellaOps.Scanner.Surface.Secrets/Providers/InMemorySurfaceSecretProvider.cs @@ -21,6 +21,21 @@ public sealed class InMemorySurfaceSecretProvider : ISurfaceSecretProvider _secrets[request.CacheKey] = handle; } + public SurfaceSecretHandle Get(SurfaceSecretRequest request) + { + if (request is null) + { + throw new ArgumentNullException(nameof(request)); + } + + if (_secrets.TryGetValue(request.CacheKey, out var handle)) + { + return handle; + } + + throw new SurfaceSecretNotFoundException(request); + } + public ValueTask GetAsync(SurfaceSecretRequest request, CancellationToken cancellationToken = default) { if (request is null) diff --git a/src/Scanner/__Libraries/StellaOps.Scanner.Surface.Secrets/Providers/InlineSurfaceSecretProvider.cs b/src/Scanner/__Libraries/StellaOps.Scanner.Surface.Secrets/Providers/InlineSurfaceSecretProvider.cs index 8afcb6b15..489aa72fd 100644 --- a/src/Scanner/__Libraries/StellaOps.Scanner.Surface.Secrets/Providers/InlineSurfaceSecretProvider.cs +++ b/src/Scanner/__Libraries/StellaOps.Scanner.Surface.Secrets/Providers/InlineSurfaceSecretProvider.cs @@ -14,6 +14,30 @@ internal sealed class InlineSurfaceSecretProvider : ISurfaceSecretProvider _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); } + public SurfaceSecretHandle Get(SurfaceSecretRequest request) + { + if (!_configuration.AllowInline) + { + throw new SurfaceSecretNotFoundException(request); + } + + var envKey = BuildEnvironmentKey(request); + var value = Environment.GetEnvironmentVariable(envKey); + if (string.IsNullOrWhiteSpace(value)) + { + throw new SurfaceSecretNotFoundException(request); + } + + var bytes = Convert.FromBase64String(value); + var metadata = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["source"] = "inline-env", + ["key"] = envKey + }; + + return SurfaceSecretHandle.FromBytes(bytes, metadata); + } + public ValueTask GetAsync( SurfaceSecretRequest request, CancellationToken cancellationToken = default) diff --git a/src/Scanner/__Libraries/StellaOps.Scanner.Surface.Secrets/Providers/KubernetesSurfaceSecretProvider.cs b/src/Scanner/__Libraries/StellaOps.Scanner.Surface.Secrets/Providers/KubernetesSurfaceSecretProvider.cs index 521a611b5..5f6d35607 100644 --- a/src/Scanner/__Libraries/StellaOps.Scanner.Surface.Secrets/Providers/KubernetesSurfaceSecretProvider.cs +++ b/src/Scanner/__Libraries/StellaOps.Scanner.Surface.Secrets/Providers/KubernetesSurfaceSecretProvider.cs @@ -23,6 +23,30 @@ internal sealed class KubernetesSurfaceSecretProvider : ISurfaceSecretProvider } } + public SurfaceSecretHandle Get(SurfaceSecretRequest request) + { + var directory = Path.Combine(_configuration.Root!, request.Tenant, request.Component, request.SecretType); + if (!Directory.Exists(directory)) + { + _logger.LogDebug("Kubernetes secret directory {Directory} not found.", directory); + throw new SurfaceSecretNotFoundException(request); + } + + var name = request.Name ?? "default"; + var payloadPath = Path.Combine(directory, name); + if (!File.Exists(payloadPath)) + { + throw new SurfaceSecretNotFoundException(request); + } + + var bytes = File.ReadAllBytes(payloadPath); + return SurfaceSecretHandle.FromBytes(bytes, new Dictionary + { + ["source"] = "kubernetes", + ["path"] = payloadPath + }); + } + public async ValueTask GetAsync( SurfaceSecretRequest request, CancellationToken cancellationToken = default) diff --git a/src/Scanner/__Libraries/StellaOps.Scanner.Surface.Secrets/Providers/OfflineSurfaceSecretProvider.cs b/src/Scanner/__Libraries/StellaOps.Scanner.Surface.Secrets/Providers/OfflineSurfaceSecretProvider.cs index db437979b..868a5a775 100644 --- a/src/Scanner/__Libraries/StellaOps.Scanner.Surface.Secrets/Providers/OfflineSurfaceSecretProvider.cs +++ b/src/Scanner/__Libraries/StellaOps.Scanner.Surface.Secrets/Providers/OfflineSurfaceSecretProvider.cs @@ -42,6 +42,73 @@ internal sealed class OfflineSurfaceSecretProvider : ISurfaceSecretProvider } } + public SurfaceSecretHandle Get(SurfaceSecretRequest request) + { + if (request is null) + { + throw new ArgumentNullException(nameof(request)); + } + + var directory = Path.Combine(_root, request.Tenant, request.Component, request.SecretType); + if (!Directory.Exists(directory)) + { + _logger.LogDebug("Offline secret directory {Directory} not found.", directory); + throw new SurfaceSecretNotFoundException(request); + } + + // Deterministic selection: if name is specified use it, otherwise pick lexicographically smallest + var targetName = request.Name ?? SelectDeterministicName(directory); + if (targetName is null) + { + throw new SurfaceSecretNotFoundException(request); + } + + var path = Path.Combine(directory, targetName + ".json"); + if (!File.Exists(path)) + { + throw new SurfaceSecretNotFoundException(request); + } + + var json = File.ReadAllText(path); + var descriptor = JsonSerializer.Deserialize(json); + if (descriptor is null) + { + throw new SurfaceSecretNotFoundException(request); + } + + if (string.IsNullOrWhiteSpace(descriptor.Payload)) + { + return SurfaceSecretHandle.Empty; + } + + var bytes = Convert.FromBase64String(descriptor.Payload); + + // Verify integrity if manifest entry exists + var manifestKey = BuildManifestKey(request.Tenant, request.Component, request.SecretType, targetName); + if (_manifest?.TryGetValue(manifestKey, out var entry) == true) + { + var actualHash = ComputeSha256(bytes); + if (!string.Equals(actualHash, entry.Sha256, StringComparison.OrdinalIgnoreCase)) + { + _logger.LogError( + "Offline secret integrity check failed for {Key}. Expected {Expected}, got {Actual}.", + manifestKey, + entry.Sha256, + actualHash); + throw new InvalidOperationException($"Offline secret integrity check failed for {manifestKey}."); + } + + _logger.LogDebug("Offline secret integrity verified for {Key}.", manifestKey); + } + + var metadata = descriptor.Metadata ?? new Dictionary(); + metadata["source"] = "offline"; + metadata["path"] = path; + metadata["name"] = targetName; + + return SurfaceSecretHandle.FromBytes(bytes, metadata); + } + public async ValueTask GetAsync( SurfaceSecretRequest request, CancellationToken cancellationToken = default) diff --git a/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.Deno.Tests/Deno/DenoRuntimeTraceRecorderTests.cs b/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.Deno.Tests/Deno/DenoRuntimeTraceRecorderTests.cs index 6f48a5112..6f3963740 100644 --- a/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.Deno.Tests/Deno/DenoRuntimeTraceRecorderTests.cs +++ b/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.Deno.Tests/Deno/DenoRuntimeTraceRecorderTests.cs @@ -10,7 +10,9 @@ public sealed class DenoRuntimeTraceRecorderTests var root = TestPaths.CreateTemporaryDirectory(); try { - var recorder = new DenoRuntimeTraceRecorder(root); + var recorder = new DenoRuntimeTraceRecorder( + root, + new FixedTimeProvider(DateTimeOffset.Parse("2025-11-17T12:00:00Z"))); recorder.AddPermissionUse( absoluteModulePath: Path.Combine(root, "c.ts"), @@ -58,4 +60,16 @@ public sealed class DenoRuntimeTraceRecorderTests TestPaths.SafeDelete(root); } } + + private sealed class FixedTimeProvider : TimeProvider + { + private readonly DateTimeOffset _fixedTime; + + public FixedTimeProvider(DateTimeOffset fixedTime) + { + _fixedTime = fixedTime; + } + + public override DateTimeOffset GetUtcNow() => _fixedTime; + } } diff --git a/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.Deno.Tests/Deno/DenoRuntimeTraceRunnerTests.cs b/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.Deno.Tests/Deno/DenoRuntimeTraceRunnerTests.cs index 6d3d67738..b087e3a43 100644 --- a/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.Deno.Tests/Deno/DenoRuntimeTraceRunnerTests.cs +++ b/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.Deno.Tests/Deno/DenoRuntimeTraceRunnerTests.cs @@ -61,13 +61,65 @@ public sealed class DenoRuntimeTraceRunnerTests await File.WriteAllTextAsync(entrypoint, "console.log('hi')", TestContext.Current.CancellationToken); using var entryEnv = new EnvironmentVariableScope("STELLA_DENO_ENTRYPOINT", entrypoint); - using var binaryEnv = new EnvironmentVariableScope("STELLA_DENO_BINARY", Guid.NewGuid().ToString("N")); + using var binaryEnv = new EnvironmentVariableScope("STELLA_DENO_BINARY", Path.Combine("tools", "deno")); var context = new LanguageAnalyzerContext(root, TimeProvider.System); var result = await DenoRuntimeTraceRunner.TryExecuteAsync(context, logger: null, TestContext.Current.CancellationToken); Assert.False(result); - Assert.True(File.Exists(Path.Combine(root, DenoRuntimeShim.FileName))); + Assert.False(File.Exists(Path.Combine(root, DenoRuntimeShim.FileName))); + } + finally + { + TestPaths.SafeDelete(root); + } + } + + [Fact] + public async Task ReturnsFalse_WhenEntrypointOutsideRoot() + { + var root = TestPaths.CreateTemporaryDirectory(); + var externalRoot = TestPaths.CreateTemporaryDirectory(); + + try + { + var externalEntrypoint = Path.Combine(externalRoot, "main.ts"); + await File.WriteAllTextAsync(externalEntrypoint, "console.log('hi')", TestContext.Current.CancellationToken); + + using var entryEnv = new EnvironmentVariableScope("STELLA_DENO_ENTRYPOINT", externalEntrypoint); + var context = new LanguageAnalyzerContext(root, TimeProvider.System); + + var result = await DenoRuntimeTraceRunner.TryExecuteAsync(context, logger: null, TestContext.Current.CancellationToken); + + Assert.False(result); + Assert.False(File.Exists(Path.Combine(root, DenoRuntimeShim.FileName))); + Assert.False(File.Exists(Path.Combine(root, "deno-runtime.ndjson"))); + } + finally + { + TestPaths.SafeDelete(externalRoot); + TestPaths.SafeDelete(root); + } + } + + [Fact] + public async Task ReturnsFalse_WhenDenoBinaryDisallowed() + { + var root = TestPaths.CreateTemporaryDirectory(); + + try + { + var entrypoint = Path.Combine(root, "main.ts"); + await File.WriteAllTextAsync(entrypoint, "console.log('hi')", TestContext.Current.CancellationToken); + + using var entryEnv = new EnvironmentVariableScope("STELLA_DENO_ENTRYPOINT", entrypoint); + using var binaryEnv = new EnvironmentVariableScope("STELLA_DENO_BINARY", "not-deno"); + + var context = new LanguageAnalyzerContext(root, TimeProvider.System); + var result = await DenoRuntimeTraceRunner.TryExecuteAsync(context, logger: null, TestContext.Current.CancellationToken); + + Assert.False(result); + Assert.False(File.Exists(Path.Combine(root, DenoRuntimeShim.FileName))); } finally { @@ -137,14 +189,7 @@ public sealed class DenoRuntimeTraceRunnerTests EOF """; File.WriteAllText(path, script); - try - { - System.Diagnostics.Process.Start("chmod", $"+x {path}")?.WaitForExit(); - } - catch - { - // best effort; on Windows this branch won't execute - } + File.SetUnixFileMode(path, UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute); } return path; diff --git a/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.Deno.Tests/Deno/DenoWorkspaceNormalizerTests.cs b/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.Deno.Tests/Deno/DenoWorkspaceNormalizerTests.cs index 9a2943f3d..42463c6e4 100644 --- a/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.Deno.Tests/Deno/DenoWorkspaceNormalizerTests.cs +++ b/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.Deno.Tests/Deno/DenoWorkspaceNormalizerTests.cs @@ -92,11 +92,11 @@ public sealed class DenoWorkspaceNormalizerTests if (vendorCacheEdges.Length == 0) { var sample = string.Join( - Environment.NewLine, + "\n", graph.Edges .Select(edge => $"{edge.ImportKind}:{edge.Specifier}:{edge.Provenance}") .Take(10)); - Assert.Fail($"Expected vendor cache edges but none were found. Sample edges:{Environment.NewLine}{sample}"); + Assert.Fail($"Expected vendor cache edges but none were found. Sample edges:\n{sample}"); } var vendorEdge = vendorCacheEdges.FirstOrDefault( @@ -104,9 +104,9 @@ public sealed class DenoWorkspaceNormalizerTests if (vendorEdge is null) { var details = string.Join( - Environment.NewLine, + "\n", vendorCacheEdges.Select(edge => $"{edge.Specifier} [{edge.Provenance}] -> {edge.Resolution}")); - Assert.Fail($"Unable to locate vendor cache edge for std server.ts. Observed edges:{Environment.NewLine}{details}"); + Assert.Fail($"Unable to locate vendor cache edge for std server.ts. Observed edges:\n{details}"); } var npmBridgeEdges = graph.Edges @@ -115,11 +115,11 @@ public sealed class DenoWorkspaceNormalizerTests if (npmBridgeEdges.Length == 0) { var bridgeSample = string.Join( - Environment.NewLine, + "\n", graph.Edges .Select(edge => $"{edge.ImportKind}:{edge.Specifier}:{edge.Resolution}") .Take(10)); - Assert.Fail($"No npm bridge edges discovered. Sample:{Environment.NewLine}{bridgeSample}"); + Assert.Fail($"No npm bridge edges discovered. Sample:\n{bridgeSample}"); } Assert.Contains( diff --git a/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.Deno.Tests/Golden/DenoAnalyzerGoldenTests.cs b/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.Deno.Tests/Golden/DenoAnalyzerGoldenTests.cs index 147742bd3..d9f1354e8 100644 --- a/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.Deno.Tests/Golden/DenoAnalyzerGoldenTests.cs +++ b/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.Deno.Tests/Golden/DenoAnalyzerGoldenTests.cs @@ -3,6 +3,7 @@ using Xunit; using System.Text.Encodings.Web; using System.Text.Json; using System.Text.Json.Nodes; +using System.Text.Unicode; using StellaOps.Scanner.Analyzers.Lang; using StellaOps.Scanner.Analyzers.Lang.Deno; using StellaOps.Scanner.Analyzers.Lang.Deno.Tests.TestFixtures; @@ -270,6 +271,6 @@ public sealed class DenoAnalyzerGoldenTests private static readonly JsonSerializerOptions JsonSerializerOptionsProvider = new() { WriteIndented = true, - Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping + Encoder = JavaScriptEncoder.Create(UnicodeRanges.BasicLatin) }; } diff --git a/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.Deno.Tests/TestUtilities/TestPaths.cs b/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.Deno.Tests/TestUtilities/TestPaths.cs index 312458808..519281252 100644 --- a/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.Deno.Tests/TestUtilities/TestPaths.cs +++ b/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.Deno.Tests/TestUtilities/TestPaths.cs @@ -1,7 +1,12 @@ +using System.Globalization; +using System.Threading; + namespace StellaOps.Scanner.Analyzers.Lang.Deno.Tests.TestUtilities; internal static class TestPaths { + private static long _tempCounter; + public static string ResolveFixture(params string[] segments) { var baseDirectory = AppContext.BaseDirectory; @@ -17,7 +22,8 @@ internal static class TestPaths public static string CreateTemporaryDirectory() { - var root = Path.Combine(AppContext.BaseDirectory, "tmp", Guid.NewGuid().ToString("N")); + var suffix = Interlocked.Increment(ref _tempCounter).ToString("D4", CultureInfo.InvariantCulture); + var root = Path.Combine(AppContext.BaseDirectory, "tmp", suffix); Directory.CreateDirectory(root); return root; } diff --git a/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.DotNet.Tests/DotNet/Bundling/BundlingSignalTests.cs b/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.DotNet.Tests/DotNet/Bundling/BundlingSignalTests.cs new file mode 100644 index 000000000..8f28eb3cc --- /dev/null +++ b/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.DotNet.Tests/DotNet/Bundling/BundlingSignalTests.cs @@ -0,0 +1,42 @@ +using System.Collections.Immutable; +using System.Globalization; +using StellaOps.Scanner.Analyzers.Lang.DotNet.Internal; +using Xunit; + +namespace StellaOps.Scanner.Analyzers.Lang.DotNet.Tests.DotNet.Bundling; + +public sealed class BundlingSignalTests +{ + [Fact] + public void ToMetadata_UsesInvariantCultureForNumericValues() + { + var originalCulture = CultureInfo.CurrentCulture; + var originalUiCulture = CultureInfo.CurrentUICulture; + + try + { + CultureInfo.CurrentCulture = new CultureInfo("ar-SA"); + CultureInfo.CurrentUICulture = new CultureInfo("ar-SA"); + + var signal = new BundlingSignal( + FilePath: "app.exe", + Kind: BundlingKind.ILMerge, + IsSkipped: false, + SkipReason: null, + Indicators: ImmutableArray.Empty, + SizeBytes: 123456, + EstimatedBundledAssemblies: 42); + + var metadata = signal.ToMetadata() + .ToDictionary(item => item.Key, item => item.Value, StringComparer.Ordinal); + + Assert.Equal("123456", metadata["bundle.sizeBytes"]); + Assert.Equal("42", metadata["bundle.estimatedAssemblies"]); + } + finally + { + CultureInfo.CurrentCulture = originalCulture; + CultureInfo.CurrentUICulture = originalUiCulture; + } + } +} diff --git a/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.DotNet.Tests/DotNet/Callgraph/DotNetCallgraphBuilderTests.cs b/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.DotNet.Tests/DotNet/Callgraph/DotNetCallgraphBuilderTests.cs new file mode 100644 index 000000000..601c0bcf6 --- /dev/null +++ b/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.DotNet.Tests/DotNet/Callgraph/DotNetCallgraphBuilderTests.cs @@ -0,0 +1,30 @@ +using StellaOps.Scanner.Analyzers.Lang.DotNet.Internal.Callgraph; +using Xunit; + +namespace StellaOps.Scanner.Analyzers.Lang.DotNet.Tests.DotNet.Callgraph; + +public sealed class DotNetCallgraphBuilderTests +{ + [Fact] + public void Build_UsesProvidedTimeProvider() + { + var fixedTime = new DateTimeOffset(2024, 1, 2, 3, 4, 5, TimeSpan.Zero); + var builder = new DotNetCallgraphBuilder("context-digest", new FixedTimeProvider(fixedTime)); + + var graph = builder.Build(); + + Assert.Equal(fixedTime, graph.Metadata.GeneratedAt); + } + + private sealed class FixedTimeProvider : TimeProvider + { + private readonly DateTimeOffset _utcNow; + + public FixedTimeProvider(DateTimeOffset utcNow) + { + _utcNow = utcNow; + } + + public override DateTimeOffset GetUtcNow() => _utcNow; + } +} diff --git a/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.DotNet.Tests/StellaOps.Scanner.Analyzers.Lang.DotNet.Tests.csproj b/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.DotNet.Tests/StellaOps.Scanner.Analyzers.Lang.DotNet.Tests.csproj index ab809f6bb..d57e208c9 100644 --- a/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.DotNet.Tests/StellaOps.Scanner.Analyzers.Lang.DotNet.Tests.csproj +++ b/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.DotNet.Tests/StellaOps.Scanner.Analyzers.Lang.DotNet.Tests.csproj @@ -8,7 +8,7 @@ enable false true - false + true diff --git a/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.DotNet.Tests/TestUtilities/DotNetFixtureBuilder.cs b/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.DotNet.Tests/TestUtilities/DotNetFixtureBuilder.cs index e62f67d8c..37b8f0176 100644 --- a/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.DotNet.Tests/TestUtilities/DotNetFixtureBuilder.cs +++ b/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.DotNet.Tests/TestUtilities/DotNetFixtureBuilder.cs @@ -1,4 +1,6 @@ +using System.Globalization; using System.Text; +using System.Threading; using StellaOps.Scanner.Analyzers.Lang.DotNet.Internal.Bundling; namespace StellaOps.Scanner.Analyzers.Lang.DotNet.Tests.TestUtilities; @@ -8,6 +10,7 @@ namespace StellaOps.Scanner.Analyzers.Lang.DotNet.Tests.TestUtilities; /// internal static class DotNetFixtureBuilder { + private static int _tempCounter; /// /// Creates a minimal SDK-style project file. /// @@ -373,7 +376,9 @@ internal static class DotNetFixtureBuilder /// public static string CreateTemporaryDirectory() { - var path = Path.Combine(Path.GetTempPath(), "stellaops-tests", Guid.NewGuid().ToString("N")); + var counter = Interlocked.Increment(ref _tempCounter) + .ToString(CultureInfo.InvariantCulture); + var path = Path.Combine(Path.GetTempPath(), "stellaops-tests", $"run-{counter}"); Directory.CreateDirectory(path); return path; } diff --git a/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Native.Tests/RuntimeCapture/Timeline/TimelineBuilderTests.cs b/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Native.Library.Tests/RuntimeCapture/Timeline/TimelineBuilderTests.cs similarity index 93% rename from src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Native.Tests/RuntimeCapture/Timeline/TimelineBuilderTests.cs rename to src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Native.Library.Tests/RuntimeCapture/Timeline/TimelineBuilderTests.cs index 779dba441..9deff76f8 100644 --- a/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Native.Tests/RuntimeCapture/Timeline/TimelineBuilderTests.cs +++ b/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Native.Library.Tests/RuntimeCapture/Timeline/TimelineBuilderTests.cs @@ -2,11 +2,12 @@ using FluentAssertions; using StellaOps.Scanner.Analyzers.Native.RuntimeCapture.Timeline; using Xunit; -namespace StellaOps.Scanner.Analyzers.Native.Tests.RuntimeCapture.Timeline; +namespace StellaOps.Scanner.Analyzers.Native.Library.Tests.RuntimeCapture.Timeline; public class TimelineBuilderTests { private readonly TimelineBuilder _builder = new(); + private static readonly DateTimeOffset BaseTime = new(2024, 1, 1, 0, 0, 0, TimeSpan.Zero); [Fact] public void Build_WithNoObservations_ReturnsUnknownPosture() @@ -78,15 +79,15 @@ public class TimelineBuilderTests { return new RuntimeEvidence { - FirstObservation = DateTimeOffset.UtcNow.AddHours(-1), - LastObservation = DateTimeOffset.UtcNow, + FirstObservation = BaseTime.AddHours(-1), + LastObservation = BaseTime, Observations = Array.Empty(), Sessions = new[] { new RuntimeSession { - StartTime = DateTimeOffset.UtcNow.AddHours(-1), - EndTime = DateTimeOffset.UtcNow, + StartTime = BaseTime.AddHours(-1), + EndTime = BaseTime, Platform = "linux-x64" } }, @@ -96,7 +97,7 @@ public class TimelineBuilderTests private static RuntimeEvidence CreateEvidenceWithoutComponent() { - var now = DateTimeOffset.UtcNow; + var now = BaseTime; return new RuntimeEvidence { FirstObservation = now.AddHours(-1), @@ -127,7 +128,7 @@ public class TimelineBuilderTests private static RuntimeEvidence CreateEvidenceWithNetworkExposure() { - var now = DateTimeOffset.UtcNow; + var now = BaseTime; return new RuntimeEvidence { FirstObservation = now.AddHours(-1), @@ -166,7 +167,7 @@ public class TimelineBuilderTests private static RuntimeEvidence CreateEvidenceOver24Hours() { - var start = DateTimeOffset.UtcNow.AddHours(-24); + var start = BaseTime.AddHours(-24); var observations = new List(); for (int i = 0; i < 24; i++) @@ -200,7 +201,7 @@ public class TimelineBuilderTests private static RuntimeEvidence CreateEvidenceWithComponentLoad() { - var now = DateTimeOffset.UtcNow; + var now = BaseTime; return new RuntimeEvidence { FirstObservation = now.AddHours(-1), @@ -231,7 +232,7 @@ public class TimelineBuilderTests private static RuntimeEvidence CreateEvidenceWith10Observations() { - var now = DateTimeOffset.UtcNow; + var now = BaseTime; var observations = new List(); for (int i = 0; i < 10; i++) diff --git a/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Native.Library.Tests/StellaOps.Scanner.Analyzers.Native.Library.Tests.csproj b/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Native.Library.Tests/StellaOps.Scanner.Analyzers.Native.Library.Tests.csproj new file mode 100644 index 000000000..def4db127 --- /dev/null +++ b/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Native.Library.Tests/StellaOps.Scanner.Analyzers.Native.Library.Tests.csproj @@ -0,0 +1,19 @@ + + + net10.0 + preview + enable + enable + true + false + true + + + + + + + + + + diff --git a/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Native.Tests/FixedTimeProvider.cs b/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Native.Tests/FixedTimeProvider.cs new file mode 100644 index 000000000..3cc685654 --- /dev/null +++ b/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Native.Tests/FixedTimeProvider.cs @@ -0,0 +1,13 @@ +namespace StellaOps.Scanner.Analyzers.Native.Tests; + +internal sealed class FixedTimeProvider : TimeProvider +{ + private readonly DateTimeOffset _fixedTime; + + public FixedTimeProvider(DateTimeOffset fixedTime) + { + _fixedTime = fixedTime; + } + + public override DateTimeOffset GetUtcNow() => _fixedTime; +} diff --git a/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Native.Tests/Hardening/ElfHardeningExtractorTests.cs b/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Native.Tests/Hardening/ElfHardeningExtractorTests.cs index 16986becb..0c765d24f 100644 --- a/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Native.Tests/Hardening/ElfHardeningExtractorTests.cs +++ b/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Native.Tests/Hardening/ElfHardeningExtractorTests.cs @@ -8,6 +8,7 @@ using System.Buffers.Binary; using FluentAssertions; using StellaOps.Scanner.Analyzers.Native.Hardening; +using StellaOps.Scanner.Analyzers.Native.Tests; using Xunit; namespace StellaOps.Scanner.Analyzers.Native.Tests.Hardening; @@ -18,7 +19,10 @@ namespace StellaOps.Scanner.Analyzers.Native.Tests.Hardening; /// public class ElfHardeningExtractorTests { - private readonly ElfHardeningExtractor _extractor = new(); + private static readonly TimeProvider FixedTimeProvider = + new FixedTimeProvider(new DateTimeOffset(2024, 1, 1, 0, 0, 0, TimeSpan.Zero)); + + private readonly ElfHardeningExtractor _extractor = new(FixedTimeProvider); #region Magic Detection Tests diff --git a/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Native.Tests/Hardening/HardeningScoreCalculatorTests.cs b/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Native.Tests/Hardening/HardeningScoreCalculatorTests.cs index 6794ba407..3b1f4d88c 100644 --- a/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Native.Tests/Hardening/HardeningScoreCalculatorTests.cs +++ b/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Native.Tests/Hardening/HardeningScoreCalculatorTests.cs @@ -17,6 +17,8 @@ namespace StellaOps.Scanner.Analyzers.Native.Tests.Hardening; /// public class HardeningScoreCalculatorTests { + private static readonly DateTimeOffset FixedTime = new(2024, 1, 1, 0, 0, 0, TimeSpan.Zero); + #region Score Range Tests [Fact] @@ -38,7 +40,7 @@ public class HardeningScoreCalculatorTests Flags: flags, HardeningScore: CalculateScore(flags, BinaryFormat.Elf), MissingFlags: [], - ExtractedAt: DateTimeOffset.UtcNow); + ExtractedAt: FixedTime); // Assert result.HardeningScore.Should().BeGreaterThanOrEqualTo(0.8); @@ -63,7 +65,7 @@ public class HardeningScoreCalculatorTests Flags: flags, HardeningScore: CalculateScore(flags, BinaryFormat.Elf), MissingFlags: ["PIE", "RELRO", "NX", "STACK_CANARY", "FORTIFY"], - ExtractedAt: DateTimeOffset.UtcNow); + ExtractedAt: FixedTime); // Assert result.HardeningScore.Should().Be(0); @@ -82,7 +84,7 @@ public class HardeningScoreCalculatorTests Flags: flags, HardeningScore: CalculateScore(flags, BinaryFormat.Elf), MissingFlags: [], - ExtractedAt: DateTimeOffset.UtcNow); + ExtractedAt: FixedTime); // Assert result.HardeningScore.Should().Be(0); diff --git a/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Native.Tests/Hardening/PeHardeningExtractorTests.cs b/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Native.Tests/Hardening/PeHardeningExtractorTests.cs index b8bed5281..1123ca913 100644 --- a/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Native.Tests/Hardening/PeHardeningExtractorTests.cs +++ b/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Native.Tests/Hardening/PeHardeningExtractorTests.cs @@ -8,6 +8,7 @@ using System.Buffers.Binary; using FluentAssertions; using StellaOps.Scanner.Analyzers.Native.Hardening; +using StellaOps.Scanner.Analyzers.Native.Tests; using Xunit; namespace StellaOps.Scanner.Analyzers.Native.Tests.Hardening; @@ -18,7 +19,10 @@ namespace StellaOps.Scanner.Analyzers.Native.Tests.Hardening; /// public class PeHardeningExtractorTests { - private readonly PeHardeningExtractor _extractor = new(); + private static readonly TimeProvider FixedTimeProvider = + new FixedTimeProvider(new DateTimeOffset(2024, 1, 1, 0, 0, 0, TimeSpan.Zero)); + + private readonly PeHardeningExtractor _extractor = new(FixedTimeProvider); #region Magic Detection Tests diff --git a/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Native.Tests/Index/OfflineBuildIdIndexSignatureTests.cs b/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Native.Tests/Index/OfflineBuildIdIndexSignatureTests.cs index 3da8ae3e5..f3adcbdf7 100644 --- a/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Native.Tests/Index/OfflineBuildIdIndexSignatureTests.cs +++ b/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Native.Tests/Index/OfflineBuildIdIndexSignatureTests.cs @@ -3,6 +3,7 @@ using System.Text.Json; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using StellaOps.Cryptography; +using StellaOps.Scanner.Analyzers.Native.Tests; using StellaOps.Scanner.Analyzers.Native.Index; using StellaOps.Scanner.ProofSpine; using StellaOps.Scanner.ProofSpine.Options; @@ -10,33 +11,21 @@ using Xunit; namespace StellaOps.Scanner.Analyzers.Native.Index.Tests; -public sealed class OfflineBuildIdIndexSignatureTests : IDisposable +public sealed class OfflineBuildIdIndexSignatureTests { - private readonly string _tempDir; - - public OfflineBuildIdIndexSignatureTests() - { - _tempDir = Path.Combine(Path.GetTempPath(), $"buildid-sig-test-{Guid.NewGuid():N}"); - Directory.CreateDirectory(_tempDir); - } - - public void Dispose() - { - if (Directory.Exists(_tempDir)) - { - Directory.Delete(_tempDir, recursive: true); - } - } + private static readonly TimeProvider FixedTimeProvider = + new FixedTimeProvider(new DateTimeOffset(2024, 1, 1, 0, 0, 0, TimeSpan.Zero)); [Fact] public async Task LoadAsync_RequiresTrustedDsseSignature_WhenEnabled() { - var indexPath = Path.Combine(_tempDir, "index.ndjson"); + using var temp = TempDirectory.Create(); + var indexPath = Path.Combine(temp.Path, "index.ndjson"); await File.WriteAllTextAsync(indexPath, """ {"build_id":"gnu-build-id:abc123","purl":"pkg:deb/debian/libc6@2.31","distro":"debian","confidence":"exact","indexed_at":"2025-01-15T10:00:00Z"} """); - var signaturePath = Path.Combine(_tempDir, "index.ndjson.dsse.json"); + var signaturePath = Path.Combine(temp.Path, "index.ndjson.dsse.json"); await File.WriteAllTextAsync(signaturePath, CreateDsseSignature(indexPath, expectedSha256: ComputeSha256Hex(indexPath))); var dsseService = CreateTrustedDsseService(keyId: "buildid-index-test-key", secretBase64: Convert.ToBase64String("supersecret-supersecret-supersecret"u8.ToArray())); @@ -48,7 +37,7 @@ public sealed class OfflineBuildIdIndexSignatureTests : IDisposable RequireSignature = true, }); - var index = new OfflineBuildIdIndex(options, NullLogger.Instance, dsseService); + var index = new OfflineBuildIdIndex(options, NullLogger.Instance, FixedTimeProvider, dsseService); await index.LoadAsync(); Assert.True(index.IsLoaded); @@ -62,12 +51,13 @@ public sealed class OfflineBuildIdIndexSignatureTests : IDisposable [Fact] public async Task LoadAsync_RefusesToLoadIndex_WhenDigestDoesNotMatchSignaturePayload() { - var indexPath = Path.Combine(_tempDir, "index.ndjson"); + using var temp = TempDirectory.Create(); + var indexPath = Path.Combine(temp.Path, "index.ndjson"); await File.WriteAllTextAsync(indexPath, """ {"build_id":"gnu-build-id:abc123","purl":"pkg:deb/debian/libc6@2.31"} """); - var signaturePath = Path.Combine(_tempDir, "index.ndjson.dsse.json"); + var signaturePath = Path.Combine(temp.Path, "index.ndjson.dsse.json"); await File.WriteAllTextAsync(signaturePath, CreateDsseSignature(indexPath, expectedSha256: "deadbeef")); var dsseService = CreateTrustedDsseService(keyId: "buildid-index-test-key", secretBase64: Convert.ToBase64String("supersecret-supersecret-supersecret"u8.ToArray())); @@ -79,7 +69,7 @@ public sealed class OfflineBuildIdIndexSignatureTests : IDisposable RequireSignature = true, }); - var index = new OfflineBuildIdIndex(options, NullLogger.Instance, dsseService); + var index = new OfflineBuildIdIndex(options, NullLogger.Instance, FixedTimeProvider, dsseService); await index.LoadAsync(); Assert.True(index.IsLoaded); @@ -89,12 +79,13 @@ public sealed class OfflineBuildIdIndexSignatureTests : IDisposable [Fact] public async Task LoadAsync_RefusesToLoadIndex_WhenSignatureFileMissing() { - var indexPath = Path.Combine(_tempDir, "index.ndjson"); + using var temp = TempDirectory.Create(); + var indexPath = Path.Combine(temp.Path, "index.ndjson"); await File.WriteAllTextAsync(indexPath, """ {"build_id":"gnu-build-id:abc123","purl":"pkg:deb/debian/libc6@2.31"} """); - var signaturePath = Path.Combine(_tempDir, "missing.dsse.json"); + var signaturePath = Path.Combine(temp.Path, "missing.dsse.json"); var dsseService = CreateTrustedDsseService(keyId: "buildid-index-test-key", secretBase64: Convert.ToBase64String("supersecret-supersecret-supersecret"u8.ToArray())); var options = Options.Create(new BuildIdIndexOptions @@ -104,7 +95,7 @@ public sealed class OfflineBuildIdIndexSignatureTests : IDisposable RequireSignature = true, }); - var index = new OfflineBuildIdIndex(options, NullLogger.Instance, dsseService); + var index = new OfflineBuildIdIndex(options, NullLogger.Instance, FixedTimeProvider, dsseService); await index.LoadAsync(); Assert.True(index.IsLoaded); diff --git a/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Native.Tests/Index/OfflineBuildIdIndexTests.cs b/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Native.Tests/Index/OfflineBuildIdIndexTests.cs index 51289bee2..1d7d6dfb4 100644 --- a/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Native.Tests/Index/OfflineBuildIdIndexTests.cs +++ b/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Native.Tests/Index/OfflineBuildIdIndexTests.cs @@ -1,6 +1,7 @@ using System.Text.Json; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; +using StellaOps.Scanner.Analyzers.Native.Tests; using Xunit; namespace StellaOps.Scanner.Analyzers.Native.Index.Tests; @@ -8,23 +9,10 @@ namespace StellaOps.Scanner.Analyzers.Native.Index.Tests; /// /// Unit tests for . /// -public sealed class OfflineBuildIdIndexTests : IDisposable +public sealed class OfflineBuildIdIndexTests { - private readonly string _tempDir; - - public OfflineBuildIdIndexTests() - { - _tempDir = Path.Combine(Path.GetTempPath(), $"buildid-test-{Guid.NewGuid():N}"); - Directory.CreateDirectory(_tempDir); - } - - public void Dispose() - { - if (Directory.Exists(_tempDir)) - { - Directory.Delete(_tempDir, recursive: true); - } - } + private static readonly TimeProvider FixedTimeProvider = + new FixedTimeProvider(new DateTimeOffset(2024, 1, 1, 0, 0, 0, TimeSpan.Zero)); #region Loading Tests @@ -32,7 +20,7 @@ public sealed class OfflineBuildIdIndexTests : IDisposable public async Task LoadAsync_EmptyIndex_WhenNoPathConfigured() { var options = Options.Create(new BuildIdIndexOptions { IndexPath = null }); - var index = new OfflineBuildIdIndex(options, NullLogger.Instance); + var index = new OfflineBuildIdIndex(options, NullLogger.Instance, FixedTimeProvider); await index.LoadAsync(); @@ -44,7 +32,7 @@ public sealed class OfflineBuildIdIndexTests : IDisposable public async Task LoadAsync_EmptyIndex_WhenFileNotFound() { var options = Options.Create(new BuildIdIndexOptions { IndexPath = "/nonexistent/file.ndjson" }); - var index = new OfflineBuildIdIndex(options, NullLogger.Instance); + var index = new OfflineBuildIdIndex(options, NullLogger.Instance, FixedTimeProvider); await index.LoadAsync(); @@ -55,7 +43,8 @@ public sealed class OfflineBuildIdIndexTests : IDisposable [Fact] public async Task LoadAsync_ParsesNdjsonEntries() { - var indexPath = Path.Combine(_tempDir, "index.ndjson"); + using var temp = TempDirectory.Create(); + var indexPath = Path.Combine(temp.Path, "index.ndjson"); await File.WriteAllTextAsync(indexPath, """ {"build_id":"gnu-build-id:abc123","purl":"pkg:deb/debian/libc6@2.31","distro":"debian","confidence":"exact","indexed_at":"2025-01-15T10:00:00Z"} {"build_id":"pe-cv:12345678-1234-1234-1234-123456789012-1","purl":"pkg:nuget/System.Text.Json@8.0.0","confidence":"inferred"} @@ -63,7 +52,7 @@ public sealed class OfflineBuildIdIndexTests : IDisposable """); var options = Options.Create(new BuildIdIndexOptions { IndexPath = indexPath, RequireSignature = false }); - var index = new OfflineBuildIdIndex(options, NullLogger.Instance); + var index = new OfflineBuildIdIndex(options, NullLogger.Instance, FixedTimeProvider); await index.LoadAsync(); @@ -74,7 +63,8 @@ public sealed class OfflineBuildIdIndexTests : IDisposable [Fact] public async Task LoadAsync_SkipsEmptyLines() { - var indexPath = Path.Combine(_tempDir, "index-empty-lines.ndjson"); + using var temp = TempDirectory.Create(); + var indexPath = Path.Combine(temp.Path, "index-empty-lines.ndjson"); await File.WriteAllTextAsync(indexPath, """ {"build_id":"gnu-build-id:abc123","purl":"pkg:deb/debian/libc6@2.31"} @@ -83,7 +73,7 @@ public sealed class OfflineBuildIdIndexTests : IDisposable """); var options = Options.Create(new BuildIdIndexOptions { IndexPath = indexPath, RequireSignature = false }); - var index = new OfflineBuildIdIndex(options, NullLogger.Instance); + var index = new OfflineBuildIdIndex(options, NullLogger.Instance, FixedTimeProvider); await index.LoadAsync(); @@ -93,7 +83,8 @@ public sealed class OfflineBuildIdIndexTests : IDisposable [Fact] public async Task LoadAsync_SkipsCommentLines() { - var indexPath = Path.Combine(_tempDir, "index-comments.ndjson"); + using var temp = TempDirectory.Create(); + var indexPath = Path.Combine(temp.Path, "index-comments.ndjson"); await File.WriteAllTextAsync(indexPath, """ # This is a comment {"build_id":"gnu-build-id:abc123","purl":"pkg:deb/debian/libc6@2.31"} @@ -102,7 +93,7 @@ public sealed class OfflineBuildIdIndexTests : IDisposable """); var options = Options.Create(new BuildIdIndexOptions { IndexPath = indexPath, RequireSignature = false }); - var index = new OfflineBuildIdIndex(options, NullLogger.Instance); + var index = new OfflineBuildIdIndex(options, NullLogger.Instance, FixedTimeProvider); await index.LoadAsync(); @@ -112,7 +103,8 @@ public sealed class OfflineBuildIdIndexTests : IDisposable [Fact] public async Task LoadAsync_SkipsInvalidJsonLines() { - var indexPath = Path.Combine(_tempDir, "index-invalid.ndjson"); + using var temp = TempDirectory.Create(); + var indexPath = Path.Combine(temp.Path, "index-invalid.ndjson"); await File.WriteAllTextAsync(indexPath, """ {"build_id":"gnu-build-id:abc123","purl":"pkg:deb/debian/libc6@2.31"} not valid json at all @@ -120,7 +112,7 @@ public sealed class OfflineBuildIdIndexTests : IDisposable """); var options = Options.Create(new BuildIdIndexOptions { IndexPath = indexPath, RequireSignature = false }); - var index = new OfflineBuildIdIndex(options, NullLogger.Instance); + var index = new OfflineBuildIdIndex(options, NullLogger.Instance, FixedTimeProvider); await index.LoadAsync(); @@ -134,13 +126,14 @@ public sealed class OfflineBuildIdIndexTests : IDisposable [Fact] public async Task LookupAsync_ReturnsNull_WhenNotFound() { - var indexPath = Path.Combine(_tempDir, "index.ndjson"); + using var temp = TempDirectory.Create(); + var indexPath = Path.Combine(temp.Path, "index.ndjson"); await File.WriteAllTextAsync(indexPath, """ {"build_id":"gnu-build-id:abc123","purl":"pkg:deb/debian/libc6@2.31"} """); var options = Options.Create(new BuildIdIndexOptions { IndexPath = indexPath, RequireSignature = false }); - var index = new OfflineBuildIdIndex(options, NullLogger.Instance); + var index = new OfflineBuildIdIndex(options, NullLogger.Instance, FixedTimeProvider); await index.LoadAsync(); var result = await index.LookupAsync("gnu-build-id:notfound"); @@ -151,13 +144,14 @@ public sealed class OfflineBuildIdIndexTests : IDisposable [Fact] public async Task LookupAsync_ReturnsNull_ForNullOrEmpty() { - var indexPath = Path.Combine(_tempDir, "index.ndjson"); + using var temp = TempDirectory.Create(); + var indexPath = Path.Combine(temp.Path, "index.ndjson"); await File.WriteAllTextAsync(indexPath, """ {"build_id":"gnu-build-id:abc123","purl":"pkg:deb/debian/libc6@2.31"} """); var options = Options.Create(new BuildIdIndexOptions { IndexPath = indexPath, RequireSignature = false }); - var index = new OfflineBuildIdIndex(options, NullLogger.Instance); + var index = new OfflineBuildIdIndex(options, NullLogger.Instance, FixedTimeProvider); await index.LoadAsync(); Assert.Null(await index.LookupAsync(null!)); @@ -168,13 +162,14 @@ public sealed class OfflineBuildIdIndexTests : IDisposable [Fact] public async Task LookupAsync_FindsExactMatch() { - var indexPath = Path.Combine(_tempDir, "index.ndjson"); + using var temp = TempDirectory.Create(); + var indexPath = Path.Combine(temp.Path, "index.ndjson"); await File.WriteAllTextAsync(indexPath, """ {"build_id":"gnu-build-id:abc123def456","purl":"pkg:deb/debian/libc6@2.31","version":"2.31","distro":"debian","confidence":"exact","indexed_at":"2025-01-15T10:00:00Z"} """); var options = Options.Create(new BuildIdIndexOptions { IndexPath = indexPath, RequireSignature = false }); - var index = new OfflineBuildIdIndex(options, NullLogger.Instance); + var index = new OfflineBuildIdIndex(options, NullLogger.Instance, FixedTimeProvider); await index.LoadAsync(); var result = await index.LookupAsync("gnu-build-id:abc123def456"); @@ -190,13 +185,14 @@ public sealed class OfflineBuildIdIndexTests : IDisposable [Fact] public async Task LookupAsync_CaseInsensitive() { - var indexPath = Path.Combine(_tempDir, "index.ndjson"); + using var temp = TempDirectory.Create(); + var indexPath = Path.Combine(temp.Path, "index.ndjson"); await File.WriteAllTextAsync(indexPath, """ {"build_id":"gnu-build-id:ABC123DEF456","purl":"pkg:deb/debian/libc6@2.31"} """); var options = Options.Create(new BuildIdIndexOptions { IndexPath = indexPath, RequireSignature = false }); - var index = new OfflineBuildIdIndex(options, NullLogger.Instance); + var index = new OfflineBuildIdIndex(options, NullLogger.Instance, FixedTimeProvider); await index.LoadAsync(); // Query with lowercase @@ -213,7 +209,8 @@ public sealed class OfflineBuildIdIndexTests : IDisposable [Fact] public async Task BatchLookupAsync_ReturnsFoundEntries() { - var indexPath = Path.Combine(_tempDir, "index.ndjson"); + using var temp = TempDirectory.Create(); + var indexPath = Path.Combine(temp.Path, "index.ndjson"); await File.WriteAllTextAsync(indexPath, """ {"build_id":"gnu-build-id:aaa","purl":"pkg:deb/debian/liba@1.0"} {"build_id":"gnu-build-id:bbb","purl":"pkg:deb/debian/libb@1.0"} @@ -221,7 +218,7 @@ public sealed class OfflineBuildIdIndexTests : IDisposable """); var options = Options.Create(new BuildIdIndexOptions { IndexPath = indexPath, RequireSignature = false }); - var index = new OfflineBuildIdIndex(options, NullLogger.Instance); + var index = new OfflineBuildIdIndex(options, NullLogger.Instance, FixedTimeProvider); await index.LoadAsync(); var results = await index.BatchLookupAsync(["gnu-build-id:aaa", "gnu-build-id:notfound", "gnu-build-id:ccc"]); @@ -234,13 +231,14 @@ public sealed class OfflineBuildIdIndexTests : IDisposable [Fact] public async Task BatchLookupAsync_SkipsNullAndEmpty() { - var indexPath = Path.Combine(_tempDir, "index.ndjson"); + using var temp = TempDirectory.Create(); + var indexPath = Path.Combine(temp.Path, "index.ndjson"); await File.WriteAllTextAsync(indexPath, """ {"build_id":"gnu-build-id:aaa","purl":"pkg:deb/debian/liba@1.0"} """); var options = Options.Create(new BuildIdIndexOptions { IndexPath = indexPath, RequireSignature = false }); - var index = new OfflineBuildIdIndex(options, NullLogger.Instance); + var index = new OfflineBuildIdIndex(options, NullLogger.Instance, FixedTimeProvider); await index.LoadAsync(); var results = await index.BatchLookupAsync([null!, "", " ", "gnu-build-id:aaa"]); @@ -263,12 +261,13 @@ public sealed class OfflineBuildIdIndexTests : IDisposable [InlineData("", BuildIdConfidence.Heuristic)] public async Task LoadAsync_ParsesConfidenceLevels(string confidenceValue, BuildIdConfidence expected) { - var indexPath = Path.Combine(_tempDir, "index.ndjson"); + using var temp = TempDirectory.Create(); + var indexPath = Path.Combine(temp.Path, "index.ndjson"); var entry = new { build_id = "gnu-build-id:test", purl = "pkg:test/test@1.0", confidence = confidenceValue }; await File.WriteAllTextAsync(indexPath, JsonSerializer.Serialize(entry)); var options = Options.Create(new BuildIdIndexOptions { IndexPath = indexPath, RequireSignature = false }); - var index = new OfflineBuildIdIndex(options, NullLogger.Instance); + var index = new OfflineBuildIdIndex(options, NullLogger.Instance, FixedTimeProvider); await index.LoadAsync(); var result = await index.LookupAsync("gnu-build-id:test"); diff --git a/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Native.Tests/Index/TempDirectory.cs b/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Native.Tests/Index/TempDirectory.cs new file mode 100644 index 000000000..9920ab8af --- /dev/null +++ b/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Native.Tests/Index/TempDirectory.cs @@ -0,0 +1,44 @@ +using System.Runtime.CompilerServices; + +namespace StellaOps.Scanner.Analyzers.Native.Index.Tests; + +internal sealed class TempDirectory : IDisposable +{ + public string Path { get; } + + private TempDirectory(string path) + { + Path = path; + Directory.CreateDirectory(path); + } + + public static TempDirectory Create([CallerMemberName] string? testName = null) + { + var safeName = Sanitize(testName ?? "unknown"); + var path = System.IO.Path.Combine( + System.IO.Path.GetTempPath(), + "stellaops-tests", + "buildid-index", + safeName); + return new TempDirectory(path); + } + + public void Dispose() + { + if (Directory.Exists(Path)) + { + Directory.Delete(Path, recursive: true); + } + } + + private static string Sanitize(string value) + { + var buffer = new char[value.Length]; + for (var i = 0; i < value.Length; i++) + { + var ch = value[i]; + buffer[i] = char.IsLetterOrDigit(ch) ? ch : '_'; + } + return new string(buffer); + } +} diff --git a/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Native.Tests/Reachability/RichgraphV1AlignmentTests.cs b/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Native.Tests/Reachability/RichgraphV1AlignmentTests.cs index d5d8721db..a1f6d7a60 100644 --- a/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Native.Tests/Reachability/RichgraphV1AlignmentTests.cs +++ b/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Native.Tests/Reachability/RichgraphV1AlignmentTests.cs @@ -14,6 +14,8 @@ namespace StellaOps.Scanner.Analyzers.Native.Tests.Reachability; /// public class RichgraphV1AlignmentTests { + private static readonly DateTimeOffset FixedTime = new(2024, 1, 1, 0, 0, 0, TimeSpan.Zero); + /// /// §8.2: SymbolID Construction uses sym: prefix with 16 hex chars. /// @@ -400,7 +402,7 @@ public class RichgraphV1AlignmentTests { // Arrange & Act var metadata = new TestGraphMetadata( - GeneratedAt: DateTimeOffset.UtcNow, + GeneratedAt: FixedTime, GeneratorVersion: "1.0.0", LayerDigest: "sha256:layer123", BinaryCount: 5, @@ -410,7 +412,7 @@ public class RichgraphV1AlignmentTests SyntheticRootCount: 8); // Assert - metadata.GeneratedAt.Should().BeCloseTo(DateTimeOffset.UtcNow, TimeSpan.FromMinutes(1)); + metadata.GeneratedAt.Should().Be(FixedTime); metadata.GeneratorVersion.Should().Be("1.0.0"); metadata.LayerDigest.Should().StartWith("sha256:"); metadata.BinaryCount.Should().BeGreaterThan(0); diff --git a/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Native.Tests/RuntimeCaptureTests.cs b/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Native.Tests/RuntimeCaptureTests.cs index b29a02b19..9e82d8690 100644 --- a/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Native.Tests/RuntimeCaptureTests.cs +++ b/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Native.Tests/RuntimeCaptureTests.cs @@ -1,10 +1,38 @@ using FluentAssertions; +using StellaOps.Determinism; using StellaOps.Scanner.Analyzers.Native.RuntimeCapture; using StellaOps.TestKit; using Xunit; namespace StellaOps.Scanner.Analyzers.Native.Tests; +internal static class RuntimeCaptureTestClock +{ + internal static readonly DateTime BaseTime = new(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc); + internal static readonly DateTimeOffset BaseTimeOffset = new(2024, 1, 1, 0, 0, 0, TimeSpan.Zero); +} + +internal static class RuntimeCaptureTestFactory +{ + internal static TimeProvider CreateTimeProvider() => new FixedTimeProvider(RuntimeCaptureTestClock.BaseTimeOffset); + + internal static IGuidProvider CreateGuidProvider() => new SequentialGuidProvider(); + + internal static IRuntimeCaptureAdapter? CreatePlatformAdapter(TimeProvider timeProvider, IGuidProvider guidProvider) + { + if (OperatingSystem.IsLinux()) + return new LinuxEbpfCaptureAdapter(timeProvider, guidProvider); + + if (OperatingSystem.IsWindows()) + return new WindowsEtwCaptureAdapter(timeProvider, guidProvider); + + if (OperatingSystem.IsMacOS()) + return new MacOsDyldCaptureAdapter(timeProvider, guidProvider); + + return null; + } +} + public class RuntimeCaptureOptionsTests { [Trait("Category", TestCategories.Unit)] @@ -261,7 +289,7 @@ public class RuntimeEvidenceAggregatorTests var events = new[] { new RuntimeLoadEvent( - DateTime.UtcNow.AddMinutes(-5), + RuntimeCaptureTestClock.BaseTime.AddMinutes(-5), ProcessId: 1234, ThreadId: 1, LoadType: RuntimeLoadType.Dlopen, @@ -273,7 +301,7 @@ public class RuntimeEvidenceAggregatorTests CallerModule: "myapp", CallerAddress: 0x400000), new RuntimeLoadEvent( - DateTime.UtcNow.AddMinutes(-4), + RuntimeCaptureTestClock.BaseTime.AddMinutes(-4), ProcessId: 1234, ThreadId: 1, LoadType: RuntimeLoadType.Dlopen, @@ -288,8 +316,8 @@ public class RuntimeEvidenceAggregatorTests var session = new RuntimeCaptureSession( SessionId: "test-session", - StartTime: DateTime.UtcNow.AddMinutes(-10), - EndTime: DateTime.UtcNow, + StartTime: RuntimeCaptureTestClock.BaseTime.AddMinutes(-10), + EndTime: RuntimeCaptureTestClock.BaseTime, Platform: "linux", CaptureMethod: "ebpf", TargetProcessId: 1234, @@ -315,7 +343,7 @@ public class RuntimeEvidenceAggregatorTests public void Aggregate_DuplicateLoads_AggregatesCorrectly() { // Arrange - var baseTime = DateTime.UtcNow.AddMinutes(-10); + var baseTime = RuntimeCaptureTestClock.BaseTime.AddMinutes(-10); var events = new[] { new RuntimeLoadEvent(baseTime, 1, 1, RuntimeLoadType.Dlopen, "libc.so.6", "/lib/libc.so.6", null, true, null, null, null), @@ -323,7 +351,7 @@ public class RuntimeEvidenceAggregatorTests new RuntimeLoadEvent(baseTime.AddMinutes(2), 1, 1, RuntimeLoadType.Dlopen, "libc.so.6", "/lib/libc.so.6", null, true, null, null, null), }; - var session = new RuntimeCaptureSession("test", baseTime, DateTime.UtcNow, "linux", "ebpf", 1, events, 0, 0); + var session = new RuntimeCaptureSession("test", baseTime, RuntimeCaptureTestClock.BaseTime, "linux", "ebpf", 1, events, 0, 0); // Act var evidence = RuntimeEvidenceAggregator.Aggregate([session]); @@ -341,10 +369,19 @@ public class RuntimeEvidenceAggregatorTests // Arrange var events = new[] { - new RuntimeLoadEvent(DateTime.UtcNow, 1, 1, RuntimeLoadType.Dlopen, "missing.so", null, null, false, -1, null, null), + new RuntimeLoadEvent(RuntimeCaptureTestClock.BaseTime, 1, 1, RuntimeLoadType.Dlopen, "missing.so", null, null, false, -1, null, null), }; - var session = new RuntimeCaptureSession("test", DateTime.UtcNow.AddMinutes(-1), DateTime.UtcNow, "linux", "ebpf", 1, events, 0, 0); + var session = new RuntimeCaptureSession( + "test", + RuntimeCaptureTestClock.BaseTime.AddMinutes(-1), + RuntimeCaptureTestClock.BaseTime, + "linux", + "ebpf", + 1, + events, + 0, + 0); // Act var evidence = RuntimeEvidenceAggregator.Aggregate([session]); @@ -359,8 +396,8 @@ public class RuntimeEvidenceAggregatorTests public void Aggregate_MultipleSessions_MergesCorrectly() { // Arrange - var time1 = DateTime.UtcNow.AddHours(-2); - var time2 = DateTime.UtcNow.AddHours(-1); + var time1 = RuntimeCaptureTestClock.BaseTime.AddHours(-2); + var time2 = RuntimeCaptureTestClock.BaseTime.AddHours(-1); var session1 = new RuntimeCaptureSession( "s1", time1, time1.AddMinutes(30), "linux", "ebpf", 1, @@ -387,8 +424,11 @@ public class RuntimeCaptureAdapterFactoryTests [Fact] public void CreateForCurrentPlatform_ReturnsAdapter() { + var timeProvider = RuntimeCaptureTestFactory.CreateTimeProvider(); + var guidProvider = RuntimeCaptureTestFactory.CreateGuidProvider(); + // Act - var adapter = RuntimeCaptureAdapterFactory.CreateForCurrentPlatform(); + var adapter = RuntimeCaptureAdapterFactory.CreateForCurrentPlatform(timeProvider, guidProvider); // Assert // Should return an adapter on Linux/Windows/macOS, null on other platforms @@ -406,8 +446,11 @@ public class RuntimeCaptureAdapterFactoryTests [Fact] public void GetAvailableAdapters_ReturnsAdaptersForCurrentPlatform() { + var timeProvider = RuntimeCaptureTestFactory.CreateTimeProvider(); + var guidProvider = RuntimeCaptureTestFactory.CreateGuidProvider(); + // Act - var adapters = RuntimeCaptureAdapterFactory.GetAvailableAdapters(); + var adapters = RuntimeCaptureAdapterFactory.GetAvailableAdapters(timeProvider, guidProvider); // Assert if (OperatingSystem.IsLinux() || OperatingSystem.IsWindows() || OperatingSystem.IsMacOS()) @@ -431,8 +474,8 @@ public class SandboxCaptureTests // Arrange var mockEvents = new[] { - new RuntimeLoadEvent(DateTime.UtcNow, 1, 1, RuntimeLoadType.Dlopen, "libtest.so", "/lib/libtest.so", null, true, null, null, null), - new RuntimeLoadEvent(DateTime.UtcNow, 1, 1, RuntimeLoadType.Dlopen, "libother.so", "/lib/libother.so", null, true, null, null, null), + new RuntimeLoadEvent(RuntimeCaptureTestClock.BaseTime, 1, 1, RuntimeLoadType.Dlopen, "libtest.so", "/lib/libtest.so", null, true, null, null, null), + new RuntimeLoadEvent(RuntimeCaptureTestClock.BaseTime, 1, 1, RuntimeLoadType.Dlopen, "libother.so", "/lib/libother.so", null, true, null, null, null), }; var options = new RuntimeCaptureOptions @@ -446,13 +489,9 @@ public class SandboxCaptureTests } }; - IRuntimeCaptureAdapter? adapter = null; - if (OperatingSystem.IsLinux()) - adapter = new LinuxEbpfCaptureAdapter(); - else if (OperatingSystem.IsWindows()) - adapter = new WindowsEtwCaptureAdapter(); - else if (OperatingSystem.IsMacOS()) - adapter = new MacOsDyldCaptureAdapter(); + var timeProvider = RuntimeCaptureTestFactory.CreateTimeProvider(); + var guidProvider = RuntimeCaptureTestFactory.CreateGuidProvider(); + var adapter = RuntimeCaptureTestFactory.CreatePlatformAdapter(timeProvider, guidProvider); if (adapter == null) return; // Skip on unsupported platforms @@ -486,13 +525,9 @@ public class SandboxCaptureTests } }; - IRuntimeCaptureAdapter? adapter = null; - if (OperatingSystem.IsLinux()) - adapter = new LinuxEbpfCaptureAdapter(); - else if (OperatingSystem.IsWindows()) - adapter = new WindowsEtwCaptureAdapter(); - else if (OperatingSystem.IsMacOS()) - adapter = new MacOsDyldCaptureAdapter(); + var timeProvider = RuntimeCaptureTestFactory.CreateTimeProvider(); + var guidProvider = RuntimeCaptureTestFactory.CreateGuidProvider(); + var adapter = RuntimeCaptureTestFactory.CreatePlatformAdapter(timeProvider, guidProvider); if (adapter == null) return; // Skip on unsupported platforms @@ -527,13 +562,9 @@ public class SandboxCaptureTests } }; - IRuntimeCaptureAdapter? adapter = null; - if (OperatingSystem.IsLinux()) - adapter = new LinuxEbpfCaptureAdapter(); - else if (OperatingSystem.IsWindows()) - adapter = new WindowsEtwCaptureAdapter(); - else if (OperatingSystem.IsMacOS()) - adapter = new MacOsDyldCaptureAdapter(); + var timeProvider = RuntimeCaptureTestFactory.CreateTimeProvider(); + var guidProvider = RuntimeCaptureTestFactory.CreateGuidProvider(); + var adapter = RuntimeCaptureTestFactory.CreatePlatformAdapter(timeProvider, guidProvider); if (adapter == null) return; // Skip on unsupported platforms @@ -558,7 +589,7 @@ public class RuntimeEvidenceModelTests public void RuntimeLoadEvent_RecordEquality_Works() { // Arrange - var time = DateTime.UtcNow; + var time = RuntimeCaptureTestClock.BaseTime; var event1 = new RuntimeLoadEvent(time, 1, 1, RuntimeLoadType.Dlopen, "lib.so", "/lib.so", null, true, null, null, null); var event2 = new RuntimeLoadEvent(time, 1, 1, RuntimeLoadType.Dlopen, "lib.so", "/lib.so", null, true, null, null, null); var event3 = new RuntimeLoadEvent(time, 2, 1, RuntimeLoadType.Dlopen, "lib.so", "/lib.so", null, true, null, null, null); @@ -580,10 +611,54 @@ public class RuntimeEvidenceModelTests { // Verify each type can be used to create an event var evt = new RuntimeLoadEvent( - DateTime.UtcNow, 1, 1, loadType, + RuntimeCaptureTestClock.BaseTime, 1, 1, loadType, "test.so", "/test.so", null, true, null, null, null); evt.LoadType.Should().Be(loadType); } } } + +public class CaptureDurationTimerTests +{ + [Trait("Category", TestCategories.Unit)] + [Fact] + public async Task RunAsync_CallsStopWithProvidedToken() + { + using var captureCts = new CancellationTokenSource(); + using var stopCts = new CancellationTokenSource(); + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + Task StopAsync(CancellationToken token) + { + tcs.TrySetResult(token); + return Task.CompletedTask; + } + + var timerTask = CaptureDurationTimer.RunAsync(TimeSpan.Zero, StopAsync, captureCts.Token, stopCts.Token); + var observedToken = await tcs.Task.WaitAsync(TimeSpan.FromSeconds(1)); + + observedToken.Should().Be(stopCts.Token); + await timerTask; + } + + [Trait("Category", TestCategories.Unit)] + [Fact] + public async Task RunAsync_CanceledCapture_DoesNotInvokeStop() + { + using var captureCts = new CancellationTokenSource(); + using var stopCts = new CancellationTokenSource(); + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + Task StopAsync(CancellationToken token) + { + tcs.TrySetResult(); + return Task.CompletedTask; + } + + captureCts.Cancel(); + await CaptureDurationTimer.RunAsync(TimeSpan.FromMinutes(1), StopAsync, captureCts.Token, stopCts.Token); + + tcs.Task.IsCompleted.Should().BeFalse(); + } +} diff --git a/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Native.Tests/StellaOps.Scanner.Analyzers.Native.Tests.csproj b/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Native.Tests/StellaOps.Scanner.Analyzers.Native.Tests.csproj index 9cfa7deef..f0fe8c443 100644 --- a/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Native.Tests/StellaOps.Scanner.Analyzers.Native.Tests.csproj +++ b/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Native.Tests/StellaOps.Scanner.Analyzers.Native.Tests.csproj @@ -4,7 +4,7 @@ preview enable enable - false + true false @@ -13,11 +13,6 @@ - - - - - @@ -32,4 +27,4 @@ true - \ No newline at end of file + diff --git a/src/Scanner/__Tests/StellaOps.Scanner.CallGraph.Tests/CallGraphDigestsTests.cs b/src/Scanner/__Tests/StellaOps.Scanner.CallGraph.Tests/CallGraphDigestsTests.cs index 9410f9608..76412d367 100644 --- a/src/Scanner/__Tests/StellaOps.Scanner.CallGraph.Tests/CallGraphDigestsTests.cs +++ b/src/Scanner/__Tests/StellaOps.Scanner.CallGraph.Tests/CallGraphDigestsTests.cs @@ -426,6 +426,20 @@ public class CallGraphDigestsTests Assert.Equal("native:SSL_read", stableId); } + [Fact] + public void ComputeResultDigest_IsDeterministic() + { + // Arrange + var result = CreateMinimalResult(); + + // Act + var digest1 = CallGraphDigests.ComputeResultDigest(result); + var digest2 = CallGraphDigests.ComputeResultDigest(result); + + // Assert + Assert.Equal(digest1, digest2); + } + private static CallGraphSnapshot CreateMinimalSnapshot() { return new CallGraphSnapshot( @@ -453,6 +467,24 @@ public class CallGraphDigestsTests ); } + private static ReachabilityAnalysisResult CreateMinimalResult() + { + return new ReachabilityAnalysisResult( + ScanId: "test-scan-001", + GraphDigest: "sha256:graph", + Language: "native", + ComputedAt: DateTimeOffset.UtcNow, + ReachableNodeIds: ImmutableArray.Create("entry", "node-a"), + ReachableSinkIds: ImmutableArray.Create("sink-a"), + Paths: ImmutableArray.Create( + new ReachabilityPath( + EntrypointId: "entry", + SinkId: "sink-a", + NodeIds: ImmutableArray.Create("entry", "node-a", "sink-a"))), + ResultDigest: string.Empty + ); + } + private static bool IsValidHex(string hex) { if (string.IsNullOrEmpty(hex)) diff --git a/src/Scanner/__Tests/StellaOps.Scanner.Reachability.Stack.Tests/ReachabilityStackEvaluatorTests.cs b/src/Scanner/__Tests/StellaOps.Scanner.Reachability.Stack.Tests/ReachabilityStackEvaluatorTests.cs index 424a392b3..924f6bc1f 100644 --- a/src/Scanner/__Tests/StellaOps.Scanner.Reachability.Stack.Tests/ReachabilityStackEvaluatorTests.cs +++ b/src/Scanner/__Tests/StellaOps.Scanner.Reachability.Stack.Tests/ReachabilityStackEvaluatorTests.cs @@ -1,7 +1,9 @@ // SPDX-License-Identifier: AGPL-3.0-or-later // Copyright (c) StellaOps +using System.Globalization; using FluentAssertions; +using StellaOps.Determinism; using StellaOps.Scanner.Explainability.Assumptions; using StellaOps.Scanner.Reachability.Stack; @@ -10,7 +12,7 @@ namespace StellaOps.Scanner.Reachability.Stack.Tests; public class ReachabilityStackEvaluatorTests { - private readonly ReachabilityStackEvaluator _evaluator = new(); + private readonly ReachabilityStackEvaluator _evaluator = new(new SequentialGuidProvider()); private static VulnerableSymbol CreateTestSymbol() => new( Name: "EVP_DecryptUpdate", @@ -172,17 +174,19 @@ public class ReachabilityStackEvaluatorTests var layer1 = CreateLayer1(isReachable: true, ConfidenceLevel.High); var layer2 = CreateLayer2(isResolved: true, ConfidenceLevel.High); var layer3 = CreateLayer3(isGated: false, GatingOutcome.NotGated, ConfidenceLevel.High); + var timeProvider = new FixedTimeProvider(new DateTimeOffset(2026, 1, 12, 9, 0, 0, TimeSpan.Zero)); + var expectedId = new SequentialGuidProvider().NewGuid().ToString("N", CultureInfo.InvariantCulture); - var stack = _evaluator.Evaluate("finding-123", symbol, layer1, layer2, layer3); + var stack = _evaluator.Evaluate("finding-123", symbol, layer1, layer2, layer3, timeProvider); - stack.Id.Should().NotBeNullOrEmpty(); + stack.Id.Should().Be(expectedId); stack.FindingId.Should().Be("finding-123"); stack.Symbol.Should().Be(symbol); stack.StaticCallGraph.Should().Be(layer1); stack.BinaryResolution.Should().Be(layer2); stack.RuntimeGating.Should().Be(layer3); stack.Verdict.Should().Be(ReachabilityVerdict.Exploitable); - stack.AnalyzedAt.Should().BeCloseTo(DateTimeOffset.UtcNow, TimeSpan.FromSeconds(5)); + stack.AnalyzedAt.Should().Be(timeProvider.GetUtcNow()); stack.Explanation.Should().NotBeNullOrEmpty(); } @@ -422,4 +426,16 @@ public class ReachabilityStackEvaluatorTests } #endregion + + private sealed class FixedTimeProvider : TimeProvider + { + private readonly DateTimeOffset _fixedTime; + + public FixedTimeProvider(DateTimeOffset fixedTime) + { + _fixedTime = fixedTime; + } + + public override DateTimeOffset GetUtcNow() => _fixedTime; + } } diff --git a/src/Scanner/__Tests/StellaOps.Scanner.Reachability.Stack.Tests/StellaOps.Scanner.Reachability.Stack.Tests.csproj b/src/Scanner/__Tests/StellaOps.Scanner.Reachability.Stack.Tests/StellaOps.Scanner.Reachability.Stack.Tests.csproj index 3ae4eb385..fc39fdba2 100644 --- a/src/Scanner/__Tests/StellaOps.Scanner.Reachability.Stack.Tests/StellaOps.Scanner.Reachability.Stack.Tests.csproj +++ b/src/Scanner/__Tests/StellaOps.Scanner.Reachability.Stack.Tests/StellaOps.Scanner.Reachability.Stack.Tests.csproj @@ -14,5 +14,6 @@ + - \ No newline at end of file + diff --git a/src/Scanner/__Tests/StellaOps.Scanner.Reachability.Tests/StellaOps.Scanner.Reachability.Tests.csproj b/src/Scanner/__Tests/StellaOps.Scanner.Reachability.Tests/StellaOps.Scanner.Reachability.Tests.csproj index e1481ccef..ea2e2d598 100644 --- a/src/Scanner/__Tests/StellaOps.Scanner.Reachability.Tests/StellaOps.Scanner.Reachability.Tests.csproj +++ b/src/Scanner/__Tests/StellaOps.Scanner.Reachability.Tests/StellaOps.Scanner.Reachability.Tests.csproj @@ -18,6 +18,7 @@ + diff --git a/src/Scanner/__Tests/StellaOps.Scanner.Reachability.Tests/WitnessDsseSignerTests.cs b/src/Scanner/__Tests/StellaOps.Scanner.Reachability.Tests/WitnessDsseSignerTests.cs index 51e460488..212cdc75b 100644 --- a/src/Scanner/__Tests/StellaOps.Scanner.Reachability.Tests/WitnessDsseSignerTests.cs +++ b/src/Scanner/__Tests/StellaOps.Scanner.Reachability.Tests/WitnessDsseSignerTests.cs @@ -1,7 +1,11 @@ +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.Json.Serialization; using Org.BouncyCastle.Crypto.Generators; using Org.BouncyCastle.Crypto.Parameters; using Org.BouncyCastle.Security; using StellaOps.Attestor.Envelope; +using StellaOps.Canonical.Json; using StellaOps.Scanner.Reachability.Witnesses; using Xunit; @@ -137,6 +141,49 @@ public class WitnessDsseSignerTests Assert.Equal(result1.PayloadBytes, result2.PayloadBytes); } + [Trait("Category", TestCategories.Unit)] + [Fact] + public void SignWitness_UsesCanonicalPayloadAndDssePae() + { + // Arrange + var witness = CreateTestWitness(); + var (privateKey, publicKey) = CreateTestKeyPair(); + var signingKey = EnvelopeKey.CreateEd25519Signer(privateKey, publicKey); + var signer = new WitnessDsseSigner(new EnvelopeSignatureService()); + + var options = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, + WriteIndented = false, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + Encoder = JavaScriptEncoder.Default + }; + + // Act + var signResult = signer.SignWitness(witness, signingKey, TestCancellationToken); + + // Assert + Assert.True(signResult.IsSuccess, signResult.Error); + Assert.NotNull(signResult.Envelope); + Assert.NotNull(signResult.PayloadBytes); + + var payloadBytes = signResult.PayloadBytes!; + var expectedPayload = CanonJson.Canonicalize(witness, options); + Assert.Equal(expectedPayload, payloadBytes); + + var verifyKey = EnvelopeKey.CreateEd25519Verifier(publicKey); + var signatureBytes = Convert.FromBase64String(signResult.Envelope!.Signatures[0].Signature); + var envelopeSignature = new EnvelopeSignature(signingKey.KeyId, signingKey.AlgorithmId, signatureBytes); + var verifyResult = new EnvelopeSignatureService().VerifyDsse( + WitnessSchema.DssePayloadType, + payloadBytes, + envelopeSignature, + verifyKey, + TestCancellationToken); + + Assert.True(verifyResult.IsSuccess); + } + [Trait("Category", TestCategories.Unit)] [Fact] public void VerifyWitness_WithInvalidPayloadType_ReturnsFails() diff --git a/src/Scanner/__Tests/StellaOps.Scanner.Reachability.Tests/Witnesses/SuppressionDsseSignerTests.cs b/src/Scanner/__Tests/StellaOps.Scanner.Reachability.Tests/Witnesses/SuppressionDsseSignerTests.cs index 59416382c..1c09ae4a6 100644 --- a/src/Scanner/__Tests/StellaOps.Scanner.Reachability.Tests/Witnesses/SuppressionDsseSignerTests.cs +++ b/src/Scanner/__Tests/StellaOps.Scanner.Reachability.Tests/Witnesses/SuppressionDsseSignerTests.cs @@ -1,7 +1,11 @@ +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.Json.Serialization; using Org.BouncyCastle.Crypto.Generators; using Org.BouncyCastle.Crypto.Parameters; using Org.BouncyCastle.Security; using StellaOps.Attestor.Envelope; +using StellaOps.Canonical.Json; using StellaOps.Scanner.Reachability.Witnesses; using StellaOps.TestKit; using Xunit; @@ -286,6 +290,49 @@ public sealed class SuppressionDsseSignerTests verifyResult.Witness.Evidence.Unreachability?.UnreachableSymbol); } + [Trait("Category", TestCategories.Unit)] + [Fact] + public void SignWitness_UsesCanonicalPayloadAndDssePae() + { + // Arrange + var witness = CreateTestWitness(); + var (privateKey, publicKey) = CreateTestKeyPair(); + var signingKey = EnvelopeKey.CreateEd25519Signer(privateKey, publicKey); + var signer = new SuppressionDsseSigner(new EnvelopeSignatureService()); + + var options = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, + WriteIndented = false, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + Encoder = JavaScriptEncoder.Default + }; + + // Act + var signResult = signer.SignWitness(witness, signingKey, TestCancellationToken); + + // Assert + Assert.True(signResult.IsSuccess, signResult.Error); + Assert.NotNull(signResult.Envelope); + Assert.NotNull(signResult.PayloadBytes); + + var payloadBytes = signResult.PayloadBytes!; + var expectedPayload = CanonJson.Canonicalize(witness, options); + Assert.Equal(expectedPayload, payloadBytes); + + var verifyKey = EnvelopeKey.CreateEd25519Verifier(publicKey); + var signatureBytes = Convert.FromBase64String(signResult.Envelope!.Signatures[0].Signature); + var envelopeSignature = new EnvelopeSignature(signingKey.KeyId, signingKey.AlgorithmId, signatureBytes); + var verifyResult = new EnvelopeSignatureService().VerifyDsse( + SuppressionWitnessSchema.DssePayloadType, + payloadBytes, + envelopeSignature, + verifyKey, + TestCancellationToken); + + Assert.True(verifyResult.IsSuccess); + } + private sealed class FixedRandomGenerator : Org.BouncyCastle.Crypto.Prng.IRandomGenerator { private byte _value; diff --git a/src/Scanner/__Tests/StellaOps.Scanner.Sbomer.BuildXPlugin.Tests/Attestation/AttestorClientTests.cs b/src/Scanner/__Tests/StellaOps.Scanner.Sbomer.BuildXPlugin.Tests/Attestation/AttestorClientTests.cs index 8f75eca2d..d4628ec6a 100644 --- a/src/Scanner/__Tests/StellaOps.Scanner.Sbomer.BuildXPlugin.Tests/Attestation/AttestorClientTests.cs +++ b/src/Scanner/__Tests/StellaOps.Scanner.Sbomer.BuildXPlugin.Tests/Attestation/AttestorClientTests.cs @@ -4,6 +4,7 @@ using System.Net.Http; using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; using StellaOps.Scanner.Sbomer.BuildXPlugin; using StellaOps.Scanner.Sbomer.BuildXPlugin.Attestation; using StellaOps.Scanner.Sbomer.BuildXPlugin.Descriptor; @@ -17,7 +18,9 @@ public sealed class AttestorClientTests public async Task SendPlaceholderAsync_PostsJsonPayload() { var handler = new RecordingHandler(new HttpResponseMessage(HttpStatusCode.Accepted)); - using var httpClient = new HttpClient(handler); + using var provider = BuildHttpClientProvider(handler); + var factory = provider.GetRequiredService(); + using var httpClient = factory.CreateClient("attestor"); var client = new AttestorClient(httpClient); var document = BuildDescriptorDocument(); @@ -43,7 +46,9 @@ public sealed class AttestorClientTests { Content = new StringContent("invalid") }); - using var httpClient = new HttpClient(handler); + using var provider = BuildHttpClientProvider(handler); + var factory = provider.GetRequiredService(); + using var httpClient = factory.CreateClient("attestor"); var client = new AttestorClient(httpClient); var document = BuildDescriptorDocument(); @@ -54,12 +59,21 @@ public sealed class AttestorClientTests private static DescriptorDocument BuildDescriptorDocument() { + var createdAt = DateTimeOffset.Parse("2026-01-13T00:00:00Z"); var subject = new DescriptorSubject("application/vnd.oci.image.manifest.v1+json", "sha256:img"); var artifact = new DescriptorArtifact("application/vnd.cyclonedx+json; version=1.7", "sha256:sbom", 42, new System.Collections.Generic.Dictionary()); var provenance = new DescriptorProvenance("pending", "sha256:dsse", "nonce", "https://attestor.example.com/api/v1/provenance", "https://slsa.dev/provenance/v1"); var generatorMetadata = new DescriptorGeneratorMetadata("generator", "1.0.0"); var metadata = new System.Collections.Generic.Dictionary(); - return new DescriptorDocument("schema", DateTimeOffset.UtcNow, generatorMetadata, subject, artifact, provenance, metadata); + return new DescriptorDocument("schema", createdAt, generatorMetadata, subject, artifact, provenance, metadata); + } + + private static ServiceProvider BuildHttpClientProvider(HttpMessageHandler handler) + { + var services = new ServiceCollection(); + services.AddHttpClient("attestor") + .ConfigurePrimaryHttpMessageHandler(() => handler); + return services.BuildServiceProvider(); } private sealed class RecordingHandler : HttpMessageHandler diff --git a/src/Scanner/__Tests/StellaOps.Scanner.Sbomer.BuildXPlugin.Tests/Descriptor/DescriptorCommandSurfaceTests.cs b/src/Scanner/__Tests/StellaOps.Scanner.Sbomer.BuildXPlugin.Tests/Descriptor/DescriptorCommandSurfaceTests.cs index 2bdc42d37..e328aa650 100644 --- a/src/Scanner/__Tests/StellaOps.Scanner.Sbomer.BuildXPlugin.Tests/Descriptor/DescriptorCommandSurfaceTests.cs +++ b/src/Scanner/__Tests/StellaOps.Scanner.Sbomer.BuildXPlugin.Tests/Descriptor/DescriptorCommandSurfaceTests.cs @@ -74,6 +74,15 @@ public sealed class DescriptorCommandSurfaceTests throw new FileNotFoundException($"BuildX plug-in assembly not found at '{pluginAssembly}'."); } + EnsurePathWithinRoot(actualRepoRoot, pluginAssembly, "Plug-in assembly"); + EnsurePathWithinRoot(actualRepoRoot, manifestDirectory, "Manifest directory"); + EnsurePathWithinRoot(temp.Path, casRoot, "CAS root"); + EnsurePathWithinRoot(temp.Path, sbomPath, "SBOM path"); + EnsurePathWithinRoot(temp.Path, layerFragmentsPath, "Layer fragments path"); + EnsurePathWithinRoot(temp.Path, entryTraceGraphPath, "Entry trace graph path"); + EnsurePathWithinRoot(temp.Path, entryTraceNdjsonPath, "Entry trace NDJSON path"); + EnsurePathWithinRoot(temp.Path, manifestOutputPath, "Manifest output path"); + var psi = new ProcessStartInfo("dotnet") { RedirectStandardOutput = true, @@ -107,10 +116,11 @@ public sealed class DescriptorCommandSurfaceTests psi.ArgumentList.Add("--surface-manifest-output"); psi.ArgumentList.Add(manifestOutputPath); + ValidateProcessStartInfo(psi); var process = Process.Start(psi) ?? throw new InvalidOperationException("Failed to start BuildX plug-in process."); var stdout = await process.StandardOutput.ReadToEndAsync(); var stderr = await process.StandardError.ReadToEndAsync(); - await process.WaitForExitAsync(); + await process.WaitForExitAsync(TestContext.Current.CancellationToken); Assert.True(process.ExitCode == 0, $"Descriptor command failed.\nSTDOUT: {stdout}\nSTDERR: {stderr}"); @@ -163,4 +173,33 @@ public sealed class DescriptorCommandSurfaceTests var digest = hash.ComputeHash(bytes, HashAlgorithms.Sha256); return $"sha256:{Convert.ToHexString(digest).ToLowerInvariant()}"; } + + private static void ValidateProcessStartInfo(ProcessStartInfo psi) + { + if (!string.Equals(System.IO.Path.GetFileName(psi.FileName), "dotnet", StringComparison.OrdinalIgnoreCase)) + { + throw new InvalidOperationException("Only dotnet execution is permitted for plug-in tests."); + } + + foreach (var argument in psi.ArgumentList) + { + if (argument.Contains('\n') || argument.Contains('\r')) + { + throw new InvalidOperationException("Process arguments must not contain newline characters."); + } + } + } + + private static void EnsurePathWithinRoot(string root, string candidatePath, string label) + { + var normalizedRoot = System.IO.Path.GetFullPath(root) + .TrimEnd(System.IO.Path.DirectorySeparatorChar, System.IO.Path.AltDirectorySeparatorChar) + + System.IO.Path.DirectorySeparatorChar; + var normalizedPath = System.IO.Path.GetFullPath(candidatePath); + + if (!normalizedPath.StartsWith(normalizedRoot, StringComparison.OrdinalIgnoreCase)) + { + throw new InvalidOperationException($"{label} must stay under '{normalizedRoot}'."); + } + } } diff --git a/src/Scanner/__Tests/StellaOps.Scanner.Sbomer.BuildXPlugin.Tests/Surface/SurfaceManifestWriterTests.cs b/src/Scanner/__Tests/StellaOps.Scanner.Sbomer.BuildXPlugin.Tests/Surface/SurfaceManifestWriterTests.cs index a75c83109..d6f55fd9e 100644 --- a/src/Scanner/__Tests/StellaOps.Scanner.Sbomer.BuildXPlugin.Tests/Surface/SurfaceManifestWriterTests.cs +++ b/src/Scanner/__Tests/StellaOps.Scanner.Sbomer.BuildXPlugin.Tests/Surface/SurfaceManifestWriterTests.cs @@ -1,8 +1,12 @@ using System; using System.IO; using System.Linq; +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; +using StellaOps.Canonical.Json; using StellaOps.Cryptography; using StellaOps.Scanner.Sbomer.BuildXPlugin.Surface; using StellaOps.Scanner.Sbomer.BuildXPlugin.Tests.TestUtilities; @@ -43,7 +47,8 @@ public sealed class SurfaceManifestWriterTests EntryTraceNdjsonPath: ndjsonPath, ManifestOutputPath: manifestOutputPath); - var writer = new SurfaceManifestWriter(TimeProvider.System, CryptoHashFactory.CreateDefault()); + var timeProvider = new FixedTimeProvider(new DateTimeOffset(2026, 1, 13, 0, 0, 0, TimeSpan.Zero)); + var writer = new SurfaceManifestWriter(timeProvider, CryptoHashFactory.CreateDefault()); var result = await writer.WriteAsync(options, TestContext.Current.CancellationToken); Assert.NotNull(result); @@ -67,6 +72,10 @@ public sealed class SurfaceManifestWriterTests Assert.False(string.IsNullOrWhiteSpace(artifact.ManifestArtifact.Uri)); Assert.StartsWith("cas://scanner-artifacts/", artifact.ManifestArtifact.Uri, StringComparison.Ordinal); } + + var canonicalBytes = CanonJson.Canonicalize(result.Document, CreateManifestJsonOptions()); + var manifestBytes = await File.ReadAllBytesAsync(result.ManifestPath, TestContext.Current.CancellationToken); + Assert.Equal(canonicalBytes, manifestBytes); } [Fact] @@ -89,8 +98,60 @@ public sealed class SurfaceManifestWriterTests EntryTraceNdjsonPath: null, ManifestOutputPath: null); - var writer = new SurfaceManifestWriter(TimeProvider.System, CryptoHashFactory.CreateDefault()); + var timeProvider = new FixedTimeProvider(new DateTimeOffset(2026, 1, 13, 0, 0, 0, TimeSpan.Zero)); + var writer = new SurfaceManifestWriter(timeProvider, CryptoHashFactory.CreateDefault()); var result = await writer.WriteAsync(options, TestContext.Current.CancellationToken); Assert.Null(result); } + + [Fact] + public async Task WriteAsync_DefaultsWorkerInstanceToComponent() + { + await using var temp = new TempDirectory(); + var fragmentsPath = Path.Combine(temp.Path, "layer-fragments.json"); + await File.WriteAllTextAsync(fragmentsPath, "[]"); + + var options = new SurfaceOptions( + CacheRoot: temp.Path, + CacheBucket: "scanner-artifacts", + RootPrefix: "scanner", + Tenant: "tenant-a", + Component: "scanner.buildx", + ComponentVersion: "1.2.3", + WorkerInstance: "", + Attempt: 1, + ImageDigest: "sha256:feedface", + ScanId: "scan-123", + LayerFragmentsPath: fragmentsPath, + EntryTraceGraphPath: null, + EntryTraceNdjsonPath: null, + ManifestOutputPath: null); + + var timeProvider = new FixedTimeProvider(new DateTimeOffset(2026, 1, 13, 0, 0, 0, TimeSpan.Zero)); + var writer = new SurfaceManifestWriter(timeProvider, CryptoHashFactory.CreateDefault()); + var result = await writer.WriteAsync(options, TestContext.Current.CancellationToken); + + Assert.NotNull(result); + Assert.Equal("scanner.buildx", result!.Document.Source?.WorkerInstance); + } + + private static JsonSerializerOptions CreateManifestJsonOptions() + => new(JsonSerializerDefaults.Web) + { + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + WriteIndented = false, + Encoder = JavaScriptEncoder.Default + }; + + private sealed class FixedTimeProvider : TimeProvider + { + private readonly DateTimeOffset _utcNow; + + public FixedTimeProvider(DateTimeOffset utcNow) + { + _utcNow = utcNow; + } + + public override DateTimeOffset GetUtcNow() => _utcNow; + } } diff --git a/src/Scanner/__Tests/StellaOps.Scanner.Sbomer.BuildXPlugin.Tests/TestUtilities/TempDirectory.cs b/src/Scanner/__Tests/StellaOps.Scanner.Sbomer.BuildXPlugin.Tests/TestUtilities/TempDirectory.cs index f91445f16..0a77854b1 100644 --- a/src/Scanner/__Tests/StellaOps.Scanner.Sbomer.BuildXPlugin.Tests/TestUtilities/TempDirectory.cs +++ b/src/Scanner/__Tests/StellaOps.Scanner.Sbomer.BuildXPlugin.Tests/TestUtilities/TempDirectory.cs @@ -1,16 +1,20 @@ using System; using System.IO; +using System.Threading; using System.Threading.Tasks; namespace StellaOps.Scanner.Sbomer.BuildXPlugin.Tests.TestUtilities; internal sealed class TempDirectory : IDisposable, IAsyncDisposable { + private static int _sequence; + public string Path { get; } public TempDirectory() { - Path = System.IO.Path.Combine(System.IO.Path.GetTempPath(), $"stellaops-buildx-{Guid.NewGuid():N}"); + var suffix = Interlocked.Increment(ref _sequence); + Path = System.IO.Path.Combine(System.IO.Path.GetTempPath(), $"stellaops-buildx-{suffix:D4}"); Directory.CreateDirectory(Path); } diff --git a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/ActionablesEndpointsTests.cs b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/ActionablesEndpointsTests.cs index 1d0ef23bf..afd12df67 100644 --- a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/ActionablesEndpointsTests.cs +++ b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/ActionablesEndpointsTests.cs @@ -24,7 +24,8 @@ public sealed class ActionablesEndpointsTests [Fact] public async Task GetDeltaActionables_ValidDeltaId_ReturnsActionables() { - using var factory = new ScannerApplicationFactory(); + await using var factory = new ScannerApplicationFactory(); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var response = await client.GetAsync("/api/v1/actionables/delta/delta-12345678"); @@ -40,7 +41,8 @@ public sealed class ActionablesEndpointsTests [Fact] public async Task GetDeltaActionables_SortedByPriority() { - using var factory = new ScannerApplicationFactory(); + await using var factory = new ScannerApplicationFactory(); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var response = await client.GetAsync("/api/v1/actionables/delta/delta-12345678"); @@ -58,7 +60,8 @@ public sealed class ActionablesEndpointsTests [Fact] public async Task GetActionablesByPriority_Critical_FiltersCorrectly() { - using var factory = new ScannerApplicationFactory(); + await using var factory = new ScannerApplicationFactory(); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var response = await client.GetAsync("/api/v1/actionables/delta/delta-12345678/by-priority/critical"); @@ -73,7 +76,8 @@ public sealed class ActionablesEndpointsTests [Fact] public async Task GetActionablesByPriority_InvalidPriority_ReturnsBadRequest() { - using var factory = new ScannerApplicationFactory(); + await using var factory = new ScannerApplicationFactory(); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var response = await client.GetAsync("/api/v1/actionables/delta/delta-12345678/by-priority/invalid"); @@ -84,7 +88,8 @@ public sealed class ActionablesEndpointsTests [Fact] public async Task GetActionablesByType_Upgrade_FiltersCorrectly() { - using var factory = new ScannerApplicationFactory(); + await using var factory = new ScannerApplicationFactory(); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var response = await client.GetAsync("/api/v1/actionables/delta/delta-12345678/by-type/upgrade"); @@ -99,7 +104,8 @@ public sealed class ActionablesEndpointsTests [Fact] public async Task GetActionablesByType_Vex_FiltersCorrectly() { - using var factory = new ScannerApplicationFactory(); + await using var factory = new ScannerApplicationFactory(); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var response = await client.GetAsync("/api/v1/actionables/delta/delta-12345678/by-type/vex"); @@ -114,7 +120,8 @@ public sealed class ActionablesEndpointsTests [Fact] public async Task GetActionablesByType_InvalidType_ReturnsBadRequest() { - using var factory = new ScannerApplicationFactory(); + await using var factory = new ScannerApplicationFactory(); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var response = await client.GetAsync("/api/v1/actionables/delta/delta-12345678/by-type/invalid"); @@ -125,7 +132,8 @@ public sealed class ActionablesEndpointsTests [Fact] public async Task GetDeltaActionables_IncludesEstimatedEffort() { - using var factory = new ScannerApplicationFactory(); + await using var factory = new ScannerApplicationFactory(); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var response = await client.GetAsync("/api/v1/actionables/delta/delta-12345678"); diff --git a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/ApprovalEndpointsTests.cs b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/ApprovalEndpointsTests.cs index 3cf1684be..9b20207bc 100644 --- a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/ApprovalEndpointsTests.cs +++ b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/ApprovalEndpointsTests.cs @@ -18,27 +18,27 @@ namespace StellaOps.Scanner.WebService.Tests; [Trait("Category", "Integration")] [Trait("Sprint", "3801.0001")] -public sealed class ApprovalEndpointsTests : IDisposable +public sealed class ApprovalEndpointsTests : IAsyncLifetime { - private readonly TestSurfaceSecretsScope _secrets; - private readonly ScannerApplicationFactory _factory; - private readonly HttpClient _client; + private TestSurfaceSecretsScope _secrets = null!; + private ScannerApplicationFactory _factory = null!; + private HttpClient _client = null!; - public ApprovalEndpointsTests() + public async Task InitializeAsync() { _secrets = new TestSurfaceSecretsScope(); // Use default factory without auth overrides - same pattern as ManifestEndpointsTests // The factory defaults to anonymous auth which allows all policy assertions _factory = new ScannerApplicationFactory(); - + await _factory.InitializeAsync(); _client = _factory.CreateClient(); } - public void Dispose() + public async Task DisposeAsync() { _client.Dispose(); - _factory.Dispose(); + await _factory.DisposeAsync(); _secrets.Dispose(); } diff --git a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/AuthorizationTests.cs b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/AuthorizationTests.cs index 8f313b766..71e36cf85 100644 --- a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/AuthorizationTests.cs +++ b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/AuthorizationTests.cs @@ -10,7 +10,7 @@ public sealed class AuthorizationTests [Fact] public async Task ApiRoutesRequireAuthenticationWhenAuthorityEnabled() { - using var factory = new ScannerApplicationFactory().WithOverrides(configuration => + await using var factory = new ScannerApplicationFactory().WithOverrides(configuration => { configuration["scanner:authority:enabled"] = "true"; configuration["scanner:authority:allowAnonymousFallback"] = "false"; @@ -19,6 +19,7 @@ public sealed class AuthorizationTests configuration["scanner:authority:clientId"] = "scanner-web"; configuration["scanner:authority:clientSecret"] = "secret"; }); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var response = await client.GetAsync("/api/v1/__auth-probe"); diff --git a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/BaselineEndpointsTests.cs b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/BaselineEndpointsTests.cs index aec5f75dc..3b8288ea5 100644 --- a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/BaselineEndpointsTests.cs +++ b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/BaselineEndpointsTests.cs @@ -25,7 +25,8 @@ public sealed class BaselineEndpointsTests [Fact] public async Task GetRecommendations_ValidDigest_ReturnsRecommendations() { - using var factory = new ScannerApplicationFactory(); + await using var factory = new ScannerApplicationFactory(); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var response = await client.GetAsync("/api/v1/baselines/recommendations/sha256:artifact123", TestContext.Current.CancellationToken); @@ -42,7 +43,8 @@ public sealed class BaselineEndpointsTests [Fact] public async Task GetRecommendations_WithEnvironment_FiltersCorrectly() { - using var factory = new ScannerApplicationFactory(); + await using var factory = new ScannerApplicationFactory(); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var response = await client.GetAsync("/api/v1/baselines/recommendations/sha256:artifact123?environment=production", TestContext.Current.CancellationToken); @@ -57,7 +59,8 @@ public sealed class BaselineEndpointsTests [Fact] public async Task GetRecommendations_IncludesRationale() { - using var factory = new ScannerApplicationFactory(); + await using var factory = new ScannerApplicationFactory(); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var response = await client.GetAsync("/api/v1/baselines/recommendations/sha256:artifact123", TestContext.Current.CancellationToken); @@ -76,7 +79,8 @@ public sealed class BaselineEndpointsTests [Fact] public async Task GetRationale_ValidDigests_ReturnsDetailedRationale() { - using var factory = new ScannerApplicationFactory(); + await using var factory = new ScannerApplicationFactory(); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var response = await client.GetAsync("/api/v1/baselines/rationale/sha256:base123/sha256:head456"); @@ -95,7 +99,8 @@ public sealed class BaselineEndpointsTests [Fact] public async Task GetRationale_IncludesSelectionCriteria() { - using var factory = new ScannerApplicationFactory(); + await using var factory = new ScannerApplicationFactory(); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var response = await client.GetAsync("/api/v1/baselines/rationale/sha256:baseline-base123/sha256:head456"); @@ -110,7 +115,8 @@ public sealed class BaselineEndpointsTests [Fact] public async Task GetRecommendations_DefaultIsFirst() { - using var factory = new ScannerApplicationFactory(); + await using var factory = new ScannerApplicationFactory(); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var response = await client.GetAsync("/api/v1/baselines/recommendations/sha256:artifact123", TestContext.Current.CancellationToken); diff --git a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/Benchmarks/TtfsPerformanceBenchmarks.cs b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/Benchmarks/TtfsPerformanceBenchmarks.cs index f7aaf4128..742453a99 100644 --- a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/Benchmarks/TtfsPerformanceBenchmarks.cs +++ b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/Benchmarks/TtfsPerformanceBenchmarks.cs @@ -3,6 +3,8 @@ // Task: TRI-MASTER-0007 - Performance benchmark suite (TTFS) using System.Diagnostics; +using System.Security.Cryptography; +using System.Text; using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Columns; using BenchmarkDotNet.Configs; @@ -18,10 +20,10 @@ namespace StellaOps.Scanner.WebService.Tests.Benchmarks; /// TTFS (Time-To-First-Signal) performance benchmarks for triage workflows. /// Measures the latency from request initiation to first meaningful evidence display. /// -/// Target KPIs (from Triage Advisory §3): +/// Target KPIs (from Triage Advisory section 3): /// - TTFS p95 < 1.5s (with 100ms RTT, 1% loss) /// - Clicks-to-Closure median < 6 clicks -/// - Evidence Completeness ≥ 90% +/// - Evidence Completeness >= 90% /// [Config(typeof(TtfsBenchmarkConfig))] [MemoryDiagnoser] @@ -149,7 +151,7 @@ public sealed class TtfsPerformanceTests { // Arrange var cache = new MockEvidenceCache(); - var alertId = Guid.NewGuid().ToString(); + const string alertId = "alert-test-0001"; // Act var sw = Stopwatch.StartNew(); @@ -189,7 +191,7 @@ public sealed class TtfsPerformanceTests { // Arrange var cache = new MockEvidenceCache(); - var alertId = Guid.NewGuid().ToString(); + const string alertId = "alert-test-0002"; var evidence = cache.GetEvidence(alertId); // Act @@ -235,7 +237,7 @@ public sealed class TtfsPerformanceTests { // Arrange var cache = new MockEvidenceCache(); - var alertId = Guid.NewGuid().ToString(); + const string alertId = "alert-test-0003"; // Act var evidence = cache.GetEvidence(alertId); @@ -290,11 +292,11 @@ public sealed class MockAlertDataStore _alerts = Enumerable.Range(0, alertCount) .Select(i => new Alert { - Id = Guid.NewGuid().ToString(), + Id = $"alert-{i:D6}", CveId = $"CVE-2024-{10000 + i}", Severity = _random.Next(0, 4) switch { 0 => "LOW", 1 => "MEDIUM", 2 => "HIGH", _ => "CRITICAL" }, Status = "open", - CreatedAt = DateTime.UtcNow.AddDays(-_random.Next(1, 30)) + CreatedAt = TtfsTestClock.FixedUtc.AddDays(-_random.Next(1, 30)) }) .ToList(); } @@ -303,9 +305,6 @@ public sealed class MockAlertDataStore public AlertListResult GetAlerts(int page, int pageSize) { - // Simulate DB query latency - Thread.Sleep(5); - var skip = (page - 1) * pageSize; return new AlertListResult { @@ -318,14 +317,12 @@ public sealed class MockAlertDataStore public Alert GetAlert(string id) { - Thread.Sleep(2); return _alerts.First(a => a.Id == id); } public DecisionResult RecordDecision(string alertId, DecisionRequest request) { - Thread.Sleep(3); - return new DecisionResult { Success = true, DecisionId = Guid.NewGuid().ToString() }; + return new DecisionResult { Success = true, DecisionId = $"decision-{alertId}" }; } } @@ -333,9 +330,6 @@ public sealed class MockEvidenceCache { public EvidenceBundle GetEvidence(string alertId) { - // Simulate evidence retrieval latency - Thread.Sleep(10); - return new EvidenceBundle { AlertId = alertId, @@ -357,7 +351,7 @@ public sealed class MockEvidenceCache VexStatus = new VexStatusEvidence { Status = "under_investigation", - LastUpdated = DateTime.UtcNow.AddDays(-2) + LastUpdated = TtfsTestClock.FixedUtc.AddDays(-2) }, GraphRevision = new GraphRevisionEvidence { @@ -374,16 +368,23 @@ public static class ReplayTokenGenerator public static ReplayToken Generate(string alertId, EvidenceBundle evidence) { // Simulate token generation - var hash = $"{alertId}:{evidence.Reachability?.Tier}:{evidence.VexStatus?.Status}".GetHashCode(); + var input = $"{alertId}:{evidence.Reachability?.Tier}:{evidence.VexStatus?.Status}"; + var hash = SHA256.HashData(Encoding.UTF8.GetBytes(input)); + var hex = Convert.ToHexString(hash).ToLowerInvariant(); return new ReplayToken { - Token = $"replay_{Math.Abs(hash):x8}", + Token = $"replay_{hex[..8]}", AlertId = alertId, - GeneratedAt = DateTime.UtcNow + GeneratedAt = TtfsTestClock.FixedUtc }; } } +internal static class TtfsTestClock +{ + public static readonly DateTime FixedUtc = new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc); +} + #endregion #region Models diff --git a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/CallGraphEndpointsTests.cs b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/CallGraphEndpointsTests.cs index dd083f928..0d876f936 100644 --- a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/CallGraphEndpointsTests.cs +++ b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/CallGraphEndpointsTests.cs @@ -14,10 +14,11 @@ public sealed class CallGraphEndpointsTests public async Task SubmitCallGraphRequiresContentDigestHeader() { using var secrets = new TestSurfaceSecretsScope(); - using var factory = new ScannerApplicationFactory().WithOverrides(configuration => + await using var factory = new ScannerApplicationFactory().WithOverrides(configuration => { configuration["scanner:authority:enabled"] = "false"; }); + await factory.InitializeAsync(); using var client = factory.CreateClient(); @@ -34,10 +35,11 @@ public sealed class CallGraphEndpointsTests public async Task SubmitCallGraphReturnsAcceptedAndDetectsDuplicates() { using var secrets = new TestSurfaceSecretsScope(); - using var factory = new ScannerApplicationFactory().WithOverrides(configuration => + await using var factory = new ScannerApplicationFactory().WithOverrides(configuration => { configuration["scanner:authority:enabled"] = "false"; }); + await factory.InitializeAsync(); using var client = factory.CreateClient(); diff --git a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/Contract/ScannerOpenApiContractTests.cs b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/Contract/ScannerOpenApiContractTests.cs index 2c7764f6c..715adec38 100644 --- a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/Contract/ScannerOpenApiContractTests.cs +++ b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/Contract/ScannerOpenApiContractTests.cs @@ -9,6 +9,7 @@ using FluentAssertions; using StellaOps.TestKit; using StellaOps.TestKit.Fixtures; using Xunit; +using Xunit.Abstractions; namespace StellaOps.Scanner.WebService.Tests.Contract; @@ -22,10 +23,12 @@ public sealed class ScannerOpenApiContractTests : IClassFixture 0) { - Console.WriteLine("Non-breaking API changes detected:"); + _output.WriteLine("Non-breaking API changes detected:"); foreach (var change in changes.NonBreakingChanges) { - Console.WriteLine($" + {change}"); + _output.WriteLine($" + {change}"); } } } diff --git a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/CounterfactualEndpointsTests.cs b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/CounterfactualEndpointsTests.cs index d28ebac31..404d45205 100644 --- a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/CounterfactualEndpointsTests.cs +++ b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/CounterfactualEndpointsTests.cs @@ -25,7 +25,8 @@ public sealed class CounterfactualEndpointsTests [Fact] public async Task PostCompute_ValidRequest_ReturnsCounterfactuals() { - using var factory = new ScannerApplicationFactory(); + await using var factory = new ScannerApplicationFactory(); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var request = new CounterfactualRequestDto @@ -52,7 +53,8 @@ public sealed class CounterfactualEndpointsTests [Fact] public async Task PostCompute_MissingFindingId_ReturnsBadRequest() { - using var factory = new ScannerApplicationFactory(); + await using var factory = new ScannerApplicationFactory(); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var request = new CounterfactualRequestDto @@ -69,7 +71,8 @@ public sealed class CounterfactualEndpointsTests [Fact] public async Task PostCompute_IncludesVexPath() { - using var factory = new ScannerApplicationFactory(); + await using var factory = new ScannerApplicationFactory(); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var request = new CounterfactualRequestDto @@ -90,7 +93,8 @@ public sealed class CounterfactualEndpointsTests [Fact] public async Task PostCompute_IncludesReachabilityPath() { - using var factory = new ScannerApplicationFactory(); + await using var factory = new ScannerApplicationFactory(); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var request = new CounterfactualRequestDto @@ -111,7 +115,8 @@ public sealed class CounterfactualEndpointsTests [Fact] public async Task PostCompute_IncludesExceptionPath() { - using var factory = new ScannerApplicationFactory(); + await using var factory = new ScannerApplicationFactory(); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var request = new CounterfactualRequestDto @@ -132,7 +137,8 @@ public sealed class CounterfactualEndpointsTests [Fact] public async Task PostCompute_WithMaxPaths_LimitsResults() { - using var factory = new ScannerApplicationFactory(); + await using var factory = new ScannerApplicationFactory(); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var request = new CounterfactualRequestDto @@ -154,7 +160,8 @@ public sealed class CounterfactualEndpointsTests [Fact] public async Task GetForFinding_ValidId_ReturnsCounterfactuals() { - using var factory = new ScannerApplicationFactory(); + await using var factory = new ScannerApplicationFactory(); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var response = await client.GetAsync("/api/v1/counterfactuals/finding/finding-123"); @@ -169,7 +176,8 @@ public sealed class CounterfactualEndpointsTests [Fact] public async Task GetScanSummary_ValidId_ReturnsSummary() { - using var factory = new ScannerApplicationFactory(); + await using var factory = new ScannerApplicationFactory(); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var response = await client.GetAsync("/api/v1/counterfactuals/scan/scan-123/summary"); @@ -185,7 +193,8 @@ public sealed class CounterfactualEndpointsTests [Fact] public async Task GetScanSummary_IncludesPathCounts() { - using var factory = new ScannerApplicationFactory(); + await using var factory = new ScannerApplicationFactory(); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var response = await client.GetAsync("/api/v1/counterfactuals/scan/scan-123/summary"); @@ -203,7 +212,8 @@ public sealed class CounterfactualEndpointsTests [Fact] public async Task PostCompute_PathsHaveConditions() { - using var factory = new ScannerApplicationFactory(); + await using var factory = new ScannerApplicationFactory(); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var request = new CounterfactualRequestDto diff --git a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/DeltaCompareEndpointsTests.cs b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/DeltaCompareEndpointsTests.cs index 892fe5b5f..2fc458c5d 100644 --- a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/DeltaCompareEndpointsTests.cs +++ b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/DeltaCompareEndpointsTests.cs @@ -25,7 +25,8 @@ public sealed class DeltaCompareEndpointsTests [Fact] public async Task PostCompare_ValidRequest_ReturnsComparisonResult() { - using var factory = new ScannerApplicationFactory(); + await using var factory = new ScannerApplicationFactory(); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var request = new DeltaCompareRequestDto @@ -54,7 +55,8 @@ public sealed class DeltaCompareEndpointsTests [Fact] public async Task PostCompare_MissingBaseDigest_ReturnsBadRequest() { - using var factory = new ScannerApplicationFactory(); + await using var factory = new ScannerApplicationFactory(); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var request = new DeltaCompareRequestDto @@ -71,7 +73,8 @@ public sealed class DeltaCompareEndpointsTests [Fact] public async Task PostCompare_MissingTargetDigest_ReturnsBadRequest() { - using var factory = new ScannerApplicationFactory(); + await using var factory = new ScannerApplicationFactory(); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var request = new DeltaCompareRequestDto @@ -88,7 +91,8 @@ public sealed class DeltaCompareEndpointsTests [Fact] public async Task GetQuickDiff_ValidDigests_ReturnsQuickSummary() { - using var factory = new ScannerApplicationFactory(); + await using var factory = new ScannerApplicationFactory(); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var response = await client.GetAsync("/api/v1/delta/quick?baseDigest=sha256:base123&targetDigest=sha256:target456"); @@ -106,7 +110,8 @@ public sealed class DeltaCompareEndpointsTests [Fact] public async Task GetQuickDiff_MissingDigest_ReturnsBadRequest() { - using var factory = new ScannerApplicationFactory(); + await using var factory = new ScannerApplicationFactory(); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var response = await client.GetAsync("/api/v1/delta/quick?baseDigest=sha256:base123"); @@ -117,7 +122,8 @@ public sealed class DeltaCompareEndpointsTests [Fact] public async Task GetComparison_NotFound_ReturnsNotFound() { - using var factory = new ScannerApplicationFactory(); + await using var factory = new ScannerApplicationFactory(); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var response = await client.GetAsync("/api/v1/delta/nonexistent-id"); @@ -128,7 +134,8 @@ public sealed class DeltaCompareEndpointsTests [Fact] public async Task PostCompare_DeterministicComparisonId_SameInputsSameId() { - using var factory = new ScannerApplicationFactory(); + await using var factory = new ScannerApplicationFactory(); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var request = new DeltaCompareRequestDto diff --git a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/EpssEndpointsTests.cs b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/EpssEndpointsTests.cs index 47a3dda86..aa06a0afc 100644 --- a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/EpssEndpointsTests.cs +++ b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/EpssEndpointsTests.cs @@ -17,14 +17,14 @@ namespace StellaOps.Scanner.WebService.Tests; [Trait("Category", "Integration")] [Trait("Sprint", "3410.0002")] -public sealed class EpssEndpointsTests : IDisposable +public sealed class EpssEndpointsTests : IAsyncLifetime { - private readonly TestSurfaceSecretsScope _secrets; - private readonly InMemoryEpssProvider _epssProvider; - private readonly ScannerApplicationFactory _factory; - private readonly HttpClient _client; + private TestSurfaceSecretsScope _secrets = null!; + private InMemoryEpssProvider _epssProvider = null!; + private ScannerApplicationFactory _factory = null!; + private HttpClient _client = null!; - public EpssEndpointsTests() + public async Task InitializeAsync() { _secrets = new TestSurfaceSecretsScope(); _epssProvider = new InMemoryEpssProvider(); @@ -37,13 +37,14 @@ public sealed class EpssEndpointsTests : IDisposable services.AddSingleton(_epssProvider); }); + await _factory.InitializeAsync(); _client = _factory.CreateClient(); } - public void Dispose() + public async Task DisposeAsync() { _client.Dispose(); - _factory.Dispose(); + await _factory.DisposeAsync(); _secrets.Dispose(); } diff --git a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/EvidenceCompositionServiceTests.cs b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/EvidenceCompositionServiceTests.cs index 3728dba8e..f677334c7 100644 --- a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/EvidenceCompositionServiceTests.cs +++ b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/EvidenceCompositionServiceTests.cs @@ -25,10 +25,11 @@ public sealed class EvidenceEndpointsTests public async Task GetEvidence_ReturnsBadRequest_WhenScanIdInvalid() { using var secrets = new TestSurfaceSecretsScope(); - using var factory = new ScannerApplicationFactory().WithOverrides(configuration => + await using var factory = new ScannerApplicationFactory().WithOverrides(configuration => { configuration["scanner:authority:enabled"] = "false"; }); + await factory.InitializeAsync(); using var client = factory.CreateClient(); // Empty scan ID - route doesn't match @@ -42,10 +43,11 @@ public sealed class EvidenceEndpointsTests public async Task GetEvidence_ReturnsNotFound_WhenScanDoesNotExist() { using var secrets = new TestSurfaceSecretsScope(); - using var factory = new ScannerApplicationFactory().WithOverrides(configuration => + await using var factory = new ScannerApplicationFactory().WithOverrides(configuration => { configuration["scanner:authority:enabled"] = "false"; }); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var response = await client.GetAsync( @@ -60,10 +62,11 @@ public sealed class EvidenceEndpointsTests { // When no finding ID is provided, the route matches the list endpoint using var secrets = new TestSurfaceSecretsScope(); - using var factory = new ScannerApplicationFactory().WithOverrides(configuration => + await using var factory = new ScannerApplicationFactory().WithOverrides(configuration => { configuration["scanner:authority:enabled"] = "false"; }); + await factory.InitializeAsync(); using var client = factory.CreateClient(); // Create a scan first @@ -81,10 +84,11 @@ public sealed class EvidenceEndpointsTests public async Task ListEvidence_ReturnsEmptyList_WhenNoFindings() { using var secrets = new TestSurfaceSecretsScope(); - using var factory = new ScannerApplicationFactory().WithOverrides(configuration => + await using var factory = new ScannerApplicationFactory().WithOverrides(configuration => { configuration["scanner:authority:enabled"] = "false"; }); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var scanId = await CreateScanAsync(client); @@ -106,10 +110,11 @@ public sealed class EvidenceEndpointsTests // The current implementation returns empty list for non-existent scans // because the reachability service returns empty findings for unknown scans using var secrets = new TestSurfaceSecretsScope(); - using var factory = new ScannerApplicationFactory().WithOverrides(configuration => + await using var factory = new ScannerApplicationFactory().WithOverrides(configuration => { configuration["scanner:authority:enabled"] = "false"; }); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var response = await client.GetAsync("/api/v1/scans/nonexistent-scan/evidence"); diff --git a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/FindingsEvidenceControllerTests.cs b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/FindingsEvidenceControllerTests.cs index bfe91b979..8cad05c03 100644 --- a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/FindingsEvidenceControllerTests.cs +++ b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/FindingsEvidenceControllerTests.cs @@ -21,10 +21,11 @@ public sealed class FindingsEvidenceControllerTests public async Task GetEvidence_ReturnsNotFound_WhenFindingMissing() { using var secrets = new TestSurfaceSecretsScope(); - using var factory = new ScannerApplicationFactory().WithOverrides(configuration => + await using var factory = new ScannerApplicationFactory().WithOverrides(configuration => { configuration["scanner:authority:enabled"] = "false"; }); + await factory.InitializeAsync(); await EnsureTriageSchemaAsync(factory); using var client = factory.CreateClient(); @@ -38,10 +39,11 @@ public sealed class FindingsEvidenceControllerTests public async Task GetEvidence_ReturnsForbidden_WhenRawScopeMissing() { using var secrets = new TestSurfaceSecretsScope(); - using var factory = new ScannerApplicationFactory().WithOverrides(configuration => + await using var factory = new ScannerApplicationFactory().WithOverrides(configuration => { configuration["scanner:authority:enabled"] = "false"; }); + await factory.InitializeAsync(); await EnsureTriageSchemaAsync(factory); using var client = factory.CreateClient(); @@ -55,10 +57,11 @@ public sealed class FindingsEvidenceControllerTests public async Task GetEvidence_ReturnsEvidence_WhenFindingExists() { using var secrets = new TestSurfaceSecretsScope(); - using var factory = new ScannerApplicationFactory().WithOverrides(configuration => + await using var factory = new ScannerApplicationFactory().WithOverrides(configuration => { configuration["scanner:authority:enabled"] = "false"; }); + await factory.InitializeAsync(); await EnsureTriageSchemaAsync(factory); using var client = factory.CreateClient(); @@ -79,10 +82,11 @@ public sealed class FindingsEvidenceControllerTests public async Task BatchEvidence_ReturnsBadRequest_WhenTooMany() { using var secrets = new TestSurfaceSecretsScope(); - using var factory = new ScannerApplicationFactory().WithOverrides(configuration => + await using var factory = new ScannerApplicationFactory().WithOverrides(configuration => { configuration["scanner:authority:enabled"] = "false"; }); + await factory.InitializeAsync(); await EnsureTriageSchemaAsync(factory); using var client = factory.CreateClient(); @@ -101,10 +105,11 @@ public sealed class FindingsEvidenceControllerTests public async Task BatchEvidence_ReturnsResults_ForExistingFindings() { using var secrets = new TestSurfaceSecretsScope(); - using var factory = new ScannerApplicationFactory().WithOverrides(configuration => + await using var factory = new ScannerApplicationFactory().WithOverrides(configuration => { configuration["scanner:authority:enabled"] = "false"; }); + await factory.InitializeAsync(); await EnsureTriageSchemaAsync(factory); using var client = factory.CreateClient(); diff --git a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/HealthEndpointsTests.cs b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/HealthEndpointsTests.cs index c64c30f44..4511b43f8 100644 --- a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/HealthEndpointsTests.cs +++ b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/HealthEndpointsTests.cs @@ -10,7 +10,8 @@ public sealed class HealthEndpointsTests [Fact] public async Task HealthAndReadyEndpointsRespond() { - using var factory = new ScannerApplicationFactory(); + await using var factory = new ScannerApplicationFactory(); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var healthResponse = await client.GetAsync("/healthz"); diff --git a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/HumanApprovalAttestationServiceTests.cs b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/HumanApprovalAttestationServiceTests.cs index 9adb21136..f1ac6defa 100644 --- a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/HumanApprovalAttestationServiceTests.cs +++ b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/HumanApprovalAttestationServiceTests.cs @@ -11,6 +11,7 @@ using System.Threading; using System.Threading.Tasks; using FluentAssertions; using Microsoft.Extensions.Logging.Abstractions; +using StellaOps.Determinism; using StellaOps.Scanner.WebService.Contracts; using StellaOps.Scanner.WebService.Domain; using StellaOps.Scanner.WebService.Services; @@ -28,6 +29,7 @@ public sealed class HumanApprovalAttestationServiceTests { private readonly FakeTimeProvider _timeProvider; private readonly HumanApprovalAttestationService _service; + private readonly IGuidProvider _guidProvider = new SequentialGuidProvider(); public HumanApprovalAttestationServiceTests() { @@ -35,6 +37,7 @@ public sealed class HumanApprovalAttestationServiceTests _service = new HumanApprovalAttestationService( NullLogger.Instance, MsOptions.Options.Create(new HumanApprovalAttestationOptions { DefaultApprovalTtlDays = 30 }), + _guidProvider, _timeProvider); } @@ -319,7 +322,7 @@ public sealed class HumanApprovalAttestationServiceTests public async Task GetAttestationAsync_NonExistentAttestation_ReturnsNull() { // Act - var result = await _service.GetAttestationAsync(ScanId.New(), "nonexistent"); + var result = await _service.GetAttestationAsync(ScanId.New(_guidProvider), "nonexistent"); // Assert result.Should().BeNull(); @@ -338,6 +341,7 @@ public sealed class HumanApprovalAttestationServiceTests var service = new HumanApprovalAttestationService( NullLogger.Instance, MsOptions.Options.Create(new HumanApprovalAttestationOptions()), + _guidProvider, expiredProvider); // Need to create in this service instance for the store to be shared @@ -354,7 +358,7 @@ public sealed class HumanApprovalAttestationServiceTests await _service.CreateAttestationAsync(input); // Act - var result = await _service.GetAttestationAsync(ScanId.New(), input.FindingId); + var result = await _service.GetAttestationAsync(ScanId.New(_guidProvider), input.FindingId); // Assert result.Should().BeNull(); @@ -384,7 +388,7 @@ public sealed class HumanApprovalAttestationServiceTests public async Task GetApprovalsByScanAsync_MultipleApprovals_ReturnsAll() { // Arrange - var scanId = ScanId.New(); + var scanId = ScanId.New(_guidProvider); var input1 = CreateValidInput() with { ScanId = scanId, FindingId = "CVE-2024-0001" }; var input2 = CreateValidInput() with { ScanId = scanId, FindingId = "CVE-2024-0002" }; @@ -403,7 +407,7 @@ public sealed class HumanApprovalAttestationServiceTests public async Task GetApprovalsByScanAsync_NoApprovals_ReturnsEmptyList() { // Act - var results = await _service.GetApprovalsByScanAsync(ScanId.New()); + var results = await _service.GetApprovalsByScanAsync(ScanId.New(_guidProvider)); // Assert results.Should().BeEmpty(); @@ -414,7 +418,7 @@ public sealed class HumanApprovalAttestationServiceTests public async Task GetApprovalsByScanAsync_ExcludesRevokedApprovals() { // Arrange - var scanId = ScanId.New(); + var scanId = ScanId.New(_guidProvider); var input = CreateValidInput() with { ScanId = scanId }; await _service.CreateAttestationAsync(input); await _service.RevokeApprovalAsync(scanId, input.FindingId, "admin", "Testing"); @@ -455,7 +459,7 @@ public sealed class HumanApprovalAttestationServiceTests { // Act var result = await _service.RevokeApprovalAsync( - ScanId.New(), + ScanId.New(_guidProvider), "nonexistent", "admin@example.com", "Testing"); @@ -541,7 +545,7 @@ public sealed class HumanApprovalAttestationServiceTests { return new HumanApprovalAttestationInput { - ScanId = ScanId.New(), + ScanId = ScanId.New(_guidProvider), FindingId = "CVE-2024-12345", Decision = ApprovalDecision.AcceptRisk, ApproverUserId = "security-lead@example.com", diff --git a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/IdempotencyMiddlewareTests.cs b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/IdempotencyMiddlewareTests.cs index 12f2e5995..bd0a6c861 100644 --- a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/IdempotencyMiddlewareTests.cs +++ b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/IdempotencyMiddlewareTests.cs @@ -24,20 +24,25 @@ public sealed class IdempotencyMiddlewareTests private const string IdempotencyKeyHeader = "X-Idempotency-Key"; private const string IdempotencyCachedHeader = "X-Idempotency-Cached"; - private static ScannerApplicationFactory CreateFactory() => - new ScannerApplicationFactory().WithOverrides( + private static async Task CreateFactoryAsync() + { + var factory = new ScannerApplicationFactory().WithOverrides( configureConfiguration: config => { config["Scanner:Idempotency:Enabled"] = "true"; config["Scanner:Idempotency:Window"] = "24:00:00"; }); + await factory.InitializeAsync(); + return factory; + } + [Trait("Category", TestCategories.Unit)] [Fact] public async Task PostRequest_WithContentDigest_ReturnsIdempotencyKey() { // Arrange - await using var factory = CreateFactory(); + await using var factory = await CreateFactoryAsync(); using var client = factory.CreateClient(); var content = new StringContent("""{"test":"data"}""", Encoding.UTF8, "application/json"); @@ -58,7 +63,7 @@ public sealed class IdempotencyMiddlewareTests public async Task DuplicateRequest_WithSameContentDigest_ReturnsCachedResponse() { // Arrange - await using var factory = CreateFactory(); + await using var factory = await CreateFactoryAsync(); using var client = factory.CreateClient(); var requestBody = """{"artifactDigest":"sha256:test123"}"""; @@ -84,7 +89,7 @@ public sealed class IdempotencyMiddlewareTests public async Task DifferentRequests_WithDifferentDigests_AreProcessedSeparately() { // Arrange - await using var factory = CreateFactory(); + await using var factory = await CreateFactoryAsync(); using var client = factory.CreateClient(); var requestBody1 = """{"artifactDigest":"sha256:unique1"}"""; @@ -110,7 +115,7 @@ public sealed class IdempotencyMiddlewareTests public async Task GetRequest_BypassesIdempotencyMiddleware() { // Arrange - await using var factory = CreateFactory(); + await using var factory = await CreateFactoryAsync(); using var client = factory.CreateClient(); // Act @@ -125,7 +130,7 @@ public sealed class IdempotencyMiddlewareTests public async Task PostRequest_WithoutContentDigest_ComputesDigest() { // Arrange - await using var factory = CreateFactory(); + await using var factory = await CreateFactoryAsync(); using var client = factory.CreateClient(); var content = new StringContent("""{"test":"nodigest"}""", Encoding.UTF8, "application/json"); diff --git a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/Integration/EvidenceIntegrationTests.cs b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/Integration/EvidenceIntegrationTests.cs index 4d31057fc..bb5ee9248 100644 --- a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/Integration/EvidenceIntegrationTests.cs +++ b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/Integration/EvidenceIntegrationTests.cs @@ -27,7 +27,7 @@ public sealed class EvidenceIntegrationTests : IAsyncLifetime private ScannerApplicationFactory _factory = null!; private HttpClient _client = null!; - public ValueTask InitializeAsync() + public async ValueTask InitializeAsync() { _factory = new ScannerApplicationFactory().WithOverrides( configuration => @@ -41,8 +41,8 @@ public sealed class EvidenceIntegrationTests : IAsyncLifetime services.AddSingleton(new InMemoryArtifactObjectStore()); }); + await _factory.InitializeAsync(); _client = _factory.CreateClient(); - return ValueTask.CompletedTask; } public async ValueTask DisposeAsync() diff --git a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/Integration/PedigreeIntegrationTests.cs b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/Integration/PedigreeIntegrationTests.cs index f161b1170..aa506256e 100644 --- a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/Integration/PedigreeIntegrationTests.cs +++ b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/Integration/PedigreeIntegrationTests.cs @@ -29,7 +29,7 @@ public sealed class PedigreeIntegrationTests : IAsyncLifetime private ScannerApplicationFactory _factory = null!; private HttpClient _client = null!; - public ValueTask InitializeAsync() + public async ValueTask InitializeAsync() { _factory = new ScannerApplicationFactory().WithOverrides( configuration => @@ -48,8 +48,8 @@ public sealed class PedigreeIntegrationTests : IAsyncLifetime services.AddSingleton(new MockPedigreeDataProvider()); }); + await _factory.InitializeAsync(); _client = _factory.CreateClient(); - return ValueTask.CompletedTask; } public async ValueTask DisposeAsync() @@ -412,7 +412,7 @@ public sealed class PedigreeIntegrationTests : IAsyncLifetime return Task.FromResult(null); } - public Task> GetPedigreesBatchAsync( + public async Task> GetPedigreesBatchAsync( IEnumerable purls, CancellationToken cancellationToken = default) { @@ -420,14 +420,14 @@ public sealed class PedigreeIntegrationTests : IAsyncLifetime foreach (var purl in purls) { - var data = GetPedigreeAsync(purl, cancellationToken).Result; + var data = await GetPedigreeAsync(purl, cancellationToken); if (data != null) { results[purl] = data; } } - return Task.FromResult>(results); + return results; } } diff --git a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/Integration/ProofReplayWorkflowTests.cs b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/Integration/ProofReplayWorkflowTests.cs index d26c83458..1aaa98326 100644 --- a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/Integration/ProofReplayWorkflowTests.cs +++ b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/Integration/ProofReplayWorkflowTests.cs @@ -2,7 +2,7 @@ // ProofReplayWorkflowTests.cs // Sprint: SPRINT_3500_0002_0003_proof_replay_api // Task: T7 - Integration Tests for Proof Replay Workflow -// Description: End-to-end tests for scan → manifest → proofs workflow +// Description: End-to-end tests for scan -> manifest -> proofs workflow // ----------------------------------------------------------------------------- using System.Net; @@ -20,7 +20,7 @@ namespace StellaOps.Scanner.WebService.Tests.Integration; /// /// Integration tests for the complete proof replay workflow: -/// Submit scan → Get manifest → Replay score → Get proofs. +/// Submit scan -> Get manifest -> Replay score -> Get proofs. /// public sealed class ProofReplayWorkflowTests { @@ -31,27 +31,28 @@ public sealed class ProofReplayWorkflowTests { // Arrange await using var factory = new ScannerApplicationFactory(); + await factory.InitializeAsync(); using var scope = factory.Services.CreateScope(); var manifestRepository = scope.ServiceProvider.GetRequiredService(); var bundleRepository = scope.ServiceProvider.GetRequiredService(); - var scanId = Guid.NewGuid(); + var scanId = CreateGuid(1); // Seed test data for the scan var manifestRow = new ScanManifestRow { - ManifestId = Guid.NewGuid(), + ManifestId = CreateGuid(2), ScanId = scanId, ManifestHash = "sha256:workflow-manifest", SbomHash = "sha256:workflow-sbom", RulesHash = "sha256:workflow-rules", FeedHash = "sha256:workflow-feed", PolicyHash = "sha256:workflow-policy", - ScanStartedAt = DateTimeOffset.UtcNow.AddMinutes(-10), - ScanCompletedAt = DateTimeOffset.UtcNow, + ScanStartedAt = FixedNow.AddMinutes(-10), + ScanCompletedAt = FixedNow, ManifestContent = """{"version":"1.0","test":"workflow"}""", ScannerVersion = "1.0.0-integration", - CreatedAt = DateTimeOffset.UtcNow + CreatedAt = FixedNow }; await manifestRepository.SaveAsync(manifestRow); @@ -62,7 +63,7 @@ public sealed class ProofReplayWorkflowTests RootHash = "sha256:workflow-root", BundleType = "standard", BundleHash = "sha256:workflow-bundle", - CreatedAt = DateTimeOffset.UtcNow + CreatedAt = FixedNow }; await bundleRepository.SaveAsync(proofBundle); @@ -103,11 +104,12 @@ public sealed class ProofReplayWorkflowTests { // Arrange await using var factory = new ScannerApplicationFactory(); + await factory.InitializeAsync(); using var scope = factory.Services.CreateScope(); var manifestRepository = scope.ServiceProvider.GetRequiredService(); var bundleRepository = scope.ServiceProvider.GetRequiredService(); - var scanId = Guid.NewGuid(); + var scanId = CreateGuid(3); // Create two proof bundles with the same content should produce same hash var manifestContent = """{"version":"1.0","inputs":{"deterministic":true,"seed":"test-seed-123"}}"""; @@ -115,18 +117,18 @@ public sealed class ProofReplayWorkflowTests var manifestRow = new ScanManifestRow { - ManifestId = Guid.NewGuid(), + ManifestId = CreateGuid(4), ScanId = scanId, ManifestHash = $"sha256:{expectedHash}", SbomHash = "sha256:deterministic-sbom", RulesHash = "sha256:deterministic-rules", FeedHash = "sha256:deterministic-feed", PolicyHash = "sha256:deterministic-policy", - ScanStartedAt = DateTimeOffset.UtcNow.AddMinutes(-5), - ScanCompletedAt = DateTimeOffset.UtcNow, + ScanStartedAt = FixedNow.AddMinutes(-5), + ScanCompletedAt = FixedNow, ManifestContent = manifestContent, ScannerVersion = "1.0.0-deterministic", - CreatedAt = DateTimeOffset.UtcNow + CreatedAt = FixedNow }; await manifestRepository.SaveAsync(manifestRow); @@ -161,6 +163,7 @@ public sealed class ProofReplayWorkflowTests { config["Scanner:Idempotency:Enabled"] = "true"; }); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var requestBody = """{"artifactDigest":"sha256:idempotent-test-123"}"""; @@ -195,8 +198,9 @@ public sealed class ProofReplayWorkflowTests config["scanner:rateLimiting:manifestPermitLimit"] = "2"; config["scanner:rateLimiting:manifestWindow"] = "00:00:30"; }); + await factory.InitializeAsync(); using var client = factory.CreateClient(); - var scanId = Guid.NewGuid(); + var scanId = CreateGuid(5); // Act - Send requests exceeding the limit var responses = new List(); @@ -226,8 +230,9 @@ public sealed class ProofReplayWorkflowTests config["scanner:rateLimiting:manifestPermitLimit"] = "1"; config["scanner:rateLimiting:manifestWindow"] = "01:00:00"; }); + await factory.InitializeAsync(); using var client = factory.CreateClient(); - var scanId = Guid.NewGuid(); + var scanId = CreateGuid(6); // First request await client.GetAsync($"/api/v1/scans/{scanId}/manifest"); @@ -246,6 +251,11 @@ public sealed class ProofReplayWorkflowTests #endregion + private static readonly DateTimeOffset FixedNow = new(2026, 1, 1, 0, 0, 0, TimeSpan.Zero); + + private static Guid CreateGuid(int seed) + => new($"00000000-0000-0000-0000-{seed:D12}"); + #region Helper Methods private static string ComputeSha256(string content) diff --git a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/Integration/ValidationIntegrationTests.cs b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/Integration/ValidationIntegrationTests.cs index 6b75a29f3..d4083edd7 100644 --- a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/Integration/ValidationIntegrationTests.cs +++ b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/Integration/ValidationIntegrationTests.cs @@ -31,7 +31,7 @@ public sealed class ValidationIntegrationTests : IAsyncLifetime private ScannerApplicationFactory _factory = null!; private HttpClient _client = null!; - public ValueTask InitializeAsync() + public async ValueTask InitializeAsync() { _factory = new ScannerApplicationFactory().WithOverrides( configuration => @@ -51,8 +51,8 @@ public sealed class ValidationIntegrationTests : IAsyncLifetime services.AddSingleton(new MockSbomValidator()); }); + await _factory.InitializeAsync(); _client = _factory.CreateClient(); - return ValueTask.CompletedTask; } public async ValueTask DisposeAsync() @@ -159,7 +159,7 @@ public sealed class ValidationIntegrationTests : IAsyncLifetime sbomBytes, SbomFormat.CycloneDxJson, validationOptions, - CancellationToken.None); + TestContext.Current.CancellationToken); // Assert Assert.NotNull(result); @@ -189,7 +189,7 @@ public sealed class ValidationIntegrationTests : IAsyncLifetime spdxBytes, SbomFormat.Spdx3JsonLd, validationOptions, - CancellationToken.None); + TestContext.Current.CancellationToken); // Assert Assert.NotNull(result); @@ -207,7 +207,7 @@ public sealed class ValidationIntegrationTests : IAsyncLifetime Assert.True(mockValidator.SupportsFormat(SbomFormat.Spdx3JsonLd)); Assert.True(mockValidator.SupportsFormat(SbomFormat.Unknown)); - var info = await mockValidator.GetInfoAsync(CancellationToken.None); + var info = await mockValidator.GetInfoAsync(TestContext.Current.CancellationToken); Assert.True(info.IsAvailable); Assert.Contains(SbomFormat.CycloneDxJson, info.SupportedFormats); } @@ -229,7 +229,7 @@ public sealed class ValidationIntegrationTests : IAsyncLifetime sbomBytes, SbomFormat.CycloneDxJson, validationOptions, - CancellationToken.None); + TestContext.Current.CancellationToken); // Assert Assert.NotNull(result); @@ -254,7 +254,7 @@ public sealed class ValidationIntegrationTests : IAsyncLifetime sbomBytes, SbomFormat.CycloneDxJson, validationOptions, - CancellationToken.None); + TestContext.Current.CancellationToken); // Assert Assert.NotNull(result); @@ -310,11 +310,11 @@ public sealed class ValidationIntegrationTests : IAsyncLifetime Reference = "example.com/validation-test:1.0", Digest = "sha256:validation123" } - }); + }, TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); - var payload = await response.Content.ReadFromJsonAsync(); + var payload = await response.Content.ReadFromJsonAsync(TestContext.Current.CancellationToken); Assert.NotNull(payload); return payload!.ScanId; } diff --git a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/LayerSbomEndpointsTests.cs b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/LayerSbomEndpointsTests.cs index 337b1eb4e..43a0ffdd1 100644 --- a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/LayerSbomEndpointsTests.cs +++ b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/LayerSbomEndpointsTests.cs @@ -34,11 +34,12 @@ public sealed class LayerSbomEndpointsTests using var secrets = new TestSurfaceSecretsScope(); var mockService = new InMemoryLayerSbomService(); - using var factory = new ScannerApplicationFactory() + await using var factory = new ScannerApplicationFactory() .WithOverrides(configureServices: services => { services.AddSingleton(mockService); }); + await factory.InitializeAsync(); using var client = factory.CreateClient(); // Submit scan via HTTP POST to get scan ID @@ -62,11 +63,12 @@ public sealed class LayerSbomEndpointsTests public async Task ListLayers_WhenScanNotFound_Returns404() { using var secrets = new TestSurfaceSecretsScope(); - using var factory = new ScannerApplicationFactory() + await using var factory = new ScannerApplicationFactory() .WithOverrides(configureServices: services => { services.AddSingleton(); }); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var response = await client.GetAsync($"{BasePath}/scan-not-found/layers"); @@ -81,11 +83,12 @@ public sealed class LayerSbomEndpointsTests using var secrets = new TestSurfaceSecretsScope(); var mockService = new InMemoryLayerSbomService(); - using var factory = new ScannerApplicationFactory() + await using var factory = new ScannerApplicationFactory() .WithOverrides(configureServices: services => { services.AddSingleton(mockService); }); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var scanId = await SubmitScanAsync(client, imageDigest); @@ -121,11 +124,12 @@ public sealed class LayerSbomEndpointsTests using var secrets = new TestSurfaceSecretsScope(); var mockService = new InMemoryLayerSbomService(); - using var factory = new ScannerApplicationFactory() + await using var factory = new ScannerApplicationFactory() .WithOverrides(configureServices: services => { services.AddSingleton(mockService); }); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var scanId = await SubmitScanAsync(client, imageDigest); @@ -149,11 +153,12 @@ public sealed class LayerSbomEndpointsTests using var secrets = new TestSurfaceSecretsScope(); var mockService = new InMemoryLayerSbomService(); - using var factory = new ScannerApplicationFactory() + await using var factory = new ScannerApplicationFactory() .WithOverrides(configureServices: services => { services.AddSingleton(mockService); }); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var scanId = await SubmitScanAsync(client, imageDigest); @@ -177,11 +182,12 @@ public sealed class LayerSbomEndpointsTests using var secrets = new TestSurfaceSecretsScope(); var mockService = new InMemoryLayerSbomService(); - using var factory = new ScannerApplicationFactory() + await using var factory = new ScannerApplicationFactory() .WithOverrides(configureServices: services => { services.AddSingleton(mockService); }); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var scanId = await SubmitScanAsync(client, imageDigest); @@ -201,11 +207,12 @@ public sealed class LayerSbomEndpointsTests public async Task GetLayerSbom_WhenScanNotFound_Returns404() { using var secrets = new TestSurfaceSecretsScope(); - using var factory = new ScannerApplicationFactory() + await using var factory = new ScannerApplicationFactory() .WithOverrides(configureServices: services => { services.AddSingleton(); }); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var response = await client.GetAsync($"{BasePath}/scan-not-found/layers/sha256:layer123/sbom"); @@ -220,11 +227,12 @@ public sealed class LayerSbomEndpointsTests using var secrets = new TestSurfaceSecretsScope(); var mockService = new InMemoryLayerSbomService(); - using var factory = new ScannerApplicationFactory() + await using var factory = new ScannerApplicationFactory() .WithOverrides(configureServices: services => { services.AddSingleton(mockService); }); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var scanId = await SubmitScanAsync(client, imageDigest); @@ -246,11 +254,12 @@ public sealed class LayerSbomEndpointsTests using var secrets = new TestSurfaceSecretsScope(); var mockService = new InMemoryLayerSbomService(); - using var factory = new ScannerApplicationFactory() + await using var factory = new ScannerApplicationFactory() .WithOverrides(configureServices: services => { services.AddSingleton(mockService); }); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var scanId = await SubmitScanAsync(client, imageDigest); @@ -273,11 +282,12 @@ public sealed class LayerSbomEndpointsTests public async Task GetCompositionRecipe_WhenScanNotFound_Returns404() { using var secrets = new TestSurfaceSecretsScope(); - using var factory = new ScannerApplicationFactory() + await using var factory = new ScannerApplicationFactory() .WithOverrides(configureServices: services => { services.AddSingleton(); }); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var response = await client.GetAsync($"{BasePath}/scan-not-found/composition-recipe"); @@ -292,11 +302,12 @@ public sealed class LayerSbomEndpointsTests using var secrets = new TestSurfaceSecretsScope(); var mockService = new InMemoryLayerSbomService(); - using var factory = new ScannerApplicationFactory() + await using var factory = new ScannerApplicationFactory() .WithOverrides(configureServices: services => { services.AddSingleton(mockService); }); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var scanId = await SubmitScanAsync(client, imageDigest); @@ -326,12 +337,13 @@ public sealed class LayerSbomEndpointsTests Errors = ImmutableArray.Empty, }); - using var factory = new ScannerApplicationFactory() + await using var factory = new ScannerApplicationFactory() .WithOverrides(configureServices: services => { services.RemoveAll(); services.AddSingleton(mockService); }); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var response = await client.PostAsync($"{BasePath}/{scanId}/composition-recipe/verify", null); @@ -359,12 +371,13 @@ public sealed class LayerSbomEndpointsTests Errors = ImmutableArray.Create("Merkle root mismatch: expected sha256:abc, got sha256:def"), }); - using var factory = new ScannerApplicationFactory() + await using var factory = new ScannerApplicationFactory() .WithOverrides(configureServices: services => { services.RemoveAll(); services.AddSingleton(mockService); }); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var response = await client.PostAsync($"{BasePath}/{scanId}/composition-recipe/verify", null); @@ -382,12 +395,13 @@ public sealed class LayerSbomEndpointsTests [Fact] public async Task VerifyCompositionRecipe_WhenScanNotFound_Returns404() { - using var factory = new ScannerApplicationFactory() + await using var factory = new ScannerApplicationFactory() .WithOverrides(configureServices: services => { services.RemoveAll(); services.AddSingleton(); }); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var response = await client.PostAsync($"{BasePath}/scan-not-found/composition-recipe/verify", null); diff --git a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/ManifestEndpointsTests.cs b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/ManifestEndpointsTests.cs index 5cf0e9f41..5a3798fc2 100644 --- a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/ManifestEndpointsTests.cs +++ b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/ManifestEndpointsTests.cs @@ -35,26 +35,27 @@ public sealed class ManifestEndpointsTests { // Arrange await using var factory = new ScannerApplicationFactory(); + await factory.InitializeAsync(); using var client = factory.CreateClient(); using var scope = factory.Services.CreateScope(); var manifestRepository = scope.ServiceProvider.GetRequiredService(); - var scanId = Guid.NewGuid(); + var scanId = CreateGuid(1); var manifestRow = new ScanManifestRow { - ManifestId = Guid.NewGuid(), + ManifestId = CreateGuid(2), ScanId = scanId, ManifestHash = "sha256:manifest123", SbomHash = "sha256:sbom123", RulesHash = "sha256:rules123", FeedHash = "sha256:feed123", PolicyHash = "sha256:policy123", - ScanStartedAt = DateTimeOffset.UtcNow.AddMinutes(-5), - ScanCompletedAt = DateTimeOffset.UtcNow, + ScanStartedAt = FixedNow.AddMinutes(-5), + ScanCompletedAt = FixedNow, ManifestContent = """{"version":"1.0","inputs":{"sbomHash":"sha256:sbom123"}}""", ScannerVersion = "1.0.0-test", - CreatedAt = DateTimeOffset.UtcNow + CreatedAt = FixedNow }; await manifestRepository.SaveAsync(manifestRow, TestContext.Current.CancellationToken); @@ -82,8 +83,9 @@ public sealed class ManifestEndpointsTests { // Arrange await using var factory = new ScannerApplicationFactory(); + await factory.InitializeAsync(); using var client = factory.CreateClient(); - var scanId = Guid.NewGuid(); + var scanId = CreateGuid(3); // Act var response = await client.GetAsync($"/api/v1/scans/{scanId}/manifest", TestContext.Current.CancellationToken); @@ -98,6 +100,7 @@ public sealed class ManifestEndpointsTests { // Arrange await using var factory = new ScannerApplicationFactory(); + await factory.InitializeAsync(); using var client = factory.CreateClient(); // Act @@ -113,11 +116,12 @@ public sealed class ManifestEndpointsTests { // Arrange await using var factory = new ScannerApplicationFactory(); + await factory.InitializeAsync(); using var client = factory.CreateClient(); using var scope = factory.Services.CreateScope(); var manifestRepository = scope.ServiceProvider.GetRequiredService(); - var scanId = Guid.NewGuid(); + var scanId = CreateGuid(4); var manifestContent = JsonSerializer.Serialize(new { @@ -133,18 +137,18 @@ public sealed class ManifestEndpointsTests var manifestRow = new ScanManifestRow { - ManifestId = Guid.NewGuid(), + ManifestId = CreateGuid(5), ScanId = scanId, ManifestHash = "sha256:manifest456", SbomHash = "sha256:sbom123", RulesHash = "sha256:rules123", FeedHash = "sha256:feed123", PolicyHash = "sha256:policy123", - ScanStartedAt = DateTimeOffset.UtcNow.AddMinutes(-5), - ScanCompletedAt = DateTimeOffset.UtcNow, + ScanStartedAt = FixedNow.AddMinutes(-5), + ScanCompletedAt = FixedNow, ManifestContent = manifestContent, ScannerVersion = "1.0.0-test", - CreatedAt = DateTimeOffset.UtcNow + CreatedAt = FixedNow }; await manifestRepository.SaveAsync(manifestRow, TestContext.Current.CancellationToken); @@ -173,26 +177,27 @@ public sealed class ManifestEndpointsTests { // Arrange await using var factory = new ScannerApplicationFactory(); + await factory.InitializeAsync(); using var client = factory.CreateClient(); using var scope = factory.Services.CreateScope(); var manifestRepository = scope.ServiceProvider.GetRequiredService(); - var scanId = Guid.NewGuid(); + var scanId = CreateGuid(6); var manifestRow = new ScanManifestRow { - ManifestId = Guid.NewGuid(), + ManifestId = CreateGuid(7), ScanId = scanId, ManifestHash = "sha256:content-digest-test", SbomHash = "sha256:sbom789", RulesHash = "sha256:rules789", FeedHash = "sha256:feed789", PolicyHash = "sha256:policy789", - ScanStartedAt = DateTimeOffset.UtcNow.AddMinutes(-2), - ScanCompletedAt = DateTimeOffset.UtcNow, + ScanStartedAt = FixedNow.AddMinutes(-2), + ScanCompletedAt = FixedNow, ManifestContent = """{"test":"content-digest"}""", ScannerVersion = "1.0.0-test", - CreatedAt = DateTimeOffset.UtcNow + CreatedAt = FixedNow }; await manifestRepository.SaveAsync(manifestRow, TestContext.Current.CancellationToken); @@ -219,8 +224,9 @@ public sealed class ManifestEndpointsTests { // Arrange await using var factory = new ScannerApplicationFactory(); + await factory.InitializeAsync(); using var client = factory.CreateClient(); - var scanId = Guid.NewGuid(); + var scanId = CreateGuid(8); // Act var response = await client.GetAsync($"/api/v1/scans/{scanId}/proofs"); @@ -240,11 +246,12 @@ public sealed class ManifestEndpointsTests { // Arrange await using var factory = new ScannerApplicationFactory(); + await factory.InitializeAsync(); using var client = factory.CreateClient(); using var scope = factory.Services.CreateScope(); var bundleRepository = scope.ServiceProvider.GetRequiredService(); - var scanId = Guid.NewGuid(); + var scanId = CreateGuid(9); var bundle1 = new ProofBundleRow { @@ -252,7 +259,7 @@ public sealed class ManifestEndpointsTests RootHash = "sha256:root1", BundleType = "standard", BundleHash = "sha256:bundle1", - CreatedAt = DateTimeOffset.UtcNow.AddMinutes(-5) + CreatedAt = FixedNow.AddMinutes(-5) }; var bundle2 = new ProofBundleRow @@ -261,7 +268,7 @@ public sealed class ManifestEndpointsTests RootHash = "sha256:root2", BundleType = "extended", BundleHash = "sha256:bundle2", - CreatedAt = DateTimeOffset.UtcNow.AddMinutes(-2) + CreatedAt = FixedNow.AddMinutes(-2) }; await bundleRepository.SaveAsync(bundle1); @@ -286,6 +293,7 @@ public sealed class ManifestEndpointsTests { // Arrange await using var factory = new ScannerApplicationFactory(); + await factory.InitializeAsync(); using var client = factory.CreateClient(); // Act @@ -305,11 +313,12 @@ public sealed class ManifestEndpointsTests { // Arrange await using var factory = new ScannerApplicationFactory(); + await factory.InitializeAsync(); using var client = factory.CreateClient(); using var scope = factory.Services.CreateScope(); var bundleRepository = scope.ServiceProvider.GetRequiredService(); - var scanId = Guid.NewGuid(); + var scanId = CreateGuid(10); var rootHash = "sha256:detailroot1"; var bundle = new ProofBundleRow @@ -324,8 +333,8 @@ public sealed class ManifestEndpointsTests VexHash = "sha256:vex1", SignatureKeyId = "key-001", SignatureAlgorithm = "ed25519", - CreatedAt = DateTimeOffset.UtcNow.AddMinutes(-3), - ExpiresAt = DateTimeOffset.UtcNow.AddDays(30) + CreatedAt = FixedNow.AddMinutes(-3), + ExpiresAt = FixedNow.AddDays(30) }; await bundleRepository.SaveAsync(bundle); @@ -356,8 +365,9 @@ public sealed class ManifestEndpointsTests { // Arrange await using var factory = new ScannerApplicationFactory(); + await factory.InitializeAsync(); using var client = factory.CreateClient(); - var scanId = Guid.NewGuid(); + var scanId = CreateGuid(11); // Act var response = await client.GetAsync($"/api/v1/scans/{scanId}/proofs/sha256:nonexistent"); @@ -372,12 +382,13 @@ public sealed class ManifestEndpointsTests { // Arrange await using var factory = new ScannerApplicationFactory(); + await factory.InitializeAsync(); using var client = factory.CreateClient(); using var scope = factory.Services.CreateScope(); var bundleRepository = scope.ServiceProvider.GetRequiredService(); - var scanId1 = Guid.NewGuid(); - var scanId2 = Guid.NewGuid(); + var scanId1 = CreateGuid(12); + var scanId2 = CreateGuid(13); var rootHash = "sha256:crossscanroot"; var bundle = new ProofBundleRow @@ -386,7 +397,7 @@ public sealed class ManifestEndpointsTests RootHash = rootHash, BundleType = "standard", BundleHash = "sha256:crossscanbundle", - CreatedAt = DateTimeOffset.UtcNow + CreatedAt = FixedNow }; await bundleRepository.SaveAsync(bundle); @@ -404,6 +415,7 @@ public sealed class ManifestEndpointsTests { // Arrange await using var factory = new ScannerApplicationFactory(); + await factory.InitializeAsync(); using var client = factory.CreateClient(); // Act @@ -419,8 +431,9 @@ public sealed class ManifestEndpointsTests { // Arrange await using var factory = new ScannerApplicationFactory(); + await factory.InitializeAsync(); using var client = factory.CreateClient(); - var scanId = Guid.NewGuid(); + var scanId = CreateGuid(14); // Act - Trailing slash with empty root hash var response = await client.GetAsync($"/api/v1/scans/{scanId}/proofs/"); @@ -431,4 +444,9 @@ public sealed class ManifestEndpointsTests } #endregion + + private static readonly DateTimeOffset FixedNow = new(2026, 1, 1, 0, 0, 0, TimeSpan.Zero); + + private static Guid CreateGuid(int seed) + => new($"00000000-0000-0000-0000-{seed:D12}"); } diff --git a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/OfflineKitEndpointsTests.cs b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/OfflineKitEndpointsTests.cs index ce6050204..3540f1973 100644 --- a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/OfflineKitEndpointsTests.cs +++ b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/OfflineKitEndpointsTests.cs @@ -29,7 +29,7 @@ public sealed class OfflineKitEndpointsTests var (keyId, keyPem, dsseJson) = CreateSignedDsse(bundleBytes); File.WriteAllText(Path.Combine(trustRoots.Path, $"{keyId}.pem"), keyPem, Encoding.UTF8); - using var factory = new ScannerApplicationFactory().WithOverrides(config => + await using var factory = new ScannerApplicationFactory().WithOverrides(config => { config["Scanner:OfflineKit:Enabled"] = "true"; config["Scanner:OfflineKit:RequireDsse"] = "true"; @@ -39,6 +39,7 @@ public sealed class OfflineKitEndpointsTests config["Scanner:OfflineKit:TrustAnchors:0:PurlPattern"] = "*"; config["Scanner:OfflineKit:TrustAnchors:0:AllowedKeyIds:0"] = keyId; }); + await factory.InitializeAsync(); using var configured = factory.WithWebHostBuilder(builder => builder.UseContentRoot(contentRoot.Path)); using var client = configured.CreateClient(); @@ -97,7 +98,7 @@ public sealed class OfflineKitEndpointsTests signatures = new[] { new { keyid = keyId, sig = Convert.ToBase64String(new byte[] { 1, 2, 3 }) } } }, new JsonSerializerOptions(JsonSerializerDefaults.Web)); - using var factory = new ScannerApplicationFactory().WithOverrides(config => + await using var factory = new ScannerApplicationFactory().WithOverrides(config => { config["Scanner:OfflineKit:Enabled"] = "true"; config["Scanner:OfflineKit:RequireDsse"] = "true"; @@ -107,6 +108,7 @@ public sealed class OfflineKitEndpointsTests config["Scanner:OfflineKit:TrustAnchors:0:PurlPattern"] = "*"; config["Scanner:OfflineKit:TrustAnchors:0:AllowedKeyIds:0"] = keyId; }); + await factory.InitializeAsync(); using var configured = factory.WithWebHostBuilder(builder => builder.UseContentRoot(contentRoot.Path)); using var client = configured.CreateClient(); @@ -151,12 +153,13 @@ public sealed class OfflineKitEndpointsTests signatures = new[] { new { keyid = "unknown", sig = Convert.ToBase64String(new byte[] { 1, 2, 3 }) } } }, new JsonSerializerOptions(JsonSerializerDefaults.Web)); - using var factory = new ScannerApplicationFactory().WithOverrides(config => + await using var factory = new ScannerApplicationFactory().WithOverrides(config => { config["Scanner:OfflineKit:Enabled"] = "true"; config["Scanner:OfflineKit:RequireDsse"] = "false"; config["Scanner:OfflineKit:RekorOfflineMode"] = "false"; }); + await factory.InitializeAsync(); using var configured = factory.WithWebHostBuilder(builder => builder.UseContentRoot(contentRoot.Path)); using var client = configured.CreateClient(); @@ -192,7 +195,7 @@ public sealed class OfflineKitEndpointsTests var auditEmitter = new CapturingAuditEmitter(); - using var factory = new ScannerApplicationFactory().WithOverrides(config => + await using var factory = new ScannerApplicationFactory().WithOverrides(config => { config["Scanner:OfflineKit:Enabled"] = "true"; config["Scanner:OfflineKit:RequireDsse"] = "false"; @@ -202,6 +205,7 @@ public sealed class OfflineKitEndpointsTests services.RemoveAll(); services.AddSingleton(auditEmitter); }); + await factory.InitializeAsync(); using var configured = factory.WithWebHostBuilder(builder => builder.UseContentRoot(contentRoot.Path)); using var client = configured.CreateClient(); @@ -241,10 +245,11 @@ public sealed class OfflineKitEndpointsTests { using var contentRoot = new TempDirectory(); - using var factory = new ScannerApplicationFactory().WithOverrides(config => + await using var factory = new ScannerApplicationFactory().WithOverrides(config => { config["Scanner:OfflineKit:Enabled"] = "true"; }); + await factory.InitializeAsync(); using var configured = factory.WithWebHostBuilder(builder => builder.UseContentRoot(contentRoot.Path)); using var client = configured.CreateClient(); @@ -262,11 +267,12 @@ public sealed class OfflineKitEndpointsTests var bundleBytes = Encoding.UTF8.GetBytes("deterministic-offline-kit-bundle"); var bundleSha = ComputeSha256Hex(bundleBytes); - using var factory = new ScannerApplicationFactory().WithOverrides(config => + await using var factory = new ScannerApplicationFactory().WithOverrides(config => { config["Scanner:OfflineKit:Enabled"] = "true"; config["Scanner:OfflineKit:RequireDsse"] = "false"; }); + await factory.InitializeAsync(); using var configured = factory.WithWebHostBuilder(builder => builder.UseContentRoot(contentRoot.Path)); using var client = configured.CreateClient(); @@ -304,10 +310,11 @@ public sealed class OfflineKitEndpointsTests { using var contentRoot = new TempDirectory(); - using var factory = new ScannerApplicationFactory().WithOverrides(config => + await using var factory = new ScannerApplicationFactory().WithOverrides(config => { config["Scanner:OfflineKit:Enabled"] = "true"; }); + await factory.InitializeAsync(); using var configured = factory.WithWebHostBuilder(builder => builder.UseContentRoot(contentRoot.Path)); using var client = configured.CreateClient(); @@ -346,10 +353,11 @@ public sealed class OfflineKitEndpointsTests { using var contentRoot = new TempDirectory(); - using var factory = new ScannerApplicationFactory().WithOverrides(config => + await using var factory = new ScannerApplicationFactory().WithOverrides(config => { config["Scanner:OfflineKit:Enabled"] = "true"; }); + await factory.InitializeAsync(); using var configured = factory.WithWebHostBuilder(builder => builder.UseContentRoot(contentRoot.Path)); using var client = configured.CreateClient(); @@ -382,10 +390,11 @@ public sealed class OfflineKitEndpointsTests { using var contentRoot = new TempDirectory(); - using var factory = new ScannerApplicationFactory().WithOverrides(config => + await using var factory = new ScannerApplicationFactory().WithOverrides(config => { config["Scanner:OfflineKit:Enabled"] = "true"; }); + await factory.InitializeAsync(); using var configured = factory.WithWebHostBuilder(builder => builder.UseContentRoot(contentRoot.Path)); using var client = configured.CreateClient(); @@ -425,10 +434,11 @@ public sealed class OfflineKitEndpointsTests { using var contentRoot = new TempDirectory(); - using var factory = new ScannerApplicationFactory().WithOverrides(config => + await using var factory = new ScannerApplicationFactory().WithOverrides(config => { config["Scanner:OfflineKit:Enabled"] = "true"; }); + await factory.InitializeAsync(); using var configured = factory.WithWebHostBuilder(builder => builder.UseContentRoot(contentRoot.Path)); using var client = configured.CreateClient(); @@ -469,10 +479,11 @@ public sealed class OfflineKitEndpointsTests { using var contentRoot = new TempDirectory(); - using var factory = new ScannerApplicationFactory().WithOverrides(config => + await using var factory = new ScannerApplicationFactory().WithOverrides(config => { config["Scanner:OfflineKit:Enabled"] = "false"; }); + await factory.InitializeAsync(); using var configured = factory.WithWebHostBuilder(builder => builder.UseContentRoot(contentRoot.Path)); using var client = configured.CreateClient(); @@ -498,10 +509,11 @@ public sealed class OfflineKitEndpointsTests { using var contentRoot = new TempDirectory(); - using var factory = new ScannerApplicationFactory().WithOverrides(config => + await using var factory = new ScannerApplicationFactory().WithOverrides(config => { config["Scanner:OfflineKit:Enabled"] = "true"; }); + await factory.InitializeAsync(); using var configured = factory.WithWebHostBuilder(builder => builder.UseContentRoot(contentRoot.Path)); using var client = configured.CreateClient(); @@ -517,10 +529,11 @@ public sealed class OfflineKitEndpointsTests { using var contentRoot = new TempDirectory(); - using var factory = new ScannerApplicationFactory().WithOverrides(config => + await using var factory = new ScannerApplicationFactory().WithOverrides(config => { config["Scanner:OfflineKit:Enabled"] = "true"; }); + await factory.InitializeAsync(); using var configured = factory.WithWebHostBuilder(builder => builder.UseContentRoot(contentRoot.Path)); using var client = configured.CreateClient(); @@ -536,10 +549,11 @@ public sealed class OfflineKitEndpointsTests { using var contentRoot = new TempDirectory(); - using var factory = new ScannerApplicationFactory().WithOverrides(config => + await using var factory = new ScannerApplicationFactory().WithOverrides(config => { config["Scanner:OfflineKit:Enabled"] = "true"; }); + await factory.InitializeAsync(); using var configured = factory.WithWebHostBuilder(builder => builder.UseContentRoot(contentRoot.Path)); using var client = configured.CreateClient(); diff --git a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/PlatformEventPublisherRegistrationTests.cs b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/PlatformEventPublisherRegistrationTests.cs index 1306bdfbd..d44e37b6c 100644 --- a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/PlatformEventPublisherRegistrationTests.cs +++ b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/PlatformEventPublisherRegistrationTests.cs @@ -13,11 +13,12 @@ public sealed class PlatformEventPublisherRegistrationTests [Fact] public void NullPublisherRegisteredWhenEventsDisabled() { - using var factory = new ScannerApplicationFactory().WithOverrides(configuration => + await using var factory = new ScannerApplicationFactory().WithOverrides(configuration => { configuration["scanner:events:enabled"] = "false"; configuration["scanner:events:dsn"] = string.Empty; }); + await factory.InitializeAsync(); using var scope = factory.Services.CreateScope(); var publisher = scope.ServiceProvider.GetRequiredService(); @@ -44,7 +45,7 @@ public sealed class PlatformEventPublisherRegistrationTests try { - using var factory = new ScannerApplicationFactory().WithOverrides(configuration => + await using var factory = new ScannerApplicationFactory().WithOverrides(configuration => { configuration["scanner:events:enabled"] = "true"; configuration["scanner:events:driver"] = "redis"; @@ -53,6 +54,7 @@ public sealed class PlatformEventPublisherRegistrationTests configuration["scanner:events:publishTimeoutSeconds"] = "1"; configuration["scanner:events:maxStreamLength"] = "100"; }); + await factory.InitializeAsync(); using var scope = factory.Services.CreateScope(); var options = scope.ServiceProvider.GetRequiredService>().Value; diff --git a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/PolicyDecisionAttestationServiceTests.cs b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/PolicyDecisionAttestationServiceTests.cs index b4dad3b14..93e5ebe9d 100644 --- a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/PolicyDecisionAttestationServiceTests.cs +++ b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/PolicyDecisionAttestationServiceTests.cs @@ -11,6 +11,7 @@ using System.Threading; using System.Threading.Tasks; using FluentAssertions; using Microsoft.Extensions.Logging.Abstractions; +using StellaOps.Determinism; using StellaOps.Scanner.WebService.Contracts; using StellaOps.Scanner.WebService.Domain; using StellaOps.Scanner.WebService.Services; @@ -28,6 +29,7 @@ public sealed class PolicyDecisionAttestationServiceTests { private readonly FakeTimeProvider _timeProvider; private readonly PolicyDecisionAttestationService _service; + private readonly IGuidProvider _guidProvider = new SequentialGuidProvider(); public PolicyDecisionAttestationServiceTests() { @@ -284,7 +286,7 @@ public sealed class PolicyDecisionAttestationServiceTests { // Act var result = await _service.GetAttestationAsync( - ScanId.New(), + ScanId.New(_guidProvider), "CVE-2024-00000@pkg:npm/nonexistent@1.0.0"); // Assert @@ -301,7 +303,7 @@ public sealed class PolicyDecisionAttestationServiceTests // Act var result = await _service.GetAttestationAsync( - ScanId.New(), // Different scan ID + ScanId.New(_guidProvider), // Different scan ID input.FindingId); // Assert @@ -393,7 +395,7 @@ public sealed class PolicyDecisionAttestationServiceTests { return new PolicyDecisionInput { - ScanId = ScanId.New(), + ScanId = ScanId.New(_guidProvider), FindingId = "CVE-2024-12345@pkg:npm/stripe@6.1.2", Cve = "CVE-2024-12345", ComponentPurl = "pkg:npm/stripe@6.1.2", diff --git a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/PolicyEndpointsTests.cs b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/PolicyEndpointsTests.cs index 9f99a2adf..d9b6d52e3 100644 --- a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/PolicyEndpointsTests.cs +++ b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/PolicyEndpointsTests.cs @@ -18,7 +18,8 @@ public sealed class PolicyEndpointsTests [Fact] public async Task PolicySchemaReturnsEmbeddedSchema() { - using var factory = new ScannerApplicationFactory(); + await using var factory = new ScannerApplicationFactory(); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var response = await client.GetAsync("/api/v1/policy/schema", TestContext.Current.CancellationToken); @@ -34,7 +35,8 @@ public sealed class PolicyEndpointsTests [Fact] public async Task PolicyDiagnosticsReturnsRecommendations() { - using var factory = new ScannerApplicationFactory(); + await using var factory = new ScannerApplicationFactory(); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var request = new PolicyDiagnosticsRequestDto @@ -62,7 +64,8 @@ public sealed class PolicyEndpointsTests [Fact] public async Task PolicyPreviewUsesProposedPolicy() { - using var factory = new ScannerApplicationFactory(); + await using var factory = new ScannerApplicationFactory(); + await factory.InitializeAsync(); using var client = factory.CreateClient(); const string policyYaml = """ diff --git a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/ProofSpineEndpointsTests.cs b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/ProofSpineEndpointsTests.cs index 3d2626df6..184408937 100644 --- a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/ProofSpineEndpointsTests.cs +++ b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/ProofSpineEndpointsTests.cs @@ -22,6 +22,7 @@ public sealed class ProofSpineEndpointsTests public async Task GetSpine_ReturnsSpine_WithVerification() { await using var factory = new ScannerApplicationFactory(); + await factory.InitializeAsync(); using var scope = factory.Services.CreateScope(); var builder = scope.ServiceProvider.GetRequiredService(); @@ -62,6 +63,7 @@ public sealed class ProofSpineEndpointsTests public async Task GetSpine_ReturnsCbor_WhenAcceptHeaderRequestsCbor() { await using var factory = new ScannerApplicationFactory(); + await factory.InitializeAsync(); using var scope = factory.Services.CreateScope(); var builder = scope.ServiceProvider.GetRequiredService(); @@ -99,6 +101,7 @@ public sealed class ProofSpineEndpointsTests public async Task ListSpinesByScan_ReturnsSummaries_WithSegmentCount() { await using var factory = new ScannerApplicationFactory(); + await factory.InitializeAsync(); using var scope = factory.Services.CreateScope(); var builder = scope.ServiceProvider.GetRequiredService(); @@ -138,6 +141,7 @@ public sealed class ProofSpineEndpointsTests public async Task ListSpinesByScan_ReturnsCbor_WhenAcceptHeaderRequestsCbor() { await using var factory = new ScannerApplicationFactory(); + await factory.InitializeAsync(); using var scope = factory.Services.CreateScope(); var builder = scope.ServiceProvider.GetRequiredService(); @@ -177,6 +181,7 @@ public sealed class ProofSpineEndpointsTests public async Task GetSpine_ReturnsInvalidStatus_WhenSegmentTampered() { await using var factory = new ScannerApplicationFactory(); + await factory.InitializeAsync(); using var scope = factory.Services.CreateScope(); var builder = scope.ServiceProvider.GetRequiredService(); diff --git a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/RateLimitingTests.cs b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/RateLimitingTests.cs index 7207965a6..4304d61ac 100644 --- a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/RateLimitingTests.cs +++ b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/RateLimitingTests.cs @@ -22,8 +22,11 @@ public sealed class RateLimitingTests private const string RateLimitRemainingHeader = "X-RateLimit-Remaining"; private const string RetryAfterHeader = "Retry-After"; - private static ScannerApplicationFactory CreateFactory(int permitLimit = 100, int windowSeconds = 3600) => - new ScannerApplicationFactory().WithOverrides( + private static async Task CreateFactoryAsync( + int permitLimit = 100, + int windowSeconds = 3600) + { + var factory = new ScannerApplicationFactory().WithOverrides( configureConfiguration: config => { config["scanner:rateLimiting:scoreReplayPermitLimit"] = permitLimit.ToString(); @@ -34,12 +37,16 @@ public sealed class RateLimitingTests config["scanner:rateLimiting:proofBundleWindow"] = TimeSpan.FromSeconds(windowSeconds).ToString(); }); + await factory.InitializeAsync(); + return factory; + } + [Trait("Category", TestCategories.Unit)] [Fact] public async Task ManifestEndpoint_IncludesRateLimitHeaders() { // Arrange - await using var factory = CreateFactory(); + await using var factory = await CreateFactoryAsync(); using var client = factory.CreateClient(); var scanId = Guid.NewGuid(); @@ -58,7 +65,7 @@ public sealed class RateLimitingTests public async Task ProofBundleEndpoint_IncludesRateLimitHeaders() { // Arrange - await using var factory = CreateFactory(); + await using var factory = await CreateFactoryAsync(); using var client = factory.CreateClient(); var scanId = Guid.NewGuid(); @@ -76,7 +83,7 @@ public sealed class RateLimitingTests public async Task ExcessiveRequests_Returns429() { // Arrange - Create factory with very low rate limit for testing - await using var factory = CreateFactory(permitLimit: 2, windowSeconds: 60); + await using var factory = await CreateFactoryAsync(permitLimit: 2, windowSeconds: 60); using var client = factory.CreateClient(); var scanId = Guid.NewGuid(); @@ -103,7 +110,7 @@ public sealed class RateLimitingTests public async Task RateLimited_Returns429WithRetryAfter() { // Arrange - await using var factory = CreateFactory(permitLimit: 1, windowSeconds: 3600); + await using var factory = await CreateFactoryAsync(permitLimit: 1, windowSeconds: 3600); using var client = factory.CreateClient(); var scanId = Guid.NewGuid(); @@ -126,7 +133,7 @@ public sealed class RateLimitingTests public async Task HealthEndpoint_NotRateLimited() { // Arrange - await using var factory = CreateFactory(permitLimit: 1); + await using var factory = await CreateFactoryAsync(permitLimit: 1); using var client = factory.CreateClient(); // Act - Send multiple health requests @@ -146,7 +153,7 @@ public sealed class RateLimitingTests public async Task RateLimitedResponse_HasProblemDetails() { // Arrange - await using var factory = CreateFactory(permitLimit: 1, windowSeconds: 3600); + await using var factory = await CreateFactoryAsync(permitLimit: 1, windowSeconds: 3600); using var client = factory.CreateClient(); var scanId = Guid.NewGuid(); @@ -173,7 +180,7 @@ public sealed class RateLimitingTests // In practice, this requires setting up different auth contexts // Arrange - await using var factory = CreateFactory(); + await using var factory = await CreateFactoryAsync(); using var client = factory.CreateClient(); var scanId = Guid.NewGuid(); diff --git a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/ReachabilityDriftEndpointsTests.cs b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/ReachabilityDriftEndpointsTests.cs index d604abec3..9f18ed104 100644 --- a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/ReachabilityDriftEndpointsTests.cs +++ b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/ReachabilityDriftEndpointsTests.cs @@ -22,10 +22,11 @@ public sealed class ReachabilityDriftEndpointsTests public async Task GetDriftReturnsNotFoundWhenNoResultAndNoBaseScanProvided() { using var secrets = new TestSurfaceSecretsScope(); - using var factory = new ScannerApplicationFactory().WithOverrides(configuration => + await using var factory = new ScannerApplicationFactory().WithOverrides(configuration => { configuration["scanner:authority:enabled"] = "false"; }); + await factory.InitializeAsync(); using var client = factory.CreateClient(); @@ -41,10 +42,11 @@ public sealed class ReachabilityDriftEndpointsTests public async Task GetDriftComputesResultAndListsDriftedSinks() { using var secrets = new TestSurfaceSecretsScope(); - using var factory = new ScannerApplicationFactory().WithOverrides(configuration => + await using var factory = new ScannerApplicationFactory().WithOverrides(configuration => { configuration["scanner:authority:enabled"] = "false"; }); + await factory.InitializeAsync(); using var client = factory.CreateClient(); diff --git a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/ReportEventDispatcherTests.cs b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/ReportEventDispatcherTests.cs index b3125fd2f..54b1b0caf 100644 --- a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/ReportEventDispatcherTests.cs +++ b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/ReportEventDispatcherTests.cs @@ -13,6 +13,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using StellaOps.Auth.Abstractions; +using StellaOps.Determinism; using StellaOps.Policy; using StellaOps.Scanner.Storage.Models; using StellaOps.Scanner.Storage.Services; @@ -36,7 +37,13 @@ public sealed class ReportEventDispatcherTests { var publisher = new RecordingEventPublisher(); var tracker = new RecordingClassificationChangeTracker(); - var dispatcher = new ReportEventDispatcher(publisher, tracker, Microsoft.Extensions.Options.Options.Create(new ScannerWebServiceOptions()), TimeProvider.System, NullLogger.Instance); + var dispatcher = new ReportEventDispatcher( + publisher, + tracker, + Microsoft.Extensions.Options.Options.Create(new ScannerWebServiceOptions()), + new SequentialGuidProvider(), + TimeProvider.System, + NullLogger.Instance); var cancellationToken = TestContext.Current.CancellationToken; var request = new ReportRequestDto @@ -177,7 +184,13 @@ public sealed class ReportEventDispatcherTests { var publisher = new RecordingEventPublisher(); var tracker = new RecordingClassificationChangeTracker(); - var dispatcher = new ReportEventDispatcher(publisher, tracker, Microsoft.Extensions.Options.Options.Create(new ScannerWebServiceOptions()), TimeProvider.System, NullLogger.Instance); + var dispatcher = new ReportEventDispatcher( + publisher, + tracker, + Microsoft.Extensions.Options.Options.Create(new ScannerWebServiceOptions()), + new SequentialGuidProvider(), + TimeProvider.System, + NullLogger.Instance); var cancellationToken = TestContext.Current.CancellationToken; var request = new ReportRequestDto @@ -262,7 +275,13 @@ public sealed class ReportEventDispatcherTests { ThrowOnTrack = true }; - var dispatcher = new ReportEventDispatcher(publisher, tracker, Microsoft.Extensions.Options.Options.Create(new ScannerWebServiceOptions()), TimeProvider.System, NullLogger.Instance); + var dispatcher = new ReportEventDispatcher( + publisher, + tracker, + Microsoft.Extensions.Options.Options.Create(new ScannerWebServiceOptions()), + new SequentialGuidProvider(), + TimeProvider.System, + NullLogger.Instance); var cancellationToken = TestContext.Current.CancellationToken; var request = new ReportRequestDto @@ -333,7 +352,13 @@ public sealed class ReportEventDispatcherTests var publisher = new RecordingEventPublisher(); var tracker = new RecordingClassificationChangeTracker(); - var dispatcher = new ReportEventDispatcher(publisher, tracker, options, TimeProvider.System, NullLogger.Instance); + var dispatcher = new ReportEventDispatcher( + publisher, + tracker, + options, + new SequentialGuidProvider(), + TimeProvider.System, + NullLogger.Instance); var cancellationToken = TestContext.Current.CancellationToken; var request = new ReportRequestDto diff --git a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/ReportsEndpointsTests.cs b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/ReportsEndpointsTests.cs index c3644bf1d..2fbb7988e 100644 --- a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/ReportsEndpointsTests.cs +++ b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/ReportsEndpointsTests.cs @@ -39,7 +39,7 @@ rules: var hmacKey = Convert.ToBase64String(Encoding.UTF8.GetBytes("scanner-report-hmac-key-2025!")); - using var factory = new ScannerApplicationFactory().WithOverrides(configuration => + await using var factory = new ScannerApplicationFactory().WithOverrides(configuration => { configuration["scanner:signing:enabled"] = "true"; configuration["scanner:signing:keyId"] = "scanner-report-signing"; @@ -47,6 +47,7 @@ rules: configuration["scanner:signing:keyPem"] = hmacKey; configuration["scanner:features:enableSignedReports"] = "true"; }); + await factory.InitializeAsync(); var store = factory.Services.GetRequiredService(); await store.SaveAsync( @@ -110,7 +111,8 @@ rules: [Fact] public async Task ReportsEndpointValidatesDigest() { - using var factory = new ScannerApplicationFactory(); + await using var factory = new ScannerApplicationFactory(); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var request = new ReportRequestDto @@ -127,7 +129,8 @@ rules: [Fact] public async Task ReportsEndpointReturnsServiceUnavailableWhenPolicyMissing() { - using var factory = new ScannerApplicationFactory(); + await using var factory = new ScannerApplicationFactory(); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var request = new ReportRequestDto @@ -155,7 +158,7 @@ rules: action: block """; - using var factory = new ScannerApplicationFactory().WithOverrides( + await using var factory = new ScannerApplicationFactory().WithOverrides( configuration => { configuration["scanner:signing:enabled"] = "true"; @@ -176,6 +179,7 @@ rules: services.AddSingleton(); services.AddSingleton(sp => sp.GetRequiredService()); }); + await factory.InitializeAsync(); var store = factory.Services.GetRequiredService(); var saveResult = await store.SaveAsync( diff --git a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/RichGraphAttestationServiceTests.cs b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/RichGraphAttestationServiceTests.cs index 5e4ee2c0a..8538fe0d7 100644 --- a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/RichGraphAttestationServiceTests.cs +++ b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/RichGraphAttestationServiceTests.cs @@ -11,6 +11,7 @@ using System.Threading; using System.Threading.Tasks; using FluentAssertions; using Microsoft.Extensions.Logging.Abstractions; +using StellaOps.Determinism; using StellaOps.Scanner.WebService.Contracts; using StellaOps.Scanner.WebService.Domain; using StellaOps.Scanner.WebService.Services; @@ -28,6 +29,7 @@ public sealed class RichGraphAttestationServiceTests { private readonly FakeTimeProvider _timeProvider; private readonly RichGraphAttestationService _service; + private readonly IGuidProvider _guidProvider = new SequentialGuidProvider(); public RichGraphAttestationServiceTests() { @@ -302,7 +304,7 @@ public sealed class RichGraphAttestationServiceTests public async Task GetAttestationAsync_NonExistentAttestation_ReturnsNull() { // Act - var result = await _service.GetAttestationAsync(ScanId.New(), "nonexistent-graph"); + var result = await _service.GetAttestationAsync(ScanId.New(_guidProvider), "nonexistent-graph"); // Assert result.Should().BeNull(); @@ -317,7 +319,7 @@ public sealed class RichGraphAttestationServiceTests await _service.CreateAttestationAsync(input); // Act - var result = await _service.GetAttestationAsync(ScanId.New(), input.GraphId); + var result = await _service.GetAttestationAsync(ScanId.New(_guidProvider), input.GraphId); // Assert result.Should().BeNull(); @@ -396,7 +398,7 @@ public sealed class RichGraphAttestationServiceTests { return new RichGraphAttestationInput { - ScanId = ScanId.New(), + ScanId = ScanId.New(_guidProvider), GraphId = $"richgraph-{Guid.NewGuid():N}", GraphDigest = "sha256:abc123def456789", NodeCount = 1234, diff --git a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/RubyPackagesEndpointsTests.cs b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/RubyPackagesEndpointsTests.cs index de8021db2..acf2784fe 100644 --- a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/RubyPackagesEndpointsTests.cs +++ b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/RubyPackagesEndpointsTests.cs @@ -29,7 +29,8 @@ public sealed class RubyPackagesEndpointsTests public async Task GetRubyPackagesReturnsNotFoundWhenInventoryMissing() { using var secrets = new TestSurfaceSecretsScope(); - using var factory = new ScannerApplicationFactory(); + await using var factory = new ScannerApplicationFactory(); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var response = await client.GetAsync("/api/v1/scans/scan-ruby-missing/ruby-packages"); @@ -46,7 +47,8 @@ public sealed class RubyPackagesEndpointsTests var generatedAt = DateTime.UtcNow.AddMinutes(-10); using var secrets = new TestSurfaceSecretsScope(); - using var factory = new ScannerApplicationFactory(); + await using var factory = new ScannerApplicationFactory(); + await factory.InitializeAsync(); using (var serviceScope = factory.Services.CreateScope()) { @@ -97,7 +99,8 @@ public sealed class RubyPackagesEndpointsTests var generatedAt = DateTime.UtcNow.AddMinutes(-5); using var secrets = new TestSurfaceSecretsScope(); - using var factory = new ScannerApplicationFactory(); + await using var factory = new ScannerApplicationFactory(); + await factory.InitializeAsync(); string? scanId = null; using (var scope = factory.Services.CreateScope()) @@ -155,7 +158,8 @@ public sealed class RubyPackagesEndpointsTests const string reference = "ghcr.io/demo/ruby-service:latest"; const string digest = "sha512:abcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcd"; using var secrets = new TestSurfaceSecretsScope(); - using var factory = new ScannerApplicationFactory(); + await using var factory = new ScannerApplicationFactory(); + await factory.InitializeAsync(); string? scanId = null; using (var scope = factory.Services.CreateScope()) @@ -248,10 +252,11 @@ public sealed class RubyPackagesEndpointsTests new EntryTraceNdjsonMetadata("scan-placeholder", digest, generatedAt)); using var secrets = new TestSurfaceSecretsScope(); - using var factory = new ScannerApplicationFactory().WithOverrides(configureServices: services => + await using var factory = new ScannerApplicationFactory().WithOverrides(configureServices: services => { services.AddSingleton(); }); + await factory.InitializeAsync(); string? canonicalScanId = null; using (var scope = factory.Services.CreateScope()) diff --git a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/RuntimeEndpointsTests.cs b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/RuntimeEndpointsTests.cs index 25bedaee2..41e7f57dc 100644 --- a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/RuntimeEndpointsTests.cs +++ b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/RuntimeEndpointsTests.cs @@ -23,7 +23,8 @@ public sealed class RuntimeEndpointsTests [Fact] public async Task RuntimeEventsEndpointPersistsEvents() { - using var factory = new ScannerApplicationFactory(); + await using var factory = new ScannerApplicationFactory(); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var request = new RuntimeEventsIngestRequestDto @@ -62,7 +63,8 @@ public sealed class RuntimeEndpointsTests [Fact] public async Task RuntimeEventsEndpointRejectsUnsupportedSchema() { - using var factory = new ScannerApplicationFactory(); + await using var factory = new ScannerApplicationFactory(); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var envelope = CreateEnvelope("evt-100", schemaVersion: "zastava.runtime.event@v2.0"); @@ -80,13 +82,14 @@ public sealed class RuntimeEndpointsTests [Fact] public async Task RuntimeEventsEndpointEnforcesRateLimit() { - using var factory = new ScannerApplicationFactory().WithOverrides(configuration => + await using var factory = new ScannerApplicationFactory().WithOverrides(configuration => { configuration["scanner:runtime:perNodeBurst"] = "1"; configuration["scanner:runtime:perNodeEventsPerSecond"] = "1"; configuration["scanner:runtime:perTenantBurst"] = "1"; configuration["scanner:runtime:perTenantEventsPerSecond"] = "1"; }); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var request = new RuntimeEventsIngestRequestDto @@ -112,10 +115,11 @@ public sealed class RuntimeEndpointsTests [Fact] public async Task RuntimePolicyEndpointReturnsDecisions() { - using var factory = new ScannerApplicationFactory().WithOverrides(configuration => + await using var factory = new ScannerApplicationFactory().WithOverrides(configuration => { configuration["scanner:runtime:policyCacheTtlSeconds"] = "600"; }); + await factory.InitializeAsync(); const string imageDigest = "sha256:deadbeef"; @@ -170,20 +174,20 @@ rules: await links.UpsertAsync(new LinkDocument { - Id = Guid.NewGuid().ToString("N"), + Id = "link-0001", FromType = LinkSourceType.Image, FromDigest = imageDigest, ArtifactId = sbomArtifactId, - CreatedAtUtc = DateTime.UtcNow + CreatedAtUtc = FixedUtc }, TestContext.Current.CancellationToken); await links.UpsertAsync(new LinkDocument { - Id = Guid.NewGuid().ToString("N"), + Id = "link-0002", FromType = LinkSourceType.Image, FromDigest = imageDigest, ArtifactId = attestationArtifactId, - CreatedAtUtc = DateTime.UtcNow + CreatedAtUtc = FixedUtc }, TestContext.Current.CancellationToken); } @@ -195,7 +199,10 @@ rules: CreateEnvelope("evt-211", imageDigest: imageDigest, buildId: "1122AABBCCDDEEFF00112233445566778899AABB") } }; - var ingestResponse = await client.PostAsJsonAsync("/api/v1/runtime/events", ingestRequest); + var ingestResponse = await client.PostAsJsonAsync( + "/api/v1/runtime/events", + ingestRequest, + TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.Accepted, ingestResponse.StatusCode); var request = new RuntimePolicyRequestDto @@ -205,7 +212,10 @@ rules: Labels = new Dictionary { ["app"] = "api" } }; - var response = await client.PostAsJsonAsync("/api/v1/policy/runtime", request); + var response = await client.PostAsJsonAsync( + "/api/v1/policy/runtime", + request, + TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.OK, response.StatusCode); var raw = await response.Content.ReadAsStringAsync(); @@ -214,7 +224,7 @@ rules: Assert.True(payload is not null, $"Runtime policy response: {raw}"); Assert.Equal(600, payload!.TtlSeconds); Assert.NotNull(payload.PolicyRevision); - Assert.True(payload.ExpiresAtUtc > DateTimeOffset.UtcNow); + Assert.True(payload.ExpiresAtUtc > FixedNow); var decision = payload.Results[imageDigest]; Assert.Equal("pass", decision.PolicyVerdict); @@ -232,7 +242,6 @@ rules: Assert.NotNull(decision.BuildIds); Assert.Contains("1122aabbccddeeff00112233445566778899aabb", decision.BuildIds!); var metadataString = decision.Metadata; - Console.WriteLine($"Runtime policy metadata: {metadataString ?? ""}"); Assert.False(string.IsNullOrWhiteSpace(metadataString)); using var metadataDocument = JsonDocument.Parse(decision.Metadata!); Assert.True(metadataDocument.RootElement.TryGetProperty("heuristics", out _)); @@ -242,7 +251,8 @@ rules: [Fact] public async Task RuntimePolicyEndpointFlagsUnsignedAndMissingSbom() { - using var factory = new ScannerApplicationFactory(); + await using var factory = new ScannerApplicationFactory(); + await factory.InitializeAsync(); using var client = factory.CreateClient(); const string imageDigest = "sha256:feedface"; @@ -268,10 +278,10 @@ rules: [] { Namespace = "payments", Images = new[] { imageDigest } - }); + }, TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var payload = await response.Content.ReadFromJsonAsync(); + var payload = await response.Content.ReadFromJsonAsync(TestContext.Current.CancellationToken); Assert.NotNull(payload); var decision = payload!.Results[imageDigest]; @@ -299,7 +309,8 @@ rules: [] [Fact] public async Task RuntimePolicyEndpointValidatesRequest() { - using var factory = new ScannerApplicationFactory(); + await using var factory = new ScannerApplicationFactory(); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var request = new RuntimePolicyRequestDto @@ -307,7 +318,7 @@ rules: [] Images = Array.Empty() }; - var response = await client.PostAsJsonAsync("/api/v1/policy/runtime", request); + var response = await client.PostAsJsonAsync("/api/v1/policy/runtime", request, TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); } @@ -321,7 +332,7 @@ rules: [] var runtimeEvent = new RuntimeEvent { EventId = eventId, - When = DateTimeOffset.UtcNow, + When = FixedNow, Kind = RuntimeEventKind.ContainerStart, Tenant = "tenant-alpha", Node = "node-a", @@ -363,4 +374,7 @@ rules: [] Event = runtimeEvent }; } + + private static readonly DateTimeOffset FixedNow = new(2026, 1, 1, 0, 0, 0, TimeSpan.Zero); + private static readonly DateTime FixedUtc = new(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc); } diff --git a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/RuntimeReconciliationTests.cs b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/RuntimeReconciliationTests.cs index aa09488dc..5302823ac 100644 --- a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/RuntimeReconciliationTests.cs +++ b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/RuntimeReconciliationTests.cs @@ -30,7 +30,8 @@ public sealed class RuntimeReconciliationTests [Fact] public async Task ReconcileEndpoint_WithNoRuntimeEvents_ReturnsNotFound() { - using var factory = new ScannerApplicationFactory(); + await using var factory = new ScannerApplicationFactory(); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var request = new RuntimeReconcileRequestDto @@ -54,12 +55,13 @@ public sealed class RuntimeReconciliationTests { var mockObjectStore = new InMemoryArtifactObjectStore(); - using var factory = new ScannerApplicationFactory().WithOverrides( + await using var factory = new ScannerApplicationFactory().WithOverrides( configureServices: services => { services.RemoveAll(); services.AddSingleton(mockObjectStore); }); + await factory.InitializeAsync(); using var client = factory.CreateClient(); // Ingest runtime event with loaded libraries @@ -104,12 +106,13 @@ public sealed class RuntimeReconciliationTests { var mockObjectStore = new InMemoryArtifactObjectStore(); - using var factory = new ScannerApplicationFactory().WithOverrides( + await using var factory = new ScannerApplicationFactory().WithOverrides( configureServices: services => { services.RemoveAll(); services.AddSingleton(mockObjectStore); }); + await factory.InitializeAsync(); using var client = factory.CreateClient(); // Setup: Create SBOM artifact with components @@ -195,12 +198,13 @@ public sealed class RuntimeReconciliationTests { var mockObjectStore = new InMemoryArtifactObjectStore(); - using var factory = new ScannerApplicationFactory().WithOverrides( + await using var factory = new ScannerApplicationFactory().WithOverrides( configureServices: services => { services.RemoveAll(); services.AddSingleton(mockObjectStore); }); + await factory.InitializeAsync(); using var client = factory.CreateClient(); const string imageDigest = "sha256:pathtest123"; @@ -281,12 +285,13 @@ public sealed class RuntimeReconciliationTests { var mockObjectStore = new InMemoryArtifactObjectStore(); - using var factory = new ScannerApplicationFactory().WithOverrides( + await using var factory = new ScannerApplicationFactory().WithOverrides( configureServices: services => { services.RemoveAll(); services.AddSingleton(mockObjectStore); }); + await factory.InitializeAsync(); using var client = factory.CreateClient(); const string imageDigest = "sha256:eventidtest"; @@ -368,7 +373,8 @@ public sealed class RuntimeReconciliationTests [Fact] public async Task ReconcileEndpoint_WithNonExistentEventId_ReturnsNotFound() { - using var factory = new ScannerApplicationFactory(); + await using var factory = new ScannerApplicationFactory(); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var request = new RuntimeReconcileRequestDto @@ -390,7 +396,8 @@ public sealed class RuntimeReconciliationTests [Fact] public async Task ReconcileEndpoint_WithMissingImageDigest_ReturnsBadRequest() { - using var factory = new ScannerApplicationFactory(); + await using var factory = new ScannerApplicationFactory(); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var request = new RuntimeReconcileRequestDto @@ -409,12 +416,13 @@ public sealed class RuntimeReconciliationTests { var mockObjectStore = new InMemoryArtifactObjectStore(); - using var factory = new ScannerApplicationFactory().WithOverrides( + await using var factory = new ScannerApplicationFactory().WithOverrides( configureServices: services => { services.RemoveAll(); services.AddSingleton(mockObjectStore); }); + await factory.InitializeAsync(); using var client = factory.CreateClient(); const string imageDigest = "sha256:mixedtest"; diff --git a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/SbomEndpointsTests.cs b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/SbomEndpointsTests.cs index c1c926fc5..aa304ba3b 100644 --- a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/SbomEndpointsTests.cs +++ b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/SbomEndpointsTests.cs @@ -19,7 +19,7 @@ public sealed class SbomEndpointsTests public async Task SubmitSbomAcceptsCycloneDxJson() { using var secrets = new TestSurfaceSecretsScope(); - using var factory = new ScannerApplicationFactory().WithOverrides(configuration => + await using var factory = new ScannerApplicationFactory().WithOverrides(configuration => { configuration["scanner:authority:enabled"] = "false"; }, configureServices: services => @@ -27,6 +27,7 @@ public sealed class SbomEndpointsTests services.RemoveAll(); services.AddSingleton(new InMemoryArtifactObjectStore()); }); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var scanId = await CreateScanAsync(client); diff --git a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/SbomUploadEndpointsTests.cs b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/SbomUploadEndpointsTests.cs index de2f19939..9d6a319cb 100644 --- a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/SbomUploadEndpointsTests.cs +++ b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/SbomUploadEndpointsTests.cs @@ -18,7 +18,7 @@ public sealed class SbomUploadEndpointsTests public async Task Upload_accepts_cyclonedx_fixture_and_returns_record() { using var secrets = new TestSurfaceSecretsScope(); - using var factory = CreateFactory(); + await using var factory = await CreateFactoryAsync(); using var client = factory.CreateClient(); var request = new SbomUploadRequestDto @@ -64,7 +64,7 @@ public sealed class SbomUploadEndpointsTests public async Task Upload_accepts_spdx_fixture_and_reports_quality_score() { using var secrets = new TestSurfaceSecretsScope(); - using var factory = CreateFactory(); + await using var factory = await CreateFactoryAsync(); using var client = factory.CreateClient(); var request = new SbomUploadRequestDto @@ -90,7 +90,7 @@ public sealed class SbomUploadEndpointsTests public async Task Upload_rejects_unknown_format() { using var secrets = new TestSurfaceSecretsScope(); - using var factory = CreateFactory(); + await using var factory = await CreateFactoryAsync(); using var client = factory.CreateClient(); var invalid = new SbomUploadRequestDto @@ -103,9 +103,9 @@ public sealed class SbomUploadEndpointsTests Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); } - private static ScannerApplicationFactory CreateFactory() + private static async Task CreateFactoryAsync() { - return new ScannerApplicationFactory().WithOverrides(configuration => + var factory = new ScannerApplicationFactory().WithOverrides(configuration => { configuration["scanner:authority:enabled"] = "false"; }, configureServices: services => @@ -113,6 +113,9 @@ public sealed class SbomUploadEndpointsTests services.RemoveAll(); services.AddSingleton(new InMemoryArtifactObjectStore()); }); + + await factory.InitializeAsync(); + return factory; } private static string LoadFixtureBase64(string fileName) diff --git a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/ScannerApplicationFactory.cs b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/ScannerApplicationFactory.cs index 0d469eb8a..687cb2a60 100644 --- a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/ScannerApplicationFactory.cs +++ b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/ScannerApplicationFactory.cs @@ -21,12 +21,12 @@ using StellaOps.Scanner.Triage; using StellaOps.Determinism; using StellaOps.Scanner.WebService.Diagnostics; using StellaOps.Scanner.WebService.Services; +using Xunit; namespace StellaOps.Scanner.WebService.Tests; -public sealed class ScannerApplicationFactory : WebApplicationFactory +public sealed class ScannerApplicationFactory : WebApplicationFactory, IAsyncLifetime, IAsyncDisposable { - private readonly ScannerWebServicePostgresFixture? postgresFixture; private readonly bool skipPostgres; private readonly Dictionary configuration = new(StringComparer.OrdinalIgnoreCase) { @@ -53,6 +53,10 @@ public sealed class ScannerApplicationFactory : WebApplicationFactory>? configureConfiguration; private Action? configureServices; private bool useTestAuthentication; + private ScannerWebServicePostgresFixture? postgresFixture; + private Task? initializationTask; + private bool initialized; + private bool disposed; public ScannerApplicationFactory() : this(skipPostgres: false) { @@ -61,25 +65,16 @@ public sealed class ScannerApplicationFactory : WebApplicationFactory @@ -109,8 +104,62 @@ public sealed class ScannerApplicationFactory : WebApplicationFactory DisposeAsync().AsTask(); + + public async ValueTask DisposeAsync() + { + if (disposed) + { + return; + } + + disposed = true; + base.Dispose(); + + if (postgresFixture is not null) + { + await postgresFixture.DisposeAsync(); + } + } + protected override void ConfigureWebHost(IWebHostBuilder builder) { + if (!initialized) + { + throw new InvalidOperationException("ScannerApplicationFactory must be initialized via InitializeAsync before use."); + } + configureConfiguration?.Invoke(configuration); builder.UseEnvironment("Testing"); @@ -200,16 +249,6 @@ public sealed class ScannerApplicationFactory : WebApplicationFactory RunAllAsync( diff --git a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/ScannerApplicationFixture.cs b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/ScannerApplicationFixture.cs index 7418a26b8..e22718cac 100644 --- a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/ScannerApplicationFixture.cs +++ b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/ScannerApplicationFixture.cs @@ -1,10 +1,11 @@ -using System; using System.Net.Http; using System.Net.Http.Headers; +using System.Threading.Tasks; +using Xunit; namespace StellaOps.Scanner.WebService.Tests; -public sealed class ScannerApplicationFixture : IDisposable +public sealed class ScannerApplicationFixture : IAsyncLifetime { private ScannerApplicationFactory? _authenticatedFactory; @@ -22,10 +23,12 @@ public sealed class ScannerApplicationFixture : IDisposable return client; } - public void Dispose() + public Task InitializeAsync() => Factory.InitializeAsync(); + + public async Task DisposeAsync() { - _authenticatedFactory?.Dispose(); - Factory.Dispose(); + _authenticatedFactory = null; + await Factory.DisposeAsync(); } } diff --git a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/ScannerSurfaceSecretConfiguratorTests.cs b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/ScannerSurfaceSecretConfiguratorTests.cs index 41df0e5c5..723ef2e60 100644 --- a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/ScannerSurfaceSecretConfiguratorTests.cs +++ b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/ScannerSurfaceSecretConfiguratorTests.cs @@ -177,6 +177,16 @@ public sealed class ScannerSurfaceSecretConfiguratorTests _handles = handles ?? throw new ArgumentNullException(nameof(handles)); } + public SurfaceSecretHandle Get(SurfaceSecretRequest request) + { + if (_handles.TryGetValue(request.SecretType, out var handle)) + { + return handle; + } + + throw new SurfaceSecretNotFoundException(request); + } + public ValueTask GetAsync(SurfaceSecretRequest request, CancellationToken cancellationToken = default) { if (_handles.TryGetValue(request.SecretType, out var handle)) diff --git a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/ScansEndpointsTests.Entropy.cs b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/ScansEndpointsTests.Entropy.cs index 1cb7570be..e16169ad1 100644 --- a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/ScansEndpointsTests.Entropy.cs +++ b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/ScansEndpointsTests.Entropy.cs @@ -16,11 +16,12 @@ public sealed partial class ScansEndpointsTests public async Task EntropyEndpoint_AttachesSnapshot_AndSurfacesInStatus() { using var secrets = new TestSurfaceSecretsScope(); - using var factory = new ScannerApplicationFactory().WithOverrides(cfg => + await using var factory = new ScannerApplicationFactory().WithOverrides(cfg => { cfg["scanner:authority:enabled"] = "false"; cfg["scanner:authority:allowAnonymousFallback"] = "true"; }); + await factory.InitializeAsync(); using var client = factory.CreateClient(); diff --git a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/ScansEndpointsTests.RecordMode.cs b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/ScansEndpointsTests.RecordMode.cs index 845ab7f28..59a15c83a 100644 --- a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/ScansEndpointsTests.RecordMode.cs +++ b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/ScansEndpointsTests.RecordMode.cs @@ -27,7 +27,7 @@ public sealed partial class ScansEndpointsTests using var secrets = new TestSurfaceSecretsScope(); var store = new InMemoryArtifactObjectStore(); - using var factory = new ScannerApplicationFactory().WithOverrides(configureConfiguration: cfg => + await using var factory = new ScannerApplicationFactory().WithOverrides(configureConfiguration: cfg => { cfg["scanner:artifactStore:bucket"] = "replay-bucket"; }, @@ -36,6 +36,7 @@ public sealed partial class ScansEndpointsTests services.RemoveAll(); services.AddSingleton(store); }); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var submit = await client.PostAsJsonAsync("/api/v1/scans", new { image = new { digest = "sha256:demo" } }, TestContext.Current.CancellationToken); diff --git a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/ScansEndpointsTests.Replay.cs b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/ScansEndpointsTests.Replay.cs index 0a3bb9365..00ba122f8 100644 --- a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/ScansEndpointsTests.Replay.cs +++ b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/ScansEndpointsTests.Replay.cs @@ -21,10 +21,11 @@ public sealed partial class ScansEndpointsTests public async Task RecordModeService_AttachesReplayAndSurfacedInStatus() { using var secrets = new TestSurfaceSecretsScope(); - using var factory = new ScannerApplicationFactory().WithOverrides(cfg => + await using var factory = new ScannerApplicationFactory().WithOverrides(cfg => { cfg["scanner:authority:enabled"] = "false"; }); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var submitResponse = await client.PostAsJsonAsync("/api/v1/scans", new diff --git a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/ScansEndpointsTests.cs b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/ScansEndpointsTests.cs index 49ec3d618..080f2e5f1 100644 --- a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/ScansEndpointsTests.cs +++ b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/ScansEndpointsTests.cs @@ -25,7 +25,8 @@ public sealed partial class ScansEndpointsTests public async Task SubmitScanValidatesImageDescriptor() { using var secrets = new TestSurfaceSecretsScope(); - using var factory = new ScannerApplicationFactory(); + await using var factory = new ScannerApplicationFactory(); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var response = await client.PostAsJsonAsync("/api/v1/scans", new @@ -43,7 +44,7 @@ public sealed partial class ScansEndpointsTests using var secrets = new TestSurfaceSecretsScope(); RecordingCoordinator coordinator = null!; - using var factory = new ScannerApplicationFactory().WithOverrides(configuration => + await using var factory = new ScannerApplicationFactory().WithOverrides(configuration => { configuration["scanner:authority:enabled"] = "false"; }, configureServices: services => @@ -57,6 +58,7 @@ public sealed partial class ScansEndpointsTests return coordinator; }); }); + await factory.InitializeAsync(); using var client = factory.CreateClient(new WebApplicationFactoryClientOptions { @@ -83,7 +85,7 @@ public sealed partial class ScansEndpointsTests using var secrets = new TestSurfaceSecretsScope(); RecordingCoordinator coordinator = null!; - using var factory = new ScannerApplicationFactory().WithOverrides(configuration => + await using var factory = new ScannerApplicationFactory().WithOverrides(configuration => { configuration["scanner:determinism:feedSnapshotId"] = "feed-2025-11-26"; configuration["scanner:determinism:policySnapshotId"] = "rev-42"; @@ -98,6 +100,7 @@ public sealed partial class ScansEndpointsTests return coordinator; }); }); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var request = new ScanSubmitRequest @@ -155,10 +158,11 @@ public sealed partial class ScansEndpointsTests var ndjson = EntryTraceNdjsonWriter.Serialize(graph, new EntryTraceNdjsonMetadata(scanId, "sha256:test", generatedAt)); var storedResult = new EntryTraceResult(scanId, "sha256:test", generatedAt, graph, ndjson); - using var factory = new ScannerApplicationFactory().WithOverrides(configureServices: services => + await using var factory = new ScannerApplicationFactory().WithOverrides(configureServices: services => { services.AddSingleton(new StubEntryTraceResultStore(storedResult)); }); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var response = await client.GetAsync($"/api/v1/scans/{scanId}/entrytrace"); @@ -176,10 +180,11 @@ public sealed partial class ScansEndpointsTests public async Task GetEntryTraceReturnsNotFoundWhenMissing() { using var secrets = new TestSurfaceSecretsScope(); - using var factory = new ScannerApplicationFactory().WithOverrides(configureServices: services => + await using var factory = new ScannerApplicationFactory().WithOverrides(configureServices: services => { services.AddSingleton(new StubEntryTraceResultStore(null)); }); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var response = await client.GetAsync("/api/v1/scans/scan-missing/entrytrace"); diff --git a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/ScoreReplayEndpointsTests.cs b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/ScoreReplayEndpointsTests.cs index e2346ea72..39ffeb7fc 100644 --- a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/ScoreReplayEndpointsTests.cs +++ b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/ScoreReplayEndpointsTests.cs @@ -19,13 +19,13 @@ namespace StellaOps.Scanner.WebService.Tests; /// [Trait("Category", "Integration")] [Trait("Sprint", "3401.0002")] -public sealed class ScoreReplayEndpointsTests : IDisposable +public sealed class ScoreReplayEndpointsTests : IAsyncLifetime { - private readonly TestSurfaceSecretsScope _secrets; - private readonly ScannerApplicationFactory _factory; - private readonly HttpClient _client; + private TestSurfaceSecretsScope _secrets = null!; + private ScannerApplicationFactory _factory = null!; + private HttpClient _client = null!; - public ScoreReplayEndpointsTests() + public async Task InitializeAsync() { _secrets = new TestSurfaceSecretsScope(); _factory = new ScannerApplicationFactory().WithOverrides(cfg => @@ -33,13 +33,14 @@ public sealed class ScoreReplayEndpointsTests : IDisposable cfg["scanner:authority:enabled"] = "false"; cfg["scanner:scoreReplay:enabled"] = "true"; }); + await _factory.InitializeAsync(); _client = _factory.CreateClient(); } - public void Dispose() + public async Task DisposeAsync() { _client.Dispose(); - _factory.Dispose(); + await _factory.DisposeAsync(); _secrets.Dispose(); } diff --git a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/Security/ScannerAuthorizationTests.cs b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/Security/ScannerAuthorizationTests.cs index 7dbb0b6a7..be57b40ef 100644 --- a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/Security/ScannerAuthorizationTests.cs +++ b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/Security/ScannerAuthorizationTests.cs @@ -33,8 +33,9 @@ public sealed class ScannerAuthorizationTests [InlineData("/api/v1/sbom/upload")] public async Task ProtectedPostEndpoints_RequireAuthentication(string endpoint) { - using var factory = new ScannerApplicationFactory().WithOverrides( + await using var factory = new ScannerApplicationFactory().WithOverrides( useTestAuthentication: true); + await factory.InitializeAsync(); using var client = factory.CreateClient(); @@ -59,7 +60,8 @@ public sealed class ScannerAuthorizationTests [InlineData("/readyz")] public async Task HealthEndpoints_ArePubliclyAccessible(string endpoint) { - using var factory = new ScannerApplicationFactory(); + await using var factory = new ScannerApplicationFactory(); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var response = await client.GetAsync(endpoint, TestContext.Current.CancellationToken); @@ -81,8 +83,9 @@ public sealed class ScannerAuthorizationTests [Fact] public async Task ExpiredToken_IsRejected() { - using var factory = new ScannerApplicationFactory().WithOverrides( + await using var factory = new ScannerApplicationFactory().WithOverrides( useTestAuthentication: true); + await factory.InitializeAsync(); using var client = factory.CreateClient(); @@ -111,8 +114,9 @@ public sealed class ScannerAuthorizationTests [InlineData("Bearer only-one-part")] public async Task MalformedToken_IsRejected(string token) { - using var factory = new ScannerApplicationFactory().WithOverrides( + await using var factory = new ScannerApplicationFactory().WithOverrides( useTestAuthentication: true); + await factory.InitializeAsync(); using var client = factory.CreateClient(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); @@ -134,8 +138,9 @@ public sealed class ScannerAuthorizationTests [Fact] public async Task TokenWithWrongIssuer_IsRejected() { - using var factory = new ScannerApplicationFactory().WithOverrides( + await using var factory = new ScannerApplicationFactory().WithOverrides( useTestAuthentication: true); + await factory.InitializeAsync(); using var client = factory.CreateClient(); @@ -160,8 +165,9 @@ public sealed class ScannerAuthorizationTests [Fact] public async Task TokenWithWrongAudience_IsRejected() { - using var factory = new ScannerApplicationFactory().WithOverrides( + await using var factory = new ScannerApplicationFactory().WithOverrides( useTestAuthentication: true); + await factory.InitializeAsync(); using var client = factory.CreateClient(); @@ -190,7 +196,8 @@ public sealed class ScannerAuthorizationTests [Fact] public async Task AnonymousFallback_AllowsAccess_WhenNoAuthConfigured() { - using var factory = new ScannerApplicationFactory(); + await using var factory = new ScannerApplicationFactory(); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var response = await client.GetAsync("/healthz", TestContext.Current.CancellationToken); @@ -208,8 +215,9 @@ public sealed class ScannerAuthorizationTests [Fact] public async Task AnonymousFallback_DeniesAccess_WhenAuthRequired() { - using var factory = new ScannerApplicationFactory().WithOverrides( + await using var factory = new ScannerApplicationFactory().WithOverrides( useTestAuthentication: true); + await factory.InitializeAsync(); using var client = factory.CreateClient(); @@ -234,8 +242,9 @@ public sealed class ScannerAuthorizationTests [Fact] public async Task WriteOperations_RequireAuthentication() { - using var factory = new ScannerApplicationFactory().WithOverrides( + await using var factory = new ScannerApplicationFactory().WithOverrides( useTestAuthentication: true); + await factory.InitializeAsync(); using var client = factory.CreateClient(); @@ -256,8 +265,9 @@ public sealed class ScannerAuthorizationTests [Fact] public async Task DeleteOperations_RequireAuthentication() { - using var factory = new ScannerApplicationFactory().WithOverrides( + await using var factory = new ScannerApplicationFactory().WithOverrides( useTestAuthentication: true); + await factory.InitializeAsync(); using var client = factory.CreateClient(); @@ -281,7 +291,8 @@ public sealed class ScannerAuthorizationTests [Fact] public async Task RequestWithoutTenant_IsHandledAppropriately() { - using var factory = new ScannerApplicationFactory(); + await using var factory = new ScannerApplicationFactory(); + await factory.InitializeAsync(); using var client = factory.CreateClient(); // Request without tenant header - use health endpoint @@ -305,7 +316,8 @@ public sealed class ScannerAuthorizationTests [Fact] public async Task Responses_ContainSecurityHeaders() { - using var factory = new ScannerApplicationFactory(); + await using var factory = new ScannerApplicationFactory(); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var response = await client.GetAsync("/healthz", TestContext.Current.CancellationToken); @@ -321,7 +333,8 @@ public sealed class ScannerAuthorizationTests [Fact] public async Task Cors_IsProperlyConfigured() { - using var factory = new ScannerApplicationFactory(); + await using var factory = new ScannerApplicationFactory(); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var request = new HttpRequestMessage(HttpMethod.Options, "/healthz"); @@ -349,8 +362,9 @@ public sealed class ScannerAuthorizationTests [Fact] public async Task ValidToken_IsAccepted() { - using var factory = new ScannerApplicationFactory().WithOverrides( + await using var factory = new ScannerApplicationFactory().WithOverrides( useTestAuthentication: true); + await factory.InitializeAsync(); using var client = factory.CreateClient(); diff --git a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/SurfaceManifestStoreOptionsConfiguratorTests.cs b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/SurfaceManifestStoreOptionsConfiguratorTests.cs index b6a542cb1..95bbcbc5d 100644 --- a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/SurfaceManifestStoreOptionsConfiguratorTests.cs +++ b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/SurfaceManifestStoreOptionsConfiguratorTests.cs @@ -17,7 +17,7 @@ public sealed class SurfaceManifestStoreOptionsConfiguratorTests [Fact] public void Configure_UsesSurfaceEnvironmentAndCacheRoot() { - var cacheRoot = new DirectoryInfo(Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"))); + var cacheRoot = new DirectoryInfo(Path.Combine(Path.GetTempPath(), "stellaops-tests", "surface-manifest")); var settings = new SurfaceEnvironmentSettings( new Uri("https://surface.example"), "surface-bucket", @@ -30,7 +30,7 @@ public sealed class SurfaceManifestStoreOptionsConfiguratorTests "tenant-a", new SurfaceTlsConfiguration(null, null, new X509Certificate2Collection())) { - CreatedAtUtc = DateTimeOffset.UtcNow + CreatedAtUtc = FixedNow }; var environment = new StubSurfaceEnvironment(settings); @@ -45,6 +45,8 @@ public sealed class SurfaceManifestStoreOptionsConfiguratorTests Assert.Equal(Path.Combine(cacheRoot.FullName, "manifests"), options.RootDirectory); } + private static readonly DateTimeOffset FixedNow = new(2026, 1, 1, 0, 0, 0, TimeSpan.Zero); + private sealed class StubSurfaceEnvironment : ISurfaceEnvironment { public StubSurfaceEnvironment(SurfaceEnvironmentSettings settings) diff --git a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/TriageStatusEndpointsTests.cs b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/TriageStatusEndpointsTests.cs index 92c43c79e..91cc9e4ba 100644 --- a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/TriageStatusEndpointsTests.cs +++ b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/TriageStatusEndpointsTests.cs @@ -24,7 +24,8 @@ public sealed class TriageStatusEndpointsTests [Fact] public async Task GetFindingStatus_NotFound_ReturnsNotFound() { - using var factory = new ScannerApplicationFactory(); + await using var factory = new ScannerApplicationFactory(); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var response = await client.GetAsync("/api/v1/triage/findings/nonexistent-finding"); @@ -35,7 +36,8 @@ public sealed class TriageStatusEndpointsTests [Fact] public async Task PostUpdateStatus_ValidRequest_ReturnsUpdatedStatus() { - using var factory = new ScannerApplicationFactory(); + await using var factory = new ScannerApplicationFactory(); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var request = new UpdateTriageStatusRequestDto @@ -54,7 +56,8 @@ public sealed class TriageStatusEndpointsTests [Fact] public async Task PostVexStatement_ValidRequest_ReturnsResponse() { - using var factory = new ScannerApplicationFactory(); + await using var factory = new ScannerApplicationFactory(); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var request = new SubmitVexStatementRequestDto @@ -73,7 +76,8 @@ public sealed class TriageStatusEndpointsTests [Fact] public async Task PostQuery_EmptyFilters_ReturnsResults() { - using var factory = new ScannerApplicationFactory(); + await using var factory = new ScannerApplicationFactory(); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var request = new BulkTriageQueryRequestDto @@ -94,7 +98,8 @@ public sealed class TriageStatusEndpointsTests [Fact] public async Task PostQuery_WithLaneFilter_FiltersCorrectly() { - using var factory = new ScannerApplicationFactory(); + await using var factory = new ScannerApplicationFactory(); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var request = new BulkTriageQueryRequestDto @@ -114,7 +119,8 @@ public sealed class TriageStatusEndpointsTests [Fact] public async Task PostQuery_WithVerdictFilter_FiltersCorrectly() { - using var factory = new ScannerApplicationFactory(); + await using var factory = new ScannerApplicationFactory(); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var request = new BulkTriageQueryRequestDto @@ -134,7 +140,8 @@ public sealed class TriageStatusEndpointsTests [Fact] public async Task GetSummary_ValidDigest_ReturnsSummary() { - using var factory = new ScannerApplicationFactory(); + await using var factory = new ScannerApplicationFactory(); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var response = await client.GetAsync("/api/v1/triage/summary?artifactDigest=sha256:artifact123"); @@ -150,7 +157,8 @@ public sealed class TriageStatusEndpointsTests [Fact] public async Task GetSummary_IncludesAllLanes() { - using var factory = new ScannerApplicationFactory(); + await using var factory = new ScannerApplicationFactory(); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var response = await client.GetAsync("/api/v1/triage/summary?artifactDigest=sha256:artifact123"); @@ -168,7 +176,8 @@ public sealed class TriageStatusEndpointsTests [Fact] public async Task GetSummary_IncludesAllVerdicts() { - using var factory = new ScannerApplicationFactory(); + await using var factory = new ScannerApplicationFactory(); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var response = await client.GetAsync("/api/v1/triage/summary?artifactDigest=sha256:artifact123"); @@ -186,7 +195,8 @@ public sealed class TriageStatusEndpointsTests [Fact] public async Task PostQuery_ResponseIncludesSummary() { - using var factory = new ScannerApplicationFactory(); + await using var factory = new ScannerApplicationFactory(); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var request = new BulkTriageQueryRequestDto diff --git a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/UnifiedEvidenceServiceTests.cs b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/UnifiedEvidenceServiceTests.cs index 80ed0c63c..d3c0825ba 100644 --- a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/UnifiedEvidenceServiceTests.cs +++ b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/UnifiedEvidenceServiceTests.cs @@ -185,7 +185,7 @@ public sealed class UnifiedEvidenceServiceTests DeltaId = "delta-101", PreviousScanId = "scan-099", CurrentScanId = "scan-100", - ComparedAt = DateTimeOffset.UtcNow, + ComparedAt = FixedNow, Summary = new DeltaSummaryDto { AddedCount = 5, @@ -306,7 +306,7 @@ public sealed class UnifiedEvidenceServiceTests ComponentPurl = "pkg:npm/test@1.0.0", Manifests = CreateMinimalManifests(), Verification = CreateMinimalVerification(), - GeneratedAt = DateTimeOffset.UtcNow, + GeneratedAt = FixedNow, // All tabs null Sbom = null, Reachability = null, @@ -346,7 +346,7 @@ public sealed class UnifiedEvidenceServiceTests Status = "affected", TrustScore = 1.0, MeetsPolicyThreshold = true, - IssuedAt = DateTimeOffset.UtcNow + IssuedAt = FixedNow }, new VexClaimDto { @@ -355,7 +355,7 @@ public sealed class UnifiedEvidenceServiceTests Status = "not_affected", TrustScore = 0.95, MeetsPolicyThreshold = true, - IssuedAt = DateTimeOffset.UtcNow.AddDays(-1) + IssuedAt = FixedNow.AddDays(-1) }, new VexClaimDto { @@ -364,12 +364,12 @@ public sealed class UnifiedEvidenceServiceTests Status = "under_investigation", TrustScore = 0.6, MeetsPolicyThreshold = false, - IssuedAt = DateTimeOffset.UtcNow.AddDays(-7) + IssuedAt = FixedNow.AddDays(-7) } }, Manifests = CreateMinimalManifests(), Verification = CreateMinimalVerification(), - GeneratedAt = DateTimeOffset.UtcNow + GeneratedAt = FixedNow }; // Assert @@ -429,7 +429,7 @@ public sealed class UnifiedEvidenceServiceTests EvidenceBundleUrl = "https://api.stellaops.local/bundles/bundle-123", Manifests = CreateMinimalManifests(), Verification = CreateMinimalVerification(), - GeneratedAt = DateTimeOffset.UtcNow + GeneratedAt = FixedNow }; // Assert @@ -455,7 +455,7 @@ public sealed class UnifiedEvidenceServiceTests AttestationsVerified = true, EvidenceComplete = true, Issues = null, - VerifiedAt = DateTimeOffset.UtcNow + VerifiedAt = FixedNow }; // Assert @@ -478,7 +478,7 @@ public sealed class UnifiedEvidenceServiceTests AttestationsVerified = false, EvidenceComplete = true, Issues = new[] { "Attestation signature verification failed" }, - VerifiedAt = DateTimeOffset.UtcNow + VerifiedAt = FixedNow }; // Assert @@ -505,7 +505,7 @@ public sealed class UnifiedEvidenceServiceTests "Attestation not found", "VEX evidence missing" }, - VerifiedAt = DateTimeOffset.UtcNow + VerifiedAt = FixedNow }; // Assert @@ -747,7 +747,7 @@ public sealed class UnifiedEvidenceServiceTests EvidenceBundleUrl = "https://api.stellaops.local/bundles/scan-001-finding-001", Manifests = CreateMinimalManifests(), Verification = CreateMinimalVerification(), - GeneratedAt = DateTimeOffset.UtcNow + GeneratedAt = FixedNow }; // Assert @@ -809,7 +809,7 @@ public sealed class UnifiedEvidenceServiceTests Status = "not_affected", TrustScore = 0.95, MeetsPolicyThreshold = true, - IssuedAt = DateTimeOffset.UtcNow + IssuedAt = FixedNow } }, Attestations = new[] @@ -827,7 +827,7 @@ public sealed class UnifiedEvidenceServiceTests DeltaId = "delta-001", PreviousScanId = "scan-099", CurrentScanId = "scan-100", - ComparedAt = DateTimeOffset.UtcNow + ComparedAt = FixedNow }, Policy = new PolicyEvidenceDto { @@ -838,7 +838,7 @@ public sealed class UnifiedEvidenceServiceTests Manifests = CreateMinimalManifests(), Verification = CreateMinimalVerification(), ReplayCommand = "stellaops replay --target pkg:npm/lodash@4.17.21 --verify", - GeneratedAt = DateTimeOffset.UtcNow + GeneratedAt = FixedNow }; private static string DetermineVerificationStatus( @@ -852,6 +852,8 @@ public sealed class UnifiedEvidenceServiceTests } #endregion + + private static readonly DateTimeOffset FixedNow = new(2026, 1, 1, 0, 0, 0, TimeSpan.Zero); } /// diff --git a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/VexGateEndpointsTests.cs b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/VexGateEndpointsTests.cs index 158307d79..524b11f0f 100644 --- a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/VexGateEndpointsTests.cs +++ b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/VexGateEndpointsTests.cs @@ -24,12 +24,13 @@ public sealed class VexGateEndpointsTests [Fact] public async Task GetGatePolicy_ReturnsPolicy() { - using var factory = new ScannerApplicationFactory() + await using var factory = new ScannerApplicationFactory() .WithOverrides(configureServices: services => { services.RemoveAll(); services.AddSingleton(); }); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var response = await client.GetAsync($"{BasePath}/gate-policy"); @@ -44,12 +45,13 @@ public sealed class VexGateEndpointsTests [Fact] public async Task GetGatePolicy_WithTenantId_ReturnsPolicy() { - using var factory = new ScannerApplicationFactory() + await using var factory = new ScannerApplicationFactory() .WithOverrides(configureServices: services => { services.RemoveAll(); services.AddSingleton(); }); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var response = await client.GetAsync($"{BasePath}/gate-policy?tenantId=tenant-a"); @@ -62,12 +64,13 @@ public sealed class VexGateEndpointsTests [Fact] public async Task GetGateResults_WhenScanNotFound_Returns404() { - using var factory = new ScannerApplicationFactory() + await using var factory = new ScannerApplicationFactory() .WithOverrides(configureServices: services => { services.RemoveAll(); services.AddSingleton(); }); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var response = await client.GetAsync($"{BasePath}/scan-not-exists/gate-results"); @@ -78,16 +81,17 @@ public sealed class VexGateEndpointsTests [Fact] public async Task GetGateResults_WhenScanExists_ReturnsResults() { - var scanId = "scan-" + Guid.NewGuid().ToString("N"); + var scanId = "scan-0001"; var mockService = new InMemoryVexGateQueryService(); mockService.AddScanResult(scanId, CreateTestGateResults(scanId)); - using var factory = new ScannerApplicationFactory() + await using var factory = new ScannerApplicationFactory() .WithOverrides(configureServices: services => { services.RemoveAll(); services.AddSingleton(mockService); }); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var response = await client.GetAsync($"{BasePath}/{scanId}/gate-results"); @@ -103,16 +107,17 @@ public sealed class VexGateEndpointsTests [Fact] public async Task GetGateResults_WithDecisionFilter_ReturnsFilteredResults() { - var scanId = "scan-" + Guid.NewGuid().ToString("N"); + var scanId = "scan-0002"; var mockService = new InMemoryVexGateQueryService(); mockService.AddScanResult(scanId, CreateTestGateResults(scanId, blockedCount: 3, warnCount: 5, passCount: 10)); - using var factory = new ScannerApplicationFactory() + await using var factory = new ScannerApplicationFactory() .WithOverrides(configureServices: services => { services.RemoveAll(); services.AddSingleton(mockService); }); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var response = await client.GetAsync($"{BasePath}/{scanId}/gate-results?decision=Block"); @@ -126,12 +131,13 @@ public sealed class VexGateEndpointsTests [Fact] public async Task GetGateSummary_WhenScanNotFound_Returns404() { - using var factory = new ScannerApplicationFactory() + await using var factory = new ScannerApplicationFactory() .WithOverrides(configureServices: services => { services.RemoveAll(); services.AddSingleton(); }); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var response = await client.GetAsync($"{BasePath}/scan-not-exists/gate-summary"); @@ -142,16 +148,17 @@ public sealed class VexGateEndpointsTests [Fact] public async Task GetGateSummary_WhenScanExists_ReturnsSummary() { - var scanId = "scan-" + Guid.NewGuid().ToString("N"); + var scanId = "scan-0003"; var mockService = new InMemoryVexGateQueryService(); mockService.AddScanResult(scanId, CreateTestGateResults(scanId, blockedCount: 2, warnCount: 8, passCount: 40)); - using var factory = new ScannerApplicationFactory() + await using var factory = new ScannerApplicationFactory() .WithOverrides(configureServices: services => { services.RemoveAll(); services.AddSingleton(mockService); }); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var response = await client.GetAsync($"{BasePath}/{scanId}/gate-summary"); @@ -168,12 +175,13 @@ public sealed class VexGateEndpointsTests [Fact] public async Task GetBlockedFindings_WhenScanNotFound_Returns404() { - using var factory = new ScannerApplicationFactory() + await using var factory = new ScannerApplicationFactory() .WithOverrides(configureServices: services => { services.RemoveAll(); services.AddSingleton(); }); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var response = await client.GetAsync($"{BasePath}/scan-not-exists/gate-blocked"); @@ -184,16 +192,17 @@ public sealed class VexGateEndpointsTests [Fact] public async Task GetBlockedFindings_WhenScanExists_ReturnsOnlyBlocked() { - var scanId = "scan-" + Guid.NewGuid().ToString("N"); + var scanId = "scan-0004"; var mockService = new InMemoryVexGateQueryService(); mockService.AddScanResult(scanId, CreateTestGateResults(scanId, blockedCount: 5, warnCount: 10, passCount: 20)); - using var factory = new ScannerApplicationFactory() + await using var factory = new ScannerApplicationFactory() .WithOverrides(configureServices: services => { services.RemoveAll(); services.AddSingleton(mockService); }); + await factory.InitializeAsync(); using var client = factory.CreateClient(); var response = await client.GetAsync($"{BasePath}/{scanId}/gate-blocked"); @@ -238,7 +247,7 @@ public sealed class VexGateEndpointsTests Passed = passCount, Warned = warnCount, Blocked = blockedCount, - EvaluatedAt = DateTimeOffset.UtcNow, + EvaluatedAt = FixedNow, }, GatedFindings = findings, }; @@ -248,7 +257,7 @@ public sealed class VexGateEndpointsTests { return new GatedFindingDto { - FindingId = $"finding-{Guid.NewGuid():N}", + FindingId = $"finding-{cve.ToLowerInvariant()}", Cve = cve, Purl = purl, Decision = decision, @@ -269,6 +278,8 @@ public sealed class VexGateEndpointsTests }, }; } + + private static readonly DateTimeOffset FixedNow = new(2026, 1, 1, 0, 0, 0, TimeSpan.Zero); } /// diff --git a/src/Scanner/__Tests/StellaOps.Scanner.Worker.Tests/EntryTraceExecutionServiceTests.cs b/src/Scanner/__Tests/StellaOps.Scanner.Worker.Tests/EntryTraceExecutionServiceTests.cs index 98f7396c2..841c2345a 100644 --- a/src/Scanner/__Tests/StellaOps.Scanner.Worker.Tests/EntryTraceExecutionServiceTests.cs +++ b/src/Scanner/__Tests/StellaOps.Scanner.Worker.Tests/EntryTraceExecutionServiceTests.cs @@ -486,6 +486,22 @@ public sealed class EntryTraceExecutionServiceTests : IDisposable _throwOnMissing = throwOnMissing; } + public SurfaceSecretHandle Get(SurfaceSecretRequest request) + { + var key = (request.SecretType, request.Name ?? string.Empty); + if (_secrets.TryGetValue(key, out var payload)) + { + return SurfaceSecretHandle.FromBytes(payload); + } + + if (_throwOnMissing) + { + throw new SurfaceSecretNotFoundException(request); + } + + return SurfaceSecretHandle.Empty; + } + public ValueTask GetAsync(SurfaceSecretRequest request, CancellationToken cancellationToken = default) { var key = (request.SecretType, request.Name ?? string.Empty); diff --git a/src/Scanner/__Tests/StellaOps.Scanner.Worker.Tests/RegistrySecretStageExecutorTests.cs b/src/Scanner/__Tests/StellaOps.Scanner.Worker.Tests/RegistrySecretStageExecutorTests.cs index 9fe08ba56..60db98477 100644 --- a/src/Scanner/__Tests/StellaOps.Scanner.Worker.Tests/RegistrySecretStageExecutorTests.cs +++ b/src/Scanner/__Tests/StellaOps.Scanner.Worker.Tests/RegistrySecretStageExecutorTests.cs @@ -145,6 +145,12 @@ public sealed class RegistrySecretStageExecutorTests _json = json; } + public SurfaceSecretHandle Get(SurfaceSecretRequest request) + { + var bytes = Encoding.UTF8.GetBytes(_json); + return SurfaceSecretHandle.FromBytes(bytes); + } + public ValueTask GetAsync(SurfaceSecretRequest request, CancellationToken cancellationToken = default) { var bytes = Encoding.UTF8.GetBytes(_json); @@ -154,6 +160,9 @@ public sealed class RegistrySecretStageExecutorTests private sealed class MissingSecretProvider : ISurfaceSecretProvider { + public SurfaceSecretHandle Get(SurfaceSecretRequest request) + => throw new SurfaceSecretNotFoundException(request); + public ValueTask GetAsync(SurfaceSecretRequest request, CancellationToken cancellationToken = default) => throw new SurfaceSecretNotFoundException(request); } diff --git a/src/Scanner/__Tests/StellaOps.Scanner.Worker.Tests/ScannerStorageSurfaceSecretConfiguratorTests.cs b/src/Scanner/__Tests/StellaOps.Scanner.Worker.Tests/ScannerStorageSurfaceSecretConfiguratorTests.cs index d6b5a421e..9cb713a9d 100644 --- a/src/Scanner/__Tests/StellaOps.Scanner.Worker.Tests/ScannerStorageSurfaceSecretConfiguratorTests.cs +++ b/src/Scanner/__Tests/StellaOps.Scanner.Worker.Tests/ScannerStorageSurfaceSecretConfiguratorTests.cs @@ -63,6 +63,8 @@ public sealed class ScannerStorageSurfaceSecretConfiguratorTests _handle = handle; } + public SurfaceSecretHandle Get(SurfaceSecretRequest request) => _handle; + public ValueTask GetAsync(SurfaceSecretRequest request, CancellationToken cancellationToken = default) => ValueTask.FromResult(_handle); } diff --git a/src/StellaOps.sln b/src/StellaOps.sln index 153fe31a3..a95cc805a 100644 --- a/src/StellaOps.sln +++ b/src/StellaOps.sln @@ -3937,6 +3937,18 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Doctor", "Doctor", "{D5C64D EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Doctor.WebService", "Doctor\StellaOps.Doctor.WebService\StellaOps.Doctor.WebService.csproj", "{1EA151EF-9BD9-49F4-A4CF-05FDCCB4D6E6}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "__Plugins", "__Plugins", "{02ABD53F-AAAC-E75D-834F-C95FE948E79A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Doctor.Plugin.Notify", "Doctor\__Plugins\StellaOps.Doctor.Plugin.Notify\StellaOps.Doctor.Plugin.Notify.csproj", "{C2F5114A-FE2A-402A-9DBC-73FE2AC71A6D}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "__Tests", "__Tests", "{461AAFBE-976B-9A92-CA4C-52298A5190FF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Doctor.Plugin.Notify.Tests", "Doctor\__Tests\StellaOps.Doctor.Plugin.Notify.Tests\StellaOps.Doctor.Plugin.Notify.Tests.csproj", "{237FDAA1-4852-4F6B-B6D6-C286B1EF73EF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Doctor.Plugin.Observability", "Doctor\__Plugins\StellaOps.Doctor.Plugin.Observability\StellaOps.Doctor.Plugin.Observability.csproj", "{B7F2A984-2DF6-4E48-AEDF-FC19C0A2D092}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Doctor.Plugin.Observability.Tests", "Doctor\__Tests\StellaOps.Doctor.Plugin.Observability.Tests\StellaOps.Doctor.Plugin.Observability.Tests.csproj", "{8B6CA9A8-DC09-49D7-8A81-BA4BB56EF36E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -15539,6 +15551,54 @@ Global {1EA151EF-9BD9-49F4-A4CF-05FDCCB4D6E6}.Release|x64.Build.0 = Release|Any CPU {1EA151EF-9BD9-49F4-A4CF-05FDCCB4D6E6}.Release|x86.ActiveCfg = Release|Any CPU {1EA151EF-9BD9-49F4-A4CF-05FDCCB4D6E6}.Release|x86.Build.0 = Release|Any CPU + {C2F5114A-FE2A-402A-9DBC-73FE2AC71A6D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C2F5114A-FE2A-402A-9DBC-73FE2AC71A6D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C2F5114A-FE2A-402A-9DBC-73FE2AC71A6D}.Debug|x64.ActiveCfg = Debug|Any CPU + {C2F5114A-FE2A-402A-9DBC-73FE2AC71A6D}.Debug|x64.Build.0 = Debug|Any CPU + {C2F5114A-FE2A-402A-9DBC-73FE2AC71A6D}.Debug|x86.ActiveCfg = Debug|Any CPU + {C2F5114A-FE2A-402A-9DBC-73FE2AC71A6D}.Debug|x86.Build.0 = Debug|Any CPU + {C2F5114A-FE2A-402A-9DBC-73FE2AC71A6D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C2F5114A-FE2A-402A-9DBC-73FE2AC71A6D}.Release|Any CPU.Build.0 = Release|Any CPU + {C2F5114A-FE2A-402A-9DBC-73FE2AC71A6D}.Release|x64.ActiveCfg = Release|Any CPU + {C2F5114A-FE2A-402A-9DBC-73FE2AC71A6D}.Release|x64.Build.0 = Release|Any CPU + {C2F5114A-FE2A-402A-9DBC-73FE2AC71A6D}.Release|x86.ActiveCfg = Release|Any CPU + {C2F5114A-FE2A-402A-9DBC-73FE2AC71A6D}.Release|x86.Build.0 = Release|Any CPU + {237FDAA1-4852-4F6B-B6D6-C286B1EF73EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {237FDAA1-4852-4F6B-B6D6-C286B1EF73EF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {237FDAA1-4852-4F6B-B6D6-C286B1EF73EF}.Debug|x64.ActiveCfg = Debug|Any CPU + {237FDAA1-4852-4F6B-B6D6-C286B1EF73EF}.Debug|x64.Build.0 = Debug|Any CPU + {237FDAA1-4852-4F6B-B6D6-C286B1EF73EF}.Debug|x86.ActiveCfg = Debug|Any CPU + {237FDAA1-4852-4F6B-B6D6-C286B1EF73EF}.Debug|x86.Build.0 = Debug|Any CPU + {237FDAA1-4852-4F6B-B6D6-C286B1EF73EF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {237FDAA1-4852-4F6B-B6D6-C286B1EF73EF}.Release|Any CPU.Build.0 = Release|Any CPU + {237FDAA1-4852-4F6B-B6D6-C286B1EF73EF}.Release|x64.ActiveCfg = Release|Any CPU + {237FDAA1-4852-4F6B-B6D6-C286B1EF73EF}.Release|x64.Build.0 = Release|Any CPU + {237FDAA1-4852-4F6B-B6D6-C286B1EF73EF}.Release|x86.ActiveCfg = Release|Any CPU + {237FDAA1-4852-4F6B-B6D6-C286B1EF73EF}.Release|x86.Build.0 = Release|Any CPU + {B7F2A984-2DF6-4E48-AEDF-FC19C0A2D092}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B7F2A984-2DF6-4E48-AEDF-FC19C0A2D092}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B7F2A984-2DF6-4E48-AEDF-FC19C0A2D092}.Debug|x64.ActiveCfg = Debug|Any CPU + {B7F2A984-2DF6-4E48-AEDF-FC19C0A2D092}.Debug|x64.Build.0 = Debug|Any CPU + {B7F2A984-2DF6-4E48-AEDF-FC19C0A2D092}.Debug|x86.ActiveCfg = Debug|Any CPU + {B7F2A984-2DF6-4E48-AEDF-FC19C0A2D092}.Debug|x86.Build.0 = Debug|Any CPU + {B7F2A984-2DF6-4E48-AEDF-FC19C0A2D092}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B7F2A984-2DF6-4E48-AEDF-FC19C0A2D092}.Release|Any CPU.Build.0 = Release|Any CPU + {B7F2A984-2DF6-4E48-AEDF-FC19C0A2D092}.Release|x64.ActiveCfg = Release|Any CPU + {B7F2A984-2DF6-4E48-AEDF-FC19C0A2D092}.Release|x64.Build.0 = Release|Any CPU + {B7F2A984-2DF6-4E48-AEDF-FC19C0A2D092}.Release|x86.ActiveCfg = Release|Any CPU + {B7F2A984-2DF6-4E48-AEDF-FC19C0A2D092}.Release|x86.Build.0 = Release|Any CPU + {8B6CA9A8-DC09-49D7-8A81-BA4BB56EF36E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8B6CA9A8-DC09-49D7-8A81-BA4BB56EF36E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8B6CA9A8-DC09-49D7-8A81-BA4BB56EF36E}.Debug|x64.ActiveCfg = Debug|Any CPU + {8B6CA9A8-DC09-49D7-8A81-BA4BB56EF36E}.Debug|x64.Build.0 = Debug|Any CPU + {8B6CA9A8-DC09-49D7-8A81-BA4BB56EF36E}.Debug|x86.ActiveCfg = Debug|Any CPU + {8B6CA9A8-DC09-49D7-8A81-BA4BB56EF36E}.Debug|x86.Build.0 = Debug|Any CPU + {8B6CA9A8-DC09-49D7-8A81-BA4BB56EF36E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8B6CA9A8-DC09-49D7-8A81-BA4BB56EF36E}.Release|Any CPU.Build.0 = Release|Any CPU + {8B6CA9A8-DC09-49D7-8A81-BA4BB56EF36E}.Release|x64.ActiveCfg = Release|Any CPU + {8B6CA9A8-DC09-49D7-8A81-BA4BB56EF36E}.Release|x64.Build.0 = Release|Any CPU + {8B6CA9A8-DC09-49D7-8A81-BA4BB56EF36E}.Release|x86.ActiveCfg = Release|Any CPU + {8B6CA9A8-DC09-49D7-8A81-BA4BB56EF36E}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -17455,6 +17515,12 @@ Global {12FD0D46-8E01-49EA-BD3F-6CE8FCBDDC81} = {12FD71E4-11F8-1486-9CBE-37C5D40A2D29} {A8886BC5-28E0-4BA6-8639-F68955F854D5} = {AB891B76-C0E8-53F9-5C21-062253F7FAD4} {1EA151EF-9BD9-49F4-A4CF-05FDCCB4D6E6} = {D5C64D53-00BC-85AB-5460-CFCE7B4ED3D3} + {02ABD53F-AAAC-E75D-834F-C95FE948E79A} = {D5C64D53-00BC-85AB-5460-CFCE7B4ED3D3} + {C2F5114A-FE2A-402A-9DBC-73FE2AC71A6D} = {02ABD53F-AAAC-E75D-834F-C95FE948E79A} + {461AAFBE-976B-9A92-CA4C-52298A5190FF} = {D5C64D53-00BC-85AB-5460-CFCE7B4ED3D3} + {237FDAA1-4852-4F6B-B6D6-C286B1EF73EF} = {461AAFBE-976B-9A92-CA4C-52298A5190FF} + {B7F2A984-2DF6-4E48-AEDF-FC19C0A2D092} = {02ABD53F-AAAC-E75D-834F-C95FE948E79A} + {8B6CA9A8-DC09-49D7-8A81-BA4BB56EF36E} = {461AAFBE-976B-9A92-CA4C-52298A5190FF} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {C3AFD506-35CE-66A9-D3CD-8E808BC537AA} diff --git a/src/Unknowns/StellaOps.Unknowns.WebService/Endpoints/GreyQueueEndpoints.cs b/src/Unknowns/StellaOps.Unknowns.WebService/Endpoints/GreyQueueEndpoints.cs new file mode 100644 index 000000000..b856b9e71 --- /dev/null +++ b/src/Unknowns/StellaOps.Unknowns.WebService/Endpoints/GreyQueueEndpoints.cs @@ -0,0 +1,583 @@ +// ----------------------------------------------------------------------------- +// GreyQueueEndpoints.cs +// Description: Minimal API endpoints for Grey Queue management. +// Implements signed, replayable evidence pipeline for ambiguous unknowns. +// ----------------------------------------------------------------------------- + +using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.AspNetCore.Mvc; +using StellaOps.Unknowns.Core.Models; +using StellaOps.Unknowns.Core.Repositories; + +namespace StellaOps.Unknowns.WebService.Endpoints; + +/// +/// Minimal API endpoints for Grey Queue service. +/// +public static class GreyQueueEndpoints +{ + /// + /// Maps all Grey Queue endpoints. + /// + public static IEndpointRouteBuilder MapGreyQueueEndpoints(this IEndpointRouteBuilder routes) + { + var group = routes.MapGroup("/api/grey-queue") + .WithTags("GreyQueue") + .WithOpenApi(); + + // List and query + group.MapGet("/", ListEntries) + .WithName("ListGreyQueueEntries") + .WithSummary("List grey queue entries with pagination") + .WithDescription("Returns paginated list of grey queue entries. Supports filtering by status and reason."); + + group.MapGet("/{id:guid}", GetEntryById) + .WithName("GetGreyQueueEntry") + .WithSummary("Get grey queue entry by ID") + .WithDescription("Returns a single grey queue entry with full evidence bundle."); + + group.MapGet("/by-unknown/{unknownId:guid}", GetByUnknownId) + .WithName("GetGreyQueueByUnknownId") + .WithSummary("Get grey queue entry by unknown ID") + .WithDescription("Returns the grey queue entry for a specific unknown."); + + group.MapGet("/ready", GetReadyForProcessing) + .WithName("GetReadyForProcessing") + .WithSummary("Get entries ready for processing") + .WithDescription("Returns entries that are ready to be processed (pending, not exhausted, past next processing time)."); + + // Triggers + group.MapGet("/triggers/feed/{feedId}", GetByFeedTrigger) + .WithName("GetByFeedTrigger") + .WithSummary("Get entries triggered by feed update") + .WithDescription("Returns entries that should be reprocessed due to a feed update."); + + group.MapGet("/triggers/tool/{toolId}", GetByToolTrigger) + .WithName("GetByToolTrigger") + .WithSummary("Get entries triggered by tool update") + .WithDescription("Returns entries that should be reprocessed due to a tool update."); + + group.MapGet("/triggers/cve/{cveId}", GetByCveTrigger) + .WithName("GetByCveTrigger") + .WithSummary("Get entries triggered by CVE update") + .WithDescription("Returns entries that should be reprocessed due to a CVE update."); + + // Actions + group.MapPost("/", EnqueueEntry) + .WithName("EnqueueGreyQueueEntry") + .WithSummary("Enqueue a new grey queue entry") + .WithDescription("Creates a new grey queue entry with evidence bundle and trigger conditions."); + + group.MapPost("/{id:guid}/process", StartProcessing) + .WithName("StartGreyQueueProcessing") + .WithSummary("Mark entry as processing") + .WithDescription("Marks an entry as currently being processed."); + + group.MapPost("/{id:guid}/result", RecordResult) + .WithName("RecordGreyQueueResult") + .WithSummary("Record processing result") + .WithDescription("Records the result of a processing attempt."); + + group.MapPost("/{id:guid}/resolve", ResolveEntry) + .WithName("ResolveGreyQueueEntry") + .WithSummary("Resolve a grey queue entry") + .WithDescription("Marks an entry as resolved with resolution type and reference."); + + group.MapPost("/{id:guid}/dismiss", DismissEntry) + .WithName("DismissGreyQueueEntry") + .WithSummary("Dismiss a grey queue entry") + .WithDescription("Manually dismisses an entry from the queue."); + + // Maintenance + group.MapPost("/expire", ExpireOldEntries) + .WithName("ExpireGreyQueueEntries") + .WithSummary("Expire old entries") + .WithDescription("Expires entries that have exceeded their TTL."); + + // Statistics + group.MapGet("/summary", GetSummary) + .WithName("GetGreyQueueSummary") + .WithSummary("Get grey queue summary statistics") + .WithDescription("Returns summary counts by status, reason, and performance metrics."); + + return routes; + } + + // List entries with pagination + private static async Task> ListEntries( + [FromHeader(Name = "X-Tenant-Id")] string tenantId, + [FromQuery] int skip = 0, + [FromQuery] int take = 50, + [FromQuery] GreyQueueStatus? status = null, + [FromQuery] GreyQueueReason? reason = null, + IGreyQueueRepository repository = null!, + CancellationToken ct = default) + { + IReadOnlyList entries; + + if (status.HasValue) + { + entries = await repository.GetByStatusAsync(tenantId, status.Value, take, skip, ct); + } + else if (reason.HasValue) + { + entries = await repository.GetByReasonAsync(tenantId, reason.Value, take, ct); + } + else + { + entries = await repository.GetByStatusAsync(tenantId, GreyQueueStatus.Pending, take, skip, ct); + } + + var total = await repository.CountPendingAsync(tenantId, ct); + + var response = new GreyQueueListResponse + { + Items = entries.Select(MapToDto).ToList(), + Total = total, + Skip = skip, + Take = take + }; + + return TypedResults.Ok(response); + } + + // Get entry by ID + private static async Task, NotFound>> GetEntryById( + Guid id, + [FromHeader(Name = "X-Tenant-Id")] string tenantId, + IGreyQueueRepository repository = null!, + CancellationToken ct = default) + { + var entry = await repository.GetByIdAsync(tenantId, id, ct); + if (entry is null) + { + return TypedResults.NotFound(); + } + return TypedResults.Ok(MapToDto(entry)); + } + + // Get by unknown ID + private static async Task, NotFound>> GetByUnknownId( + Guid unknownId, + [FromHeader(Name = "X-Tenant-Id")] string tenantId, + IGreyQueueRepository repository = null!, + CancellationToken ct = default) + { + var entry = await repository.GetByUnknownIdAsync(tenantId, unknownId, ct); + if (entry is null) + { + return TypedResults.NotFound(); + } + return TypedResults.Ok(MapToDto(entry)); + } + + // Get ready for processing + private static async Task> GetReadyForProcessing( + [FromHeader(Name = "X-Tenant-Id")] string tenantId, + [FromQuery] int limit = 50, + IGreyQueueRepository repository = null!, + CancellationToken ct = default) + { + var entries = await repository.GetReadyForProcessingAsync(tenantId, limit, ct); + + var response = new GreyQueueListResponse + { + Items = entries.Select(MapToDto).ToList(), + Total = entries.Count, + Skip = 0, + Take = limit + }; + + return TypedResults.Ok(response); + } + + // Get by feed trigger + private static async Task> GetByFeedTrigger( + string feedId, + [FromHeader(Name = "X-Tenant-Id")] string tenantId, + [FromQuery] string? version = null, + [FromQuery] int limit = 50, + IGreyQueueRepository repository = null!, + CancellationToken ct = default) + { + var entries = await repository.GetByFeedTriggerAsync(tenantId, feedId, version, limit, ct); + + var response = new GreyQueueListResponse + { + Items = entries.Select(MapToDto).ToList(), + Total = entries.Count, + Skip = 0, + Take = limit + }; + + return TypedResults.Ok(response); + } + + // Get by tool trigger + private static async Task> GetByToolTrigger( + string toolId, + [FromHeader(Name = "X-Tenant-Id")] string tenantId, + [FromQuery] string? version = null, + [FromQuery] int limit = 50, + IGreyQueueRepository repository = null!, + CancellationToken ct = default) + { + var entries = await repository.GetByToolTriggerAsync(tenantId, toolId, version, limit, ct); + + var response = new GreyQueueListResponse + { + Items = entries.Select(MapToDto).ToList(), + Total = entries.Count, + Skip = 0, + Take = limit + }; + + return TypedResults.Ok(response); + } + + // Get by CVE trigger + private static async Task> GetByCveTrigger( + string cveId, + [FromHeader(Name = "X-Tenant-Id")] string tenantId, + [FromQuery] int limit = 50, + IGreyQueueRepository repository = null!, + CancellationToken ct = default) + { + var entries = await repository.GetByCveTriggerAsync(tenantId, cveId, limit, ct); + + var response = new GreyQueueListResponse + { + Items = entries.Select(MapToDto).ToList(), + Total = entries.Count, + Skip = 0, + Take = limit + }; + + return TypedResults.Ok(response); + } + + // Enqueue new entry + private static async Task> EnqueueEntry( + [FromHeader(Name = "X-Tenant-Id")] string tenantId, + [FromBody] EnqueueGreyQueueRequest request, + IGreyQueueRepository repository = null!, + CancellationToken ct = default) + { + var evidence = request.Evidence is not null ? new GreyQueueEvidenceBundle + { + SbomSliceJson = request.Evidence.SbomSliceJson, + AdvisorySnippetJson = request.Evidence.AdvisorySnippetJson, + VexEvidenceJson = request.Evidence.VexEvidenceJson, + DiffTracesJson = request.Evidence.DiffTracesJson, + ReachabilityEvidenceJson = request.Evidence.ReachabilityEvidenceJson + } : null; + + var triggers = request.Triggers is not null ? new GreyQueueTriggers + { + Feeds = request.Triggers.Feeds?.Select(f => new FeedTrigger(f.FeedId, f.MinVersion)).ToList() ?? [], + Tools = request.Triggers.Tools?.Select(t => new ToolTrigger(t.ToolId, t.MinVersion)).ToList() ?? [], + CveIds = request.Triggers.CveIds ?? [], + PurlPatterns = request.Triggers.PurlPatterns ?? [] + } : null; + + var entry = await repository.EnqueueAsync( + tenantId, + request.UnknownId, + request.Reason, + request.ReasonDetail, + evidence, + triggers, + request.Priority, + request.CreatedBy, + request.CorrelationId, + ct); + + return TypedResults.Created($"/api/grey-queue/{entry.Id}", MapToDto(entry)); + } + + // Start processing + private static async Task, NotFound>> StartProcessing( + Guid id, + [FromHeader(Name = "X-Tenant-Id")] string tenantId, + IGreyQueueRepository repository = null!, + CancellationToken ct = default) + { + try + { + var entry = await repository.StartProcessingAsync(tenantId, id, ct); + return TypedResults.Ok(MapToDto(entry)); + } + catch (KeyNotFoundException) + { + return TypedResults.NotFound(); + } + } + + // Record result + private static async Task, NotFound>> RecordResult( + Guid id, + [FromHeader(Name = "X-Tenant-Id")] string tenantId, + [FromBody] RecordResultRequest request, + IGreyQueueRepository repository = null!, + CancellationToken ct = default) + { + try + { + var entry = await repository.RecordProcessingResultAsync( + tenantId, + id, + request.Success, + request.Result, + request.NextProcessingAt, + ct); + return TypedResults.Ok(MapToDto(entry)); + } + catch (KeyNotFoundException) + { + return TypedResults.NotFound(); + } + } + + // Resolve entry + private static async Task, NotFound>> ResolveEntry( + Guid id, + [FromHeader(Name = "X-Tenant-Id")] string tenantId, + [FromBody] ResolveEntryRequest request, + IGreyQueueRepository repository = null!, + CancellationToken ct = default) + { + try + { + var entry = await repository.ResolveAsync( + tenantId, + id, + request.Resolution, + request.ResolutionRef, + ct); + return TypedResults.Ok(MapToDto(entry)); + } + catch (KeyNotFoundException) + { + return TypedResults.NotFound(); + } + } + + // Dismiss entry + private static async Task, NotFound>> DismissEntry( + Guid id, + [FromHeader(Name = "X-Tenant-Id")] string tenantId, + [FromBody] DismissEntryRequest request, + IGreyQueueRepository repository = null!, + CancellationToken ct = default) + { + try + { + var entry = await repository.DismissAsync( + tenantId, + id, + request.DismissedBy, + request.Reason, + ct); + return TypedResults.Ok(MapToDto(entry)); + } + catch (KeyNotFoundException) + { + return TypedResults.NotFound(); + } + } + + // Expire old entries + private static async Task> ExpireOldEntries( + [FromHeader(Name = "X-Tenant-Id")] string tenantId, + IGreyQueueRepository repository = null!, + CancellationToken ct = default) + { + var count = await repository.ExpireOldEntriesAsync(tenantId, ct); + return TypedResults.Ok(new ExpireResultResponse { ExpiredCount = count }); + } + + // Get summary + private static async Task> GetSummary( + [FromHeader(Name = "X-Tenant-Id")] string tenantId, + IGreyQueueRepository repository = null!, + CancellationToken ct = default) + { + var summary = await repository.GetSummaryAsync(tenantId, ct); + + var response = new GreyQueueSummaryDto + { + Total = summary.Total, + Pending = summary.Pending, + Processing = summary.Processing, + Retrying = summary.Retrying, + Resolved = summary.Resolved, + Failed = summary.Failed, + Expired = summary.Expired, + Dismissed = summary.Dismissed, + ByReason = summary.ByReason, + AvgAttemptsToResolve = summary.AvgAttemptsToResolve, + AvgHoursToResolve = summary.AvgHoursToResolve, + OldestPendingHours = summary.OldestPendingHours + }; + + return TypedResults.Ok(response); + } + + // Mapping helpers + private static GreyQueueEntryDto MapToDto(GreyQueueEntry e) => new() + { + Id = e.Id, + TenantId = e.TenantId, + UnknownId = e.UnknownId, + Fingerprint = e.Fingerprint, + Status = e.Status.ToString(), + Priority = e.Priority, + Reason = e.Reason.ToString(), + ReasonDetail = e.ReasonDetail, + HasSbomSlice = e.SbomSlice is not null, + HasAdvisorySnippet = e.AdvisorySnippet is not null, + HasVexEvidence = e.VexEvidence is not null, + HasDiffTraces = e.DiffTraces is not null, + HasReachabilityEvidence = e.ReachabilityEvidence is not null, + ProcessingAttempts = e.ProcessingAttempts, + MaxAttempts = e.MaxAttempts, + LastProcessedAt = e.LastProcessedAt, + LastProcessingResult = e.LastProcessingResult, + NextProcessingAt = e.NextProcessingAt, + TriggerCveIds = e.TriggerOnCveUpdate.ToList(), + TriggerPurlPatterns = e.TriggerOnPurlMatch.ToList(), + CreatedAt = e.CreatedAt, + UpdatedAt = e.UpdatedAt, + ExpiresAt = e.ExpiresAt, + ResolvedAt = e.ResolvedAt, + Resolution = e.Resolution?.ToString(), + ResolutionRef = e.ResolutionRef, + IsPending = e.IsPending, + IsExhausted = e.IsExhausted, + IsReadyForProcessing = e.IsReadyForProcessing + }; +} + +// DTOs + +public sealed record GreyQueueListResponse +{ + public required IReadOnlyList Items { get; init; } + public required long Total { get; init; } + public required int Skip { get; init; } + public required int Take { get; init; } +} + +public sealed record GreyQueueEntryDto +{ + public required Guid Id { get; init; } + public required string TenantId { get; init; } + public required Guid UnknownId { get; init; } + public required string Fingerprint { get; init; } + public required string Status { get; init; } + public required int Priority { get; init; } + public required string Reason { get; init; } + public string? ReasonDetail { get; init; } + public required bool HasSbomSlice { get; init; } + public required bool HasAdvisorySnippet { get; init; } + public required bool HasVexEvidence { get; init; } + public required bool HasDiffTraces { get; init; } + public required bool HasReachabilityEvidence { get; init; } + public required int ProcessingAttempts { get; init; } + public required int MaxAttempts { get; init; } + public DateTimeOffset? LastProcessedAt { get; init; } + public string? LastProcessingResult { get; init; } + public DateTimeOffset? NextProcessingAt { get; init; } + public required IReadOnlyList TriggerCveIds { get; init; } + public required IReadOnlyList TriggerPurlPatterns { get; init; } + public required DateTimeOffset CreatedAt { get; init; } + public required DateTimeOffset UpdatedAt { get; init; } + public DateTimeOffset? ExpiresAt { get; init; } + public DateTimeOffset? ResolvedAt { get; init; } + public string? Resolution { get; init; } + public string? ResolutionRef { get; init; } + public required bool IsPending { get; init; } + public required bool IsExhausted { get; init; } + public required bool IsReadyForProcessing { get; init; } +} + +public sealed record GreyQueueSummaryDto +{ + public required long Total { get; init; } + public required long Pending { get; init; } + public required long Processing { get; init; } + public required long Retrying { get; init; } + public required long Resolved { get; init; } + public required long Failed { get; init; } + public required long Expired { get; init; } + public required long Dismissed { get; init; } + public required IReadOnlyDictionary ByReason { get; init; } + public double AvgAttemptsToResolve { get; init; } + public double AvgHoursToResolve { get; init; } + public double OldestPendingHours { get; init; } +} + +public sealed record EnqueueGreyQueueRequest +{ + public required Guid UnknownId { get; init; } + public required GreyQueueReason Reason { get; init; } + public string? ReasonDetail { get; init; } + public EvidenceBundleDto? Evidence { get; init; } + public TriggersDto? Triggers { get; init; } + public int Priority { get; init; } = 100; + public required string CreatedBy { get; init; } + public string? CorrelationId { get; init; } +} + +public sealed record EvidenceBundleDto +{ + public string? SbomSliceJson { get; init; } + public string? AdvisorySnippetJson { get; init; } + public string? VexEvidenceJson { get; init; } + public string? DiffTracesJson { get; init; } + public string? ReachabilityEvidenceJson { get; init; } +} + +public sealed record TriggersDto +{ + public IReadOnlyList? Feeds { get; init; } + public IReadOnlyList? Tools { get; init; } + public IReadOnlyList? CveIds { get; init; } + public IReadOnlyList? PurlPatterns { get; init; } +} + +public sealed record FeedTriggerDto +{ + public required string FeedId { get; init; } + public string? MinVersion { get; init; } +} + +public sealed record ToolTriggerDto +{ + public required string ToolId { get; init; } + public string? MinVersion { get; init; } +} + +public sealed record RecordResultRequest +{ + public required bool Success { get; init; } + public required string Result { get; init; } + public DateTimeOffset? NextProcessingAt { get; init; } +} + +public sealed record ResolveEntryRequest +{ + public required GreyQueueResolution Resolution { get; init; } + public string? ResolutionRef { get; init; } +} + +public sealed record DismissEntryRequest +{ + public required string DismissedBy { get; init; } + public string? Reason { get; init; } +} + +public sealed record ExpireResultResponse +{ + public required int ExpiredCount { get; init; } +} diff --git a/src/Unknowns/__Libraries/StellaOps.Unknowns.Core/Models/GreyQueueEntry.cs b/src/Unknowns/__Libraries/StellaOps.Unknowns.Core/Models/GreyQueueEntry.cs new file mode 100644 index 000000000..63fcc923d --- /dev/null +++ b/src/Unknowns/__Libraries/StellaOps.Unknowns.Core/Models/GreyQueueEntry.cs @@ -0,0 +1,235 @@ +// ----------------------------------------------------------------------------- +// GreyQueueEntry.cs +// Description: Grey Queue entry for unknowns requiring reprocessing when feeds/tools update. +// Implements signed, replayable evidence pipeline requirement for handling ambiguous cases. +// ----------------------------------------------------------------------------- + +using System.Text.Json; + +namespace StellaOps.Unknowns.Core.Models; + +/// +/// Represents an entry in the Grey Queue - unknowns that cannot be definitively resolved +/// and are queued for reprocessing when feeds or tools update. +/// +/// +/// The Grey Queue implements the "Grey Queue" pattern from the signed SBOM-VEX-policy pipeline: +/// - Persists full DSSE bundle (SBOM slice, advisory snippet, diff traces) +/// - Uses deterministic fingerprints for deduplication and replay +/// - Reprocesses when feeds/tools update +/// +public sealed record GreyQueueEntry +{ + /// Unique identifier for this queue entry. + public required Guid Id { get; init; } + + /// Tenant that owns this entry. + public required string TenantId { get; init; } + + /// Reference to the Unknown this entry relates to. + public required Guid UnknownId { get; init; } + + /// SHA-256 fingerprint for deterministic deduplication and replay. + public required string Fingerprint { get; init; } + + /// Current status of the queue entry. + public required GreyQueueStatus Status { get; init; } + + /// Priority for processing (lower = higher priority). + public required int Priority { get; init; } + + /// Reason why this entry is in the grey queue. + public required GreyQueueReason Reason { get; init; } + + /// Human-readable description of why resolution failed. + public string? ReasonDetail { get; init; } + + // Evidence Bundle (DSSE-style) + + /// SBOM slice relevant to this unknown (JSON). + public JsonDocument? SbomSlice { get; init; } + + /// SHA-256 hash of the SBOM slice for integrity. + public string? SbomSliceHash { get; init; } + + /// Advisory snippet relevant to this unknown (JSON). + public JsonDocument? AdvisorySnippet { get; init; } + + /// SHA-256 hash of the advisory snippet for integrity. + public string? AdvisorySnippetHash { get; init; } + + /// VEX evidence relevant to this unknown (JSON). + public JsonDocument? VexEvidence { get; init; } + + /// SHA-256 hash of the VEX evidence for integrity. + public string? VexEvidenceHash { get; init; } + + /// Diff/trace data for binary analysis (JSON). + public JsonDocument? DiffTraces { get; init; } + + /// SHA-256 hash of diff traces for integrity. + public string? DiffTracesHash { get; init; } + + /// Reachability evidence if applicable (JSON). + public JsonDocument? ReachabilityEvidence { get; init; } + + /// SHA-256 hash of reachability evidence for integrity. + public string? ReachabilityEvidenceHash { get; init; } + + // Processing Metadata + + /// Number of processing attempts. + public int ProcessingAttempts { get; init; } + + /// Maximum allowed processing attempts before marking as failed. + public int MaxAttempts { get; init; } = 10; + + /// When the entry was last processed. + public DateTimeOffset? LastProcessedAt { get; init; } + + /// Result of the last processing attempt. + public string? LastProcessingResult { get; init; } + + /// When to next attempt processing. + public DateTimeOffset? NextProcessingAt { get; init; } + + // Trigger Conditions + + /// Feed versions that should trigger reprocessing (JSON array of feed IDs/versions). + public JsonDocument? TriggerOnFeedUpdate { get; init; } + + /// Tool versions that should trigger reprocessing (JSON array of tool IDs/versions). + public JsonDocument? TriggerOnToolUpdate { get; init; } + + /// Specific CVE IDs that should trigger reprocessing when updated. + public IReadOnlyList TriggerOnCveUpdate { get; init; } = []; + + /// Specific PURL patterns that should trigger reprocessing. + public IReadOnlyList TriggerOnPurlMatch { get; init; } = []; + + // Timestamps + + /// When this entry was created. + public required DateTimeOffset CreatedAt { get; init; } + + /// When this entry was last updated. + public DateTimeOffset UpdatedAt { get; init; } + + /// When this entry expires (for automatic cleanup). + public DateTimeOffset? ExpiresAt { get; init; } + + /// When this entry was resolved (moved out of grey queue). + public DateTimeOffset? ResolvedAt { get; init; } + + /// How this entry was resolved. + public GreyQueueResolution? Resolution { get; init; } + + /// Reference to resolving entity (e.g., feed update ID, tool version). + public string? ResolutionRef { get; init; } + + // Audit + + /// Who created this entry. + public required string CreatedBy { get; init; } + + /// Correlation ID for tracing. + public string? CorrelationId { get; init; } + + // Computed Properties + + /// Whether this entry is still pending processing. + public bool IsPending => Status is GreyQueueStatus.Pending or GreyQueueStatus.Retrying; + + /// Whether this entry has exceeded max attempts. + public bool IsExhausted => ProcessingAttempts >= MaxAttempts; + + /// Whether this entry is ready for processing. + public bool IsReadyForProcessing => + IsPending && + !IsExhausted && + (NextProcessingAt is null || NextProcessingAt <= DateTimeOffset.UtcNow); +} + +/// Status of a grey queue entry. +public enum GreyQueueStatus +{ + /// Pending initial processing. + Pending, + + /// Currently being processed. + Processing, + + /// Waiting for retry after failed attempt. + Retrying, + + /// Successfully resolved - evidence now sufficient. + Resolved, + + /// Failed after exhausting retries. + Failed, + + /// Expired without resolution. + Expired, + + /// Manually dismissed by operator. + Dismissed +} + +/// Reason why an entry is in the grey queue. +public enum GreyQueueReason +{ + /// Insufficient VEX coverage for verdict. + InsufficientVex, + + /// Conflicting VEX statements from multiple sources. + ConflictingVex, + + /// Missing reachability evidence. + MissingReachability, + + /// Ambiguous package identity. + AmbiguousIdentity, + + /// Feed not yet available for this component. + FeedNotAvailable, + + /// Tool does not support this component type. + ToolUnsupported, + + /// Binary analysis inconclusive. + BinaryAnalysisInconclusive, + + /// Backport detection uncertain. + BackportUncertain, + + /// Multiple valid interpretations possible. + MultipleInterpretations, + + /// Waiting for upstream advisory. + AwaitingUpstreamAdvisory +} + +/// How a grey queue entry was resolved. +public enum GreyQueueResolution +{ + /// Resolved by feed update providing sufficient evidence. + FeedUpdate, + + /// Resolved by tool update enabling analysis. + ToolUpdate, + + /// Resolved by VEX statement from authoritative source. + VexReceived, + + /// Resolved by manual operator decision. + ManualResolution, + + /// Superseded by newer unknown/finding. + Superseded, + + /// Determined to be not applicable. + NotApplicable, + + /// Entry expired without resolution. + Expired +} diff --git a/src/Unknowns/__Libraries/StellaOps.Unknowns.Core/Repositories/IGreyQueueRepository.cs b/src/Unknowns/__Libraries/StellaOps.Unknowns.Core/Repositories/IGreyQueueRepository.cs new file mode 100644 index 000000000..24cbfb1bb --- /dev/null +++ b/src/Unknowns/__Libraries/StellaOps.Unknowns.Core/Repositories/IGreyQueueRepository.cs @@ -0,0 +1,275 @@ +// ----------------------------------------------------------------------------- +// IGreyQueueRepository.cs +// Description: Repository interface for Grey Queue operations. +// Supports the signed, replayable evidence pipeline pattern. +// ----------------------------------------------------------------------------- + +using StellaOps.Unknowns.Core.Models; + +namespace StellaOps.Unknowns.Core.Repositories; + +/// +/// Repository interface for Grey Queue operations. +/// +public interface IGreyQueueRepository +{ + /// + /// Enqueues an unknown into the grey queue with evidence bundle. + /// + Task EnqueueAsync( + string tenantId, + Guid unknownId, + GreyQueueReason reason, + string? reasonDetail, + GreyQueueEvidenceBundle? evidence, + GreyQueueTriggers? triggers, + int priority, + string createdBy, + string? correlationId, + CancellationToken cancellationToken); + + /// + /// Gets a grey queue entry by ID. + /// + Task GetByIdAsync( + string tenantId, + Guid id, + CancellationToken cancellationToken); + + /// + /// Gets a grey queue entry by unknown ID. + /// + Task GetByUnknownIdAsync( + string tenantId, + Guid unknownId, + CancellationToken cancellationToken); + + /// + /// Gets a grey queue entry by fingerprint (for deduplication). + /// + Task GetByFingerprintAsync( + string tenantId, + string fingerprint, + CancellationToken cancellationToken); + + /// + /// Gets pending entries ready for processing. + /// + Task> GetReadyForProcessingAsync( + string tenantId, + int limit, + CancellationToken cancellationToken); + + /// + /// Gets entries that should be triggered by a feed update. + /// + Task> GetByFeedTriggerAsync( + string tenantId, + string feedId, + string? feedVersion, + int limit, + CancellationToken cancellationToken); + + /// + /// Gets entries that should be triggered by a tool update. + /// + Task> GetByToolTriggerAsync( + string tenantId, + string toolId, + string? toolVersion, + int limit, + CancellationToken cancellationToken); + + /// + /// Gets entries that should be triggered by a CVE update. + /// + Task> GetByCveTriggerAsync( + string tenantId, + string cveId, + int limit, + CancellationToken cancellationToken); + + /// + /// Gets entries filtered by status. + /// + Task> GetByStatusAsync( + string tenantId, + GreyQueueStatus status, + int? limit, + int? offset, + CancellationToken cancellationToken); + + /// + /// Gets entries filtered by reason. + /// + Task> GetByReasonAsync( + string tenantId, + GreyQueueReason reason, + int? limit, + CancellationToken cancellationToken); + + /// + /// Marks an entry as processing. + /// + Task StartProcessingAsync( + string tenantId, + Guid id, + CancellationToken cancellationToken); + + /// + /// Records a processing attempt result. + /// + Task RecordProcessingResultAsync( + string tenantId, + Guid id, + bool success, + string result, + DateTimeOffset? nextProcessingAt, + CancellationToken cancellationToken); + + /// + /// Resolves a grey queue entry. + /// + Task ResolveAsync( + string tenantId, + Guid id, + GreyQueueResolution resolution, + string? resolutionRef, + CancellationToken cancellationToken); + + /// + /// Dismisses a grey queue entry (manual operator action). + /// + Task DismissAsync( + string tenantId, + Guid id, + string dismissedBy, + string? reason, + CancellationToken cancellationToken); + + /// + /// Expires old entries that have exceeded their TTL. + /// + Task ExpireOldEntriesAsync( + string tenantId, + CancellationToken cancellationToken); + + /// + /// Gets summary statistics for the grey queue. + /// + Task GetSummaryAsync( + string tenantId, + CancellationToken cancellationToken); + + /// + /// Counts entries by status. + /// + Task> CountByStatusAsync( + string tenantId, + CancellationToken cancellationToken); + + /// + /// Counts entries by reason. + /// + Task> CountByReasonAsync( + string tenantId, + CancellationToken cancellationToken); + + /// + /// Gets the total count of pending entries. + /// + Task CountPendingAsync( + string tenantId, + CancellationToken cancellationToken); +} + +/// +/// Evidence bundle for grey queue entries. +/// +public sealed record GreyQueueEvidenceBundle +{ + /// SBOM slice JSON. + public string? SbomSliceJson { get; init; } + + /// Advisory snippet JSON. + public string? AdvisorySnippetJson { get; init; } + + /// VEX evidence JSON. + public string? VexEvidenceJson { get; init; } + + /// Diff traces JSON. + public string? DiffTracesJson { get; init; } + + /// Reachability evidence JSON. + public string? ReachabilityEvidenceJson { get; init; } +} + +/// +/// Trigger conditions for reprocessing. +/// +public sealed record GreyQueueTriggers +{ + /// Feed IDs/versions that should trigger reprocessing. + public IReadOnlyList Feeds { get; init; } = []; + + /// Tool IDs/versions that should trigger reprocessing. + public IReadOnlyList Tools { get; init; } = []; + + /// CVE IDs that should trigger reprocessing. + public IReadOnlyList CveIds { get; init; } = []; + + /// PURL patterns that should trigger reprocessing. + public IReadOnlyList PurlPatterns { get; init; } = []; +} + +/// +/// Feed trigger specification. +/// +public sealed record FeedTrigger(string FeedId, string? MinVersion); + +/// +/// Tool trigger specification. +/// +public sealed record ToolTrigger(string ToolId, string? MinVersion); + +/// +/// Summary statistics for the grey queue. +/// +public sealed record GreyQueueSummary +{ + /// Total entries in the queue. + public required long Total { get; init; } + + /// Pending entries awaiting processing. + public required long Pending { get; init; } + + /// Entries currently being processed. + public required long Processing { get; init; } + + /// Entries waiting for retry. + public required long Retrying { get; init; } + + /// Successfully resolved entries. + public required long Resolved { get; init; } + + /// Failed entries (exhausted retries). + public required long Failed { get; init; } + + /// Expired entries. + public required long Expired { get; init; } + + /// Dismissed entries. + public required long Dismissed { get; init; } + + /// Entries by reason breakdown. + public required IReadOnlyDictionary ByReason { get; init; } + + /// Average processing attempts for resolved entries. + public double AvgAttemptsToResolve { get; init; } + + /// Average time to resolution in hours. + public double AvgHoursToResolve { get; init; } + + /// Oldest pending entry age in hours. + public double OldestPendingHours { get; init; } +} diff --git a/src/Unknowns/__Tests/StellaOps.Unknowns.Core.Tests/Models/GreyQueueEntryTests.cs b/src/Unknowns/__Tests/StellaOps.Unknowns.Core.Tests/Models/GreyQueueEntryTests.cs new file mode 100644 index 000000000..e6ee006f2 --- /dev/null +++ b/src/Unknowns/__Tests/StellaOps.Unknowns.Core.Tests/Models/GreyQueueEntryTests.cs @@ -0,0 +1,240 @@ +// ----------------------------------------------------------------------------- +// GreyQueueEntryTests.cs +// Description: Unit tests for Grey Queue entry model. +// ----------------------------------------------------------------------------- + +using System.Text.Json; +using FluentAssertions; +using StellaOps.Unknowns.Core.Models; +using Xunit; + +namespace StellaOps.Unknowns.Core.Tests.Models; + +[Trait("Category", "Unit")] +[Trait("Category", "GreyQueue")] +public sealed class GreyQueueEntryTests +{ + [Fact] + public void IsPending_WhenStatusPending_ReturnsTrue() + { + // Arrange + var entry = CreateEntry(GreyQueueStatus.Pending); + + // Act & Assert + entry.IsPending.Should().BeTrue(); + } + + [Fact] + public void IsPending_WhenStatusRetrying_ReturnsTrue() + { + // Arrange + var entry = CreateEntry(GreyQueueStatus.Retrying); + + // Act & Assert + entry.IsPending.Should().BeTrue(); + } + + [Theory] + [InlineData(GreyQueueStatus.Processing)] + [InlineData(GreyQueueStatus.Resolved)] + [InlineData(GreyQueueStatus.Failed)] + [InlineData(GreyQueueStatus.Expired)] + [InlineData(GreyQueueStatus.Dismissed)] + public void IsPending_WhenStatusNotPendingOrRetrying_ReturnsFalse(GreyQueueStatus status) + { + // Arrange + var entry = CreateEntry(status); + + // Act & Assert + entry.IsPending.Should().BeFalse(); + } + + [Fact] + public void IsExhausted_WhenAttemptsEqualMax_ReturnsTrue() + { + // Arrange + var entry = CreateEntry(GreyQueueStatus.Pending) with + { + ProcessingAttempts = 10, + MaxAttempts = 10 + }; + + // Act & Assert + entry.IsExhausted.Should().BeTrue(); + } + + [Fact] + public void IsExhausted_WhenAttemptsExceedMax_ReturnsTrue() + { + // Arrange + var entry = CreateEntry(GreyQueueStatus.Pending) with + { + ProcessingAttempts = 15, + MaxAttempts = 10 + }; + + // Act & Assert + entry.IsExhausted.Should().BeTrue(); + } + + [Fact] + public void IsExhausted_WhenAttemptsBelowMax_ReturnsFalse() + { + // Arrange + var entry = CreateEntry(GreyQueueStatus.Pending) with + { + ProcessingAttempts = 5, + MaxAttempts = 10 + }; + + // Act & Assert + entry.IsExhausted.Should().BeFalse(); + } + + [Fact] + public void IsReadyForProcessing_WhenPendingNotExhaustedAndPastTime_ReturnsTrue() + { + // Arrange + var entry = CreateEntry(GreyQueueStatus.Pending) with + { + ProcessingAttempts = 3, + MaxAttempts = 10, + NextProcessingAt = DateTimeOffset.UtcNow.AddHours(-1) + }; + + // Act & Assert + entry.IsReadyForProcessing.Should().BeTrue(); + } + + [Fact] + public void IsReadyForProcessing_WhenPendingNotExhaustedAndNullTime_ReturnsTrue() + { + // Arrange + var entry = CreateEntry(GreyQueueStatus.Pending) with + { + ProcessingAttempts = 3, + MaxAttempts = 10, + NextProcessingAt = null + }; + + // Act & Assert + entry.IsReadyForProcessing.Should().BeTrue(); + } + + [Fact] + public void IsReadyForProcessing_WhenFutureNextProcessingTime_ReturnsFalse() + { + // Arrange + var entry = CreateEntry(GreyQueueStatus.Pending) with + { + ProcessingAttempts = 3, + MaxAttempts = 10, + NextProcessingAt = DateTimeOffset.UtcNow.AddHours(1) + }; + + // Act & Assert + entry.IsReadyForProcessing.Should().BeFalse(); + } + + [Fact] + public void IsReadyForProcessing_WhenExhausted_ReturnsFalse() + { + // Arrange + var entry = CreateEntry(GreyQueueStatus.Pending) with + { + ProcessingAttempts = 10, + MaxAttempts = 10, + NextProcessingAt = DateTimeOffset.UtcNow.AddHours(-1) + }; + + // Act & Assert + entry.IsReadyForProcessing.Should().BeFalse(); + } + + [Fact] + public void IsReadyForProcessing_WhenNotPending_ReturnsFalse() + { + // Arrange + var entry = CreateEntry(GreyQueueStatus.Resolved) with + { + ProcessingAttempts = 3, + MaxAttempts = 10, + NextProcessingAt = DateTimeOffset.UtcNow.AddHours(-1) + }; + + // Act & Assert + entry.IsReadyForProcessing.Should().BeFalse(); + } + + [Fact] + public void CanSerializeToJson_WithAllFields() + { + // Arrange + var entry = CreateEntry(GreyQueueStatus.Pending) with + { + ReasonDetail = "Insufficient VEX coverage from vendors", + TriggerOnCveUpdate = ["CVE-2025-12345", "CVE-2025-12346"], + TriggerOnPurlMatch = ["pkg:npm/*"] + }; + + // Act + var json = JsonSerializer.Serialize(entry); + var deserialized = JsonSerializer.Deserialize(json); + + // Assert + deserialized.Should().NotBeNull(); + deserialized!.Id.Should().Be(entry.Id); + deserialized.Reason.Should().Be(entry.Reason); + deserialized.TriggerOnCveUpdate.Should().BeEquivalentTo(entry.TriggerOnCveUpdate); + } + + [Theory] + [InlineData(GreyQueueReason.InsufficientVex)] + [InlineData(GreyQueueReason.ConflictingVex)] + [InlineData(GreyQueueReason.MissingReachability)] + [InlineData(GreyQueueReason.AmbiguousIdentity)] + [InlineData(GreyQueueReason.FeedNotAvailable)] + [InlineData(GreyQueueReason.ToolUnsupported)] + [InlineData(GreyQueueReason.BinaryAnalysisInconclusive)] + [InlineData(GreyQueueReason.BackportUncertain)] + [InlineData(GreyQueueReason.MultipleInterpretations)] + [InlineData(GreyQueueReason.AwaitingUpstreamAdvisory)] + public void AllGreyQueueReasons_AreValid(GreyQueueReason reason) + { + // Arrange & Act + var entry = CreateEntry(GreyQueueStatus.Pending) with { Reason = reason }; + + // Assert + entry.Reason.Should().Be(reason); + } + + [Theory] + [InlineData(GreyQueueResolution.FeedUpdate)] + [InlineData(GreyQueueResolution.ToolUpdate)] + [InlineData(GreyQueueResolution.VexReceived)] + [InlineData(GreyQueueResolution.ManualResolution)] + [InlineData(GreyQueueResolution.Superseded)] + [InlineData(GreyQueueResolution.NotApplicable)] + [InlineData(GreyQueueResolution.Expired)] + public void AllGreyQueueResolutions_AreValid(GreyQueueResolution resolution) + { + // Arrange & Act + var entry = CreateEntry(GreyQueueStatus.Resolved) with { Resolution = resolution }; + + // Assert + entry.Resolution.Should().Be(resolution); + } + + private static GreyQueueEntry CreateEntry(GreyQueueStatus status) => new() + { + Id = Guid.NewGuid(), + TenantId = "test-tenant", + UnknownId = Guid.NewGuid(), + Fingerprint = "sha256:abc123", + Status = status, + Priority = 100, + Reason = GreyQueueReason.InsufficientVex, + CreatedAt = DateTimeOffset.UtcNow, + CreatedBy = "test-user" + }; +} diff --git a/src/__Libraries/StellaOps.Doctor/Export/DiagnosticBundleGenerator.cs b/src/__Libraries/StellaOps.Doctor/Export/DiagnosticBundleGenerator.cs index 66526dfbf..55a33c1c5 100644 --- a/src/__Libraries/StellaOps.Doctor/Export/DiagnosticBundleGenerator.cs +++ b/src/__Libraries/StellaOps.Doctor/Export/DiagnosticBundleGenerator.cs @@ -60,7 +60,7 @@ public sealed class DiagnosticBundleGenerator // Run full doctor check var report = await _engine.RunAsync( new DoctorRunOptions { Mode = DoctorRunMode.Full }, - cancellationToken: ct); + ct: ct); var sanitizer = new ConfigurationSanitizer(); diff --git a/src/__Libraries/StellaOps.Evidence/Budgets/EvidenceBudgetService.cs b/src/__Libraries/StellaOps.Evidence/Budgets/EvidenceBudgetService.cs index 221d84882..128927336 100644 --- a/src/__Libraries/StellaOps.Evidence/Budgets/EvidenceBudgetService.cs +++ b/src/__Libraries/StellaOps.Evidence/Budgets/EvidenceBudgetService.cs @@ -1,3 +1,4 @@ +using System.Globalization; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -5,8 +6,8 @@ namespace StellaOps.Evidence.Budgets; public interface IEvidenceBudgetService { - BudgetCheckResult CheckBudget(Guid scanId, EvidenceItem item); - BudgetStatus GetBudgetStatus(Guid scanId); + Task CheckBudgetAsync(Guid scanId, EvidenceItem item, CancellationToken ct); + Task GetBudgetStatusAsync(Guid scanId, CancellationToken ct); Task PruneToFitAsync(Guid scanId, long targetBytes, CancellationToken ct); } @@ -26,10 +27,11 @@ public sealed class EvidenceBudgetService : IEvidenceBudgetService _logger = logger; } - public BudgetCheckResult CheckBudget(Guid scanId, EvidenceItem item) + public async Task CheckBudgetAsync(Guid scanId, EvidenceItem item, CancellationToken ct) { + ArgumentNullException.ThrowIfNull(item); var budget = _options.CurrentValue; - var currentUsage = GetCurrentUsage(scanId); + var currentUsage = await GetCurrentUsageAsync(scanId, ct); var issues = new List(); @@ -37,7 +39,9 @@ public sealed class EvidenceBudgetService : IEvidenceBudgetService var projectedTotal = currentUsage.TotalBytes + item.SizeBytes; if (projectedTotal > budget.MaxScanSizeBytes) { - issues.Add($"Would exceed total budget: {projectedTotal:N0} > {budget.MaxScanSizeBytes:N0} bytes"); + issues.Add( + $"Would exceed total budget: {projectedTotal.ToString("N0", CultureInfo.InvariantCulture)} > " + + $"{budget.MaxScanSizeBytes.ToString("N0", CultureInfo.InvariantCulture)} bytes"); } // Check per-type budget @@ -47,7 +51,9 @@ public sealed class EvidenceBudgetService : IEvidenceBudgetService var projectedType = typeUsage + item.SizeBytes; if (projectedType > typeLimit) { - issues.Add($"Would exceed {item.Type} budget: {projectedType:N0} > {typeLimit:N0} bytes"); + issues.Add( + $"Would exceed {item.Type} budget: {projectedType.ToString("N0", CultureInfo.InvariantCulture)} > " + + $"{typeLimit.ToString("N0", CultureInfo.InvariantCulture)} bytes"); } } @@ -66,10 +72,10 @@ public sealed class EvidenceBudgetService : IEvidenceBudgetService }; } - public BudgetStatus GetBudgetStatus(Guid scanId) + public async Task GetBudgetStatusAsync(Guid scanId, CancellationToken ct) { var budget = _options.CurrentValue; - var usage = GetCurrentUsage(scanId); + var usage = await GetCurrentUsageAsync(scanId, ct); return new BudgetStatus { @@ -92,13 +98,10 @@ public sealed class EvidenceBudgetService : IEvidenceBudgetService }; } - public async Task PruneToFitAsync( - Guid scanId, - long targetBytes, - CancellationToken ct) + public async Task PruneToFitAsync(Guid scanId, long targetBytes, CancellationToken ct) { var budget = _options.CurrentValue; - var usage = GetCurrentUsage(scanId); + var usage = await GetCurrentUsageAsync(scanId, ct); if (usage.TotalBytes <= targetBytes) { @@ -113,6 +116,8 @@ public sealed class EvidenceBudgetService : IEvidenceBudgetService var candidates = items .Where(i => !budget.AlwaysPreserve.Contains(i.Type)) .OrderBy(i => GetPrunePriority(i)) + .ThenBy(i => i.CreatedAt.UtcDateTime.Ticks) + .ThenBy(i => i.Id) .ToList(); long prunedBytes = 0; @@ -158,15 +163,15 @@ public sealed class EvidenceBudgetService : IEvidenceBudgetService }; } - private UsageStats GetCurrentUsage(Guid scanId) + private async Task GetCurrentUsageAsync(Guid scanId, CancellationToken ct) { // Implementation to calculate current usage from repository - var items = _repository.GetByScanIdAsync(scanId, CancellationToken.None) - .GetAwaiter().GetResult(); + var items = await _repository.GetByScanIdAsync(scanId, ct); var totalBytes = items.Sum(i => i.SizeBytes); var byType = items .GroupBy(i => i.Type) + .OrderBy(g => g.Key) .ToDictionary(g => g.Key, g => g.Sum(i => i.SizeBytes)); return new UsageStats diff --git a/src/__Libraries/StellaOps.Evidence/Retention/RetentionTierManager.cs b/src/__Libraries/StellaOps.Evidence/Retention/RetentionTierManager.cs index a202a8bfd..f4cef0647 100644 --- a/src/__Libraries/StellaOps.Evidence/Retention/RetentionTierManager.cs +++ b/src/__Libraries/StellaOps.Evidence/Retention/RetentionTierManager.cs @@ -15,24 +15,27 @@ public sealed class RetentionTierManager : IRetentionTierManager private readonly IEvidenceRepository _repository; private readonly IArchiveStorage _archiveStorage; private readonly IOptionsMonitor _options; + private readonly TimeProvider _timeProvider; public RetentionTierManager( IEvidenceRepository repository, IArchiveStorage archiveStorage, - IOptionsMonitor options) + IOptionsMonitor options, + TimeProvider? timeProvider = null) { _repository = repository; _archiveStorage = archiveStorage; _options = options; + _timeProvider = timeProvider ?? TimeProvider.System; } public async Task RunMigrationAsync(CancellationToken ct) { var budget = _options.CurrentValue; - var now = DateTimeOffset.UtcNow; + var now = _timeProvider.GetUtcNow(); var migrated = new List(); - // Hot → Warm + // Hot -> Warm var hotExpiry = now - budget.RetentionPolicies[RetentionTier.Hot].Duration; var toWarm = await _repository.GetOlderThanAsync(RetentionTier.Hot, hotExpiry, ct); foreach (var item in toWarm) @@ -41,7 +44,7 @@ public sealed class RetentionTierManager : IRetentionTierManager migrated.Add(new MigratedItem(item.Id, RetentionTier.Hot, RetentionTier.Warm)); } - // Warm → Cold + // Warm -> Cold var warmExpiry = now - budget.RetentionPolicies[RetentionTier.Warm].Duration; var toCold = await _repository.GetOlderThanAsync(RetentionTier.Warm, warmExpiry, ct); foreach (var item in toCold) @@ -50,7 +53,7 @@ public sealed class RetentionTierManager : IRetentionTierManager migrated.Add(new MigratedItem(item.Id, RetentionTier.Warm, RetentionTier.Cold)); } - // Cold → Archive + // Cold -> Archive var coldExpiry = now - budget.RetentionPolicies[RetentionTier.Cold].Duration; var toArchive = await _repository.GetOlderThanAsync(RetentionTier.Cold, coldExpiry, ct); foreach (var item in toArchive) @@ -69,7 +72,7 @@ public sealed class RetentionTierManager : IRetentionTierManager public RetentionTier GetCurrentTier(EvidenceItem item) { var budget = _options.CurrentValue; - var age = DateTimeOffset.UtcNow - item.CreatedAt; + var age = _timeProvider.GetUtcNow() - item.CreatedAt; if (age < budget.RetentionPolicies[RetentionTier.Hot].Duration) return RetentionTier.Hot; @@ -122,15 +125,13 @@ public sealed class RetentionTierManager : IRetentionTierManager await _repository.MoveToTierAsync(item.Id, RetentionTier.Hot, ct); } - private async Task CompressAsync( + private Task CompressAsync( EvidenceItem item, CompressionLevel level, CancellationToken ct) { - // Placeholder for compression logic - // In real implementation, would read content, compress, and return - await Task.CompletedTask; - return Array.Empty(); + return Task.FromException(new NotSupportedException( + "Compression requires repository content retrieval, which is not implemented.")); } } diff --git a/src/__Libraries/StellaOps.Evidence/Serialization/EvidenceIndexSerializer.cs b/src/__Libraries/StellaOps.Evidence/Serialization/EvidenceIndexSerializer.cs index 35b7765aa..c5edd0b47 100644 --- a/src/__Libraries/StellaOps.Evidence/Serialization/EvidenceIndexSerializer.cs +++ b/src/__Libraries/StellaOps.Evidence/Serialization/EvidenceIndexSerializer.cs @@ -13,18 +13,22 @@ namespace StellaOps.Evidence.Serialization; /// public static class EvidenceIndexSerializer { - private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web) + private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = false, PropertyNamingPolicy = JsonNamingPolicy.CamelCase, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping + Encoder = JavaScriptEncoder.Default, + PropertyNameCaseInsensitive = true, + Converters = + { + new JsonStringEnumConverter(null, allowIntegerValues: false) + } }; public static string Serialize(EvidenceIndex index) { - var jsonBytes = JsonSerializer.SerializeToUtf8Bytes(index, JsonOptions); - var canonicalBytes = CanonJson.CanonicalizeParsedJson(jsonBytes); + var canonicalBytes = CanonJson.Canonicalize(index, JsonOptions); return Encoding.UTF8.GetString(canonicalBytes); } @@ -37,8 +41,8 @@ public static class EvidenceIndexSerializer public static string ComputeDigest(EvidenceIndex index) { var withoutDigest = index with { IndexDigest = null }; - var json = Serialize(withoutDigest); - var hash = SHA256.HashData(Encoding.UTF8.GetBytes(json)); + var canonicalBytes = CanonJson.Canonicalize(withoutDigest, JsonOptions); + var hash = SHA256.HashData(canonicalBytes); return Convert.ToHexString(hash).ToLowerInvariant(); } diff --git a/src/__Libraries/StellaOps.Evidence/Services/EvidenceLinker.cs b/src/__Libraries/StellaOps.Evidence/Services/EvidenceLinker.cs index 6fd94359a..15746bc87 100644 --- a/src/__Libraries/StellaOps.Evidence/Services/EvidenceLinker.cs +++ b/src/__Libraries/StellaOps.Evidence/Services/EvidenceLinker.cs @@ -1,6 +1,8 @@ using System.Collections.Immutable; using StellaOps.Evidence.Models; using StellaOps.Evidence.Serialization; +using System.Globalization; +using StellaOps.Determinism; namespace StellaOps.Evidence.Services; @@ -9,6 +11,8 @@ namespace StellaOps.Evidence.Services; /// public sealed class EvidenceLinker : IEvidenceLinker { + private readonly TimeProvider _timeProvider; + private readonly IGuidProvider _guidProvider; private readonly object _lock = new(); private readonly List _sboms = []; private readonly List _attestations = []; @@ -17,6 +21,17 @@ public sealed class EvidenceLinker : IEvidenceLinker private readonly List _unknowns = []; private ToolChainEvidence? _toolChain; + public EvidenceLinker() + : this(TimeProvider.System, SystemGuidProvider.Instance) + { + } + + public EvidenceLinker(TimeProvider timeProvider, IGuidProvider guidProvider) + { + _timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider)); + _guidProvider = guidProvider ?? throw new ArgumentNullException(nameof(guidProvider)); + } + public void AddSbom(SbomEvidence sbom) { lock (_lock) @@ -84,23 +99,76 @@ public sealed class EvidenceLinker : IEvidenceLinker unknowns = _unknowns.ToImmutableArray(); } + var orderedSboms = sboms + .OrderBy(s => s.Digest, StringComparer.Ordinal) + .ThenBy(s => s.SbomId, StringComparer.Ordinal) + .ThenBy(s => s.Format, StringComparer.Ordinal) + .ThenBy(s => NormalizeSortKey(s.Uri), StringComparer.Ordinal) + .ThenBy(s => s.ComponentCount) + .ThenBy(s => s.GeneratedAt.UtcDateTime.Ticks) + .ToImmutableArray(); + + var orderedAttestations = attestations + .OrderBy(a => a.Type, StringComparer.Ordinal) + .ThenBy(a => a.Digest, StringComparer.Ordinal) + .ThenBy(a => a.SignerKeyId, StringComparer.Ordinal) + .ThenBy(a => a.AttestationId, StringComparer.Ordinal) + .ThenBy(a => a.SignedAt.UtcDateTime.Ticks) + .ThenBy(a => a.SignatureValid) + .ThenBy(a => NormalizeSortKey(a.RekorLogIndex), StringComparer.Ordinal) + .ToImmutableArray(); + + var orderedVex = vexDocuments + .Select(v => v with + { + AffectedVulnerabilities = v.AffectedVulnerabilities + .OrderBy(id => id, StringComparer.Ordinal) + .ToImmutableArray() + }) + .OrderBy(v => v.Digest, StringComparer.Ordinal) + .ThenBy(v => v.VexId, StringComparer.Ordinal) + .ThenBy(v => v.Format, StringComparer.Ordinal) + .ThenBy(v => v.Source, StringComparer.Ordinal) + .ThenBy(v => v.StatementCount) + .ToImmutableArray(); + + var orderedReachability = reachabilityProofs + .OrderBy(r => r.VulnerabilityId, StringComparer.Ordinal) + .ThenBy(r => r.ComponentPurl, StringComparer.Ordinal) + .ThenBy(r => r.Status) + .ThenBy(r => NormalizeSortKey(r.EntryPoint), StringComparer.Ordinal) + .ThenBy(r => r.Digest, StringComparer.Ordinal) + .ThenBy(r => r.ProofId, StringComparer.Ordinal) + .ToImmutableArray(); + + var orderedUnknowns = unknowns + .OrderBy(u => u.ReasonCode, StringComparer.Ordinal) + .ThenBy(u => NormalizeSortKey(u.VulnerabilityId), StringComparer.Ordinal) + .ThenBy(u => NormalizeSortKey(u.ComponentPurl), StringComparer.Ordinal) + .ThenBy(u => u.Severity) + .ThenBy(u => u.UnknownId, StringComparer.Ordinal) + .ThenBy(u => u.Description, StringComparer.Ordinal) + .ToImmutableArray(); + var index = new EvidenceIndex { - IndexId = Guid.NewGuid().ToString(), + IndexId = _guidProvider.NewGuid().ToString("D", CultureInfo.InvariantCulture), SchemaVersion = "1.0.0", Verdict = verdict, - Sboms = sboms, - Attestations = attestations, - VexDocuments = vexDocuments, - ReachabilityProofs = reachabilityProofs, - Unknowns = unknowns, + Sboms = orderedSboms, + Attestations = orderedAttestations, + VexDocuments = orderedVex, + ReachabilityProofs = orderedReachability, + Unknowns = orderedUnknowns, ToolChain = toolChain, RunManifestDigest = runManifestDigest, - CreatedAt = DateTimeOffset.UtcNow + CreatedAt = _timeProvider.GetUtcNow() }; return EvidenceIndexSerializer.WithDigest(index); } + + private static string NormalizeSortKey(string? value) => value ?? string.Empty; } public interface IEvidenceLinker diff --git a/src/__Libraries/StellaOps.Evidence/Services/EvidenceQueryService.cs b/src/__Libraries/StellaOps.Evidence/Services/EvidenceQueryService.cs index 0d2992623..0508403dc 100644 --- a/src/__Libraries/StellaOps.Evidence/Services/EvidenceQueryService.cs +++ b/src/__Libraries/StellaOps.Evidence/Services/EvidenceQueryService.cs @@ -10,8 +10,15 @@ public sealed class EvidenceQueryService : IEvidenceQueryService public IEnumerable GetAttestationsForSbom( EvidenceIndex index, string sbomDigest) { + var sbomExists = index.Sboms.Any(s => string.Equals(s.Digest, sbomDigest, StringComparison.Ordinal)); + if (!sbomExists) + { + return Array.Empty(); + } + return index.Attestations - .Where(a => a.Type == "sbom" && index.Sboms.Any(s => s.Digest == sbomDigest)); + .Where(a => string.Equals(a.Type, "sbom", StringComparison.Ordinal) && + string.Equals(a.Digest, sbomDigest, StringComparison.Ordinal)); } public IEnumerable GetReachabilityForVulnerability( diff --git a/src/__Libraries/StellaOps.Evidence/StellaOps.Evidence.csproj b/src/__Libraries/StellaOps.Evidence/StellaOps.Evidence.csproj index 559fb48c6..d58a4ae66 100644 --- a/src/__Libraries/StellaOps.Evidence/StellaOps.Evidence.csproj +++ b/src/__Libraries/StellaOps.Evidence/StellaOps.Evidence.csproj @@ -15,6 +15,7 @@ + diff --git a/src/__Libraries/StellaOps.Evidence/TASKS.md b/src/__Libraries/StellaOps.Evidence/TASKS.md index 874e3810e..8d9d384fe 100644 --- a/src/__Libraries/StellaOps.Evidence/TASKS.md +++ b/src/__Libraries/StellaOps.Evidence/TASKS.md @@ -7,4 +7,4 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229 | --- | --- | --- | | AUDIT-0082-M | DONE | Revalidated 2026-01-08; open findings tracked in audit report. | | AUDIT-0082-T | DONE | Revalidated 2026-01-08; open findings tracked in audit report. | -| AUDIT-0082-A | TODO | Revalidated 2026-01-08 (open findings). | +| AUDIT-0082-A | DONE | Applied 2026-01-13; determinism, schema validation, budget async, retention safeguards, tests. | diff --git a/src/__Libraries/StellaOps.Evidence/Validation/EvidenceIndexValidator.cs b/src/__Libraries/StellaOps.Evidence/Validation/EvidenceIndexValidator.cs index c0e2f4644..64be356a0 100644 --- a/src/__Libraries/StellaOps.Evidence/Validation/EvidenceIndexValidator.cs +++ b/src/__Libraries/StellaOps.Evidence/Validation/EvidenceIndexValidator.cs @@ -1,14 +1,46 @@ using StellaOps.Evidence.Models; using StellaOps.Evidence.Serialization; +using System.Text.Json; +using Json.Schema; namespace StellaOps.Evidence.Validation; public sealed class EvidenceIndexValidator : IEvidenceIndexValidator { + private readonly JsonSchema _schema; + + public EvidenceIndexValidator() + { + var schemaJson = SchemaLoader.LoadSchema("evidence-index.schema.json"); + _schema = JsonSchema.FromText(schemaJson, new BuildOptions + { + SchemaRegistry = new SchemaRegistry() + }); + } + public ValidationResult Validate(EvidenceIndex index) { + ArgumentNullException.ThrowIfNull(index); var errors = new List(); + var json = EvidenceIndexSerializer.Serialize(index); + using var document = JsonDocument.Parse(json); + var schemaResult = _schema.Evaluate(document.RootElement); + if (!schemaResult.IsValid) + { + if (schemaResult.Errors is not null) + { + foreach (var error in schemaResult.Errors) + { + errors.Add(new ValidationError("Schema", error.Value ?? "Unknown error")); + } + } + else + { + errors.Add(new ValidationError("Schema", "Schema validation failed")); + } + } + if (index.Sboms.Length == 0) { errors.Add(new ValidationError("Sboms", "At least one SBOM required")); @@ -25,7 +57,7 @@ public sealed class EvidenceIndexValidator : IEvidenceIndexValidator foreach (var proof in index.ReachabilityProofs) { if (proof.Status == ReachabilityStatus.Inconclusive && - !index.Unknowns.Any(u => u.VulnerabilityId == proof.VulnerabilityId)) + !index.Unknowns.Any(u => string.Equals(u.VulnerabilityId, proof.VulnerabilityId, StringComparison.Ordinal))) { errors.Add(new ValidationError("ReachabilityProofs", $"Inconclusive reachability for {proof.VulnerabilityId} not recorded as unknown")); diff --git a/src/__Libraries/__Tests/StellaOps.Evidence.Tests/Budgets/EvidenceBudgetServiceTests.cs b/src/__Libraries/__Tests/StellaOps.Evidence.Tests/Budgets/EvidenceBudgetServiceTests.cs index b51e311e6..9f5d8114d 100644 --- a/src/__Libraries/__Tests/StellaOps.Evidence.Tests/Budgets/EvidenceBudgetServiceTests.cs +++ b/src/__Libraries/__Tests/StellaOps.Evidence.Tests/Budgets/EvidenceBudgetServiceTests.cs @@ -3,12 +3,16 @@ using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Moq; using StellaOps.Evidence.Budgets; +using StellaOps.TestKit; using Xunit; namespace StellaOps.Evidence.Tests.Budgets; - +[Trait("Category", TestCategories.Unit)] public class EvidenceBudgetServiceTests { + private static readonly DateTimeOffset FixedNow = new(2026, 1, 13, 0, 0, 0, TimeSpan.Zero); + private static readonly Guid DefaultScanId = Guid.Parse("11111111-1111-1111-1111-111111111111"); + private readonly Mock _repository = new(); private readonly Mock> _options = new(); private readonly EvidenceBudgetService _service; @@ -27,24 +31,32 @@ public class EvidenceBudgetServiceTests } [Fact] - public void CheckBudget_WithinLimit_ReturnsSuccess() + public async Task CheckBudget_WithinLimit_ReturnsSuccess() { - var scanId = Guid.NewGuid(); - var item = CreateItem(type: EvidenceType.CallGraph, sizeBytes: 1024); + var scanId = DefaultScanId; + var item = CreateItem( + id: Guid.Parse("00000000-0000-0000-0000-000000000010"), + scanId: scanId, + type: EvidenceType.CallGraph, + sizeBytes: 1024); - var result = _service.CheckBudget(scanId, item); + var result = await _service.CheckBudgetAsync(scanId, item, CancellationToken.None); result.IsWithinBudget.Should().BeTrue(); result.Issues.Should().BeEmpty(); } [Fact] - public void CheckBudget_ExceedsTotal_ReturnsViolation() + public async Task CheckBudget_ExceedsTotal_ReturnsViolation() { var scanId = SetupScanAtBudgetLimit(); - var item = CreateItem(type: EvidenceType.CallGraph, sizeBytes: 10 * 1024 * 1024); // 10 MB over + var item = CreateItem( + id: Guid.Parse("00000000-0000-0000-0000-000000000011"), + scanId: scanId, + type: EvidenceType.CallGraph, + sizeBytes: 10 * 1024 * 1024); // 10 MB over - var result = _service.CheckBudget(scanId, item); + var result = await _service.CheckBudgetAsync(scanId, item, CancellationToken.None); result.IsWithinBudget.Should().BeFalse(); result.Issues.Should().Contain(i => i.Contains("total budget")); @@ -52,17 +64,25 @@ public class EvidenceBudgetServiceTests } [Fact] - public void CheckBudget_ExceedsTypeLimit_ReturnsViolation() + public async Task CheckBudget_ExceedsTypeLimit_ReturnsViolation() { - var scanId = Guid.NewGuid(); - var existingCallGraph = CreateItem(type: EvidenceType.CallGraph, sizeBytes: 49 * 1024 * 1024); + var scanId = Guid.Parse("11111111-1111-1111-1111-111111111112"); + var existingCallGraph = CreateItem( + id: Guid.Parse("00000000-0000-0000-0000-000000000012"), + scanId: scanId, + type: EvidenceType.CallGraph, + sizeBytes: 49 * 1024 * 1024); _repository.Setup(r => r.GetByScanIdAsync(scanId, It.IsAny())) .ReturnsAsync(new List { existingCallGraph }); // CallGraph limit is 50MB, adding 2MB would exceed - var item = CreateItem(type: EvidenceType.CallGraph, sizeBytes: 2 * 1024 * 1024); + var item = CreateItem( + id: Guid.Parse("00000000-0000-0000-0000-000000000013"), + scanId: scanId, + type: EvidenceType.CallGraph, + sizeBytes: 2 * 1024 * 1024); - var result = _service.CheckBudget(scanId, item); + var result = await _service.CheckBudgetAsync(scanId, item, CancellationToken.None); result.IsWithinBudget.Should().BeFalse(); result.Issues.Should().Contain(i => i.Contains("CallGraph budget")); @@ -71,10 +91,14 @@ public class EvidenceBudgetServiceTests [Fact] public async Task PruneToFitAsync_NoExcess_NoPruning() { - var scanId = Guid.NewGuid(); + var scanId = Guid.Parse("11111111-1111-1111-1111-111111111113"); var items = new List { - CreateItem(type: EvidenceType.Sbom, sizeBytes: 5 * 1024 * 1024) + CreateItem( + id: Guid.Parse("00000000-0000-0000-0000-000000000014"), + scanId: scanId, + type: EvidenceType.Sbom, + sizeBytes: 5 * 1024 * 1024) }; _repository.Setup(r => r.GetByScanIdAsync(scanId, It.IsAny())) .ReturnsAsync(items); @@ -100,13 +124,29 @@ public class EvidenceBudgetServiceTests [Fact] public async Task PruneToFitAsync_PrunesLowestPriorityFirst() { - var scanId = Guid.NewGuid(); + var scanId = Guid.Parse("11111111-1111-1111-1111-111111111114"); var items = new List { - CreateItem(id: Guid.NewGuid(), type: EvidenceType.RuntimeCapture, sizeBytes: 10 * 1024 * 1024), // Priority 1 - CreateItem(id: Guid.NewGuid(), type: EvidenceType.CallGraph, sizeBytes: 10 * 1024 * 1024), // Priority 2 - CreateItem(id: Guid.NewGuid(), type: EvidenceType.Sbom, sizeBytes: 10 * 1024 * 1024), // Priority 6 - CreateItem(id: Guid.NewGuid(), type: EvidenceType.Verdict, sizeBytes: 1 * 1024 * 1024) // Priority 9 (never prune) + CreateItem( + id: Guid.Parse("00000000-0000-0000-0000-000000000020"), + scanId: scanId, + type: EvidenceType.RuntimeCapture, + sizeBytes: 10 * 1024 * 1024), // Priority 1 + CreateItem( + id: Guid.Parse("00000000-0000-0000-0000-000000000021"), + scanId: scanId, + type: EvidenceType.CallGraph, + sizeBytes: 10 * 1024 * 1024), // Priority 2 + CreateItem( + id: Guid.Parse("00000000-0000-0000-0000-000000000022"), + scanId: scanId, + type: EvidenceType.Sbom, + sizeBytes: 10 * 1024 * 1024), // Priority 6 + CreateItem( + id: Guid.Parse("00000000-0000-0000-0000-000000000023"), + scanId: scanId, + type: EvidenceType.Verdict, + sizeBytes: 1 * 1024 * 1024) // Priority 9 (never prune) }; _repository.Setup(r => r.GetByScanIdAsync(scanId, It.IsAny())) .ReturnsAsync(items); @@ -121,18 +161,57 @@ public class EvidenceBudgetServiceTests } [Fact] - public void GetBudgetStatus_CalculatesUtilization() + public async Task PruneToFitAsync_UsesCreatedAtTieBreaker() { - var scanId = Guid.NewGuid(); + var scanId = Guid.Parse("11111111-1111-1111-1111-111111111120"); + var olderId = Guid.Parse("00000000-0000-0000-0000-000000000060"); + var newerId = Guid.Parse("00000000-0000-0000-0000-000000000061"); var items = new List { - CreateItem(type: EvidenceType.CallGraph, sizeBytes: 25 * 1024 * 1024), // 25 MB - CreateItem(type: EvidenceType.Sbom, sizeBytes: 5 * 1024 * 1024) // 5 MB + CreateItem( + id: newerId, + scanId: scanId, + type: EvidenceType.RuntimeCapture, + sizeBytes: 5 * 1024 * 1024, + createdAt: FixedNow.AddMinutes(2)), + CreateItem( + id: olderId, + scanId: scanId, + type: EvidenceType.RuntimeCapture, + sizeBytes: 5 * 1024 * 1024, + createdAt: FixedNow.AddMinutes(1)) }; _repository.Setup(r => r.GetByScanIdAsync(scanId, It.IsAny())) .ReturnsAsync(items); - var status = _service.GetBudgetStatus(scanId); + var result = await _service.PruneToFitAsync(scanId, 0, CancellationToken.None); + + result.ItemsPruned.Should().HaveCount(2); + result.ItemsPruned[0].ItemId.Should().Be(olderId); + result.ItemsPruned[1].ItemId.Should().Be(newerId); + } + + [Fact] + public async Task GetBudgetStatus_CalculatesUtilization() + { + var scanId = Guid.Parse("11111111-1111-1111-1111-111111111115"); + var items = new List + { + CreateItem( + id: Guid.Parse("00000000-0000-0000-0000-000000000030"), + scanId: scanId, + type: EvidenceType.CallGraph, + sizeBytes: 25 * 1024 * 1024), // 25 MB + CreateItem( + id: Guid.Parse("00000000-0000-0000-0000-000000000031"), + scanId: scanId, + type: EvidenceType.Sbom, + sizeBytes: 5 * 1024 * 1024) // 5 MB + }; + _repository.Setup(r => r.GetByScanIdAsync(scanId, It.IsAny())) + .ReturnsAsync(items); + + var status = await _service.GetBudgetStatusAsync(scanId, CancellationToken.None); status.ScanId.Should().Be(scanId); status.TotalBudgetBytes.Should().Be(100 * 1024 * 1024); // 100 MB @@ -142,17 +221,21 @@ public class EvidenceBudgetServiceTests } [Fact] - public void GetBudgetStatus_CalculatesPerTypeUtilization() + public async Task GetBudgetStatus_CalculatesPerTypeUtilization() { - var scanId = Guid.NewGuid(); + var scanId = Guid.Parse("11111111-1111-1111-1111-111111111116"); var items = new List { - CreateItem(type: EvidenceType.CallGraph, sizeBytes: 25 * 1024 * 1024) // 25 of 50 MB limit + CreateItem( + id: Guid.Parse("00000000-0000-0000-0000-000000000032"), + scanId: scanId, + type: EvidenceType.CallGraph, + sizeBytes: 25 * 1024 * 1024) // 25 of 50 MB limit }; _repository.Setup(r => r.GetByScanIdAsync(scanId, It.IsAny())) .ReturnsAsync(items); - var status = _service.GetBudgetStatus(scanId); + var status = await _service.GetBudgetStatusAsync(scanId, CancellationToken.None); status.ByType.Should().ContainKey(EvidenceType.CallGraph); var callGraphStatus = status.ByType[EvidenceType.CallGraph]; @@ -162,7 +245,7 @@ public class EvidenceBudgetServiceTests } [Fact] - public void CheckBudget_AutoPruneAction_SetsCanAutoPrune() + public async Task CheckBudget_AutoPruneAction_SetsCanAutoPrune() { var budget = new EvidenceBudget { @@ -172,16 +255,24 @@ public class EvidenceBudgetServiceTests }; _options.Setup(o => o.CurrentValue).Returns(budget); - var scanId = Guid.NewGuid(); + var scanId = Guid.Parse("11111111-1111-1111-1111-111111111117"); var items = new List { - CreateItem(type: EvidenceType.Sbom, sizeBytes: 1000) + CreateItem( + id: Guid.Parse("00000000-0000-0000-0000-000000000033"), + scanId: scanId, + type: EvidenceType.Sbom, + sizeBytes: 1000) }; _repository.Setup(r => r.GetByScanIdAsync(scanId, It.IsAny())) .ReturnsAsync(items); - var item = CreateItem(type: EvidenceType.CallGraph, sizeBytes: 100); - var result = _service.CheckBudget(scanId, item); + var item = CreateItem( + id: Guid.Parse("00000000-0000-0000-0000-000000000034"), + scanId: scanId, + type: EvidenceType.CallGraph, + sizeBytes: 100); + var result = await _service.CheckBudgetAsync(scanId, item, CancellationToken.None); result.IsWithinBudget.Should().BeFalse(); result.RecommendedAction.Should().Be(BudgetExceededAction.AutoPrune); @@ -190,15 +281,39 @@ public class EvidenceBudgetServiceTests private Guid SetupScanAtBudgetLimit() { - var scanId = Guid.NewGuid(); + var scanId = Guid.Parse("11111111-1111-1111-1111-111111111118"); var items = new List { - CreateItem(type: EvidenceType.CallGraph, sizeBytes: 50 * 1024 * 1024), - CreateItem(type: EvidenceType.RuntimeCapture, sizeBytes: 20 * 1024 * 1024), - CreateItem(type: EvidenceType.Sbom, sizeBytes: 10 * 1024 * 1024), - CreateItem(type: EvidenceType.PolicyTrace, sizeBytes: 5 * 1024 * 1024), - CreateItem(type: EvidenceType.Verdict, sizeBytes: 5 * 1024 * 1024), - CreateItem(type: EvidenceType.Advisory, sizeBytes: 10 * 1024 * 1024) + CreateItem( + id: Guid.Parse("00000000-0000-0000-0000-000000000040"), + scanId: scanId, + type: EvidenceType.CallGraph, + sizeBytes: 50 * 1024 * 1024), + CreateItem( + id: Guid.Parse("00000000-0000-0000-0000-000000000041"), + scanId: scanId, + type: EvidenceType.RuntimeCapture, + sizeBytes: 20 * 1024 * 1024), + CreateItem( + id: Guid.Parse("00000000-0000-0000-0000-000000000042"), + scanId: scanId, + type: EvidenceType.Sbom, + sizeBytes: 10 * 1024 * 1024), + CreateItem( + id: Guid.Parse("00000000-0000-0000-0000-000000000043"), + scanId: scanId, + type: EvidenceType.PolicyTrace, + sizeBytes: 5 * 1024 * 1024), + CreateItem( + id: Guid.Parse("00000000-0000-0000-0000-000000000044"), + scanId: scanId, + type: EvidenceType.Verdict, + sizeBytes: 5 * 1024 * 1024), + CreateItem( + id: Guid.Parse("00000000-0000-0000-0000-000000000045"), + scanId: scanId, + type: EvidenceType.Advisory, + sizeBytes: 10 * 1024 * 1024) }; _repository.Setup(r => r.GetByScanIdAsync(scanId, It.IsAny())) .ReturnsAsync(items); @@ -207,15 +322,39 @@ public class EvidenceBudgetServiceTests private Guid SetupScanOverBudget() { - var scanId = Guid.NewGuid(); + var scanId = Guid.Parse("11111111-1111-1111-1111-111111111119"); var items = new List { - CreateItem(type: EvidenceType.CallGraph, sizeBytes: 40 * 1024 * 1024), - CreateItem(type: EvidenceType.RuntimeCapture, sizeBytes: 30 * 1024 * 1024), - CreateItem(type: EvidenceType.Sbom, sizeBytes: 20 * 1024 * 1024), - CreateItem(type: EvidenceType.PolicyTrace, sizeBytes: 10 * 1024 * 1024), - CreateItem(type: EvidenceType.Verdict, sizeBytes: 5 * 1024 * 1024), - CreateItem(type: EvidenceType.Attestation, sizeBytes: 5 * 1024 * 1024) + CreateItem( + id: Guid.Parse("00000000-0000-0000-0000-000000000046"), + scanId: scanId, + type: EvidenceType.CallGraph, + sizeBytes: 40 * 1024 * 1024), + CreateItem( + id: Guid.Parse("00000000-0000-0000-0000-000000000047"), + scanId: scanId, + type: EvidenceType.RuntimeCapture, + sizeBytes: 30 * 1024 * 1024), + CreateItem( + id: Guid.Parse("00000000-0000-0000-0000-000000000048"), + scanId: scanId, + type: EvidenceType.Sbom, + sizeBytes: 20 * 1024 * 1024), + CreateItem( + id: Guid.Parse("00000000-0000-0000-0000-000000000049"), + scanId: scanId, + type: EvidenceType.PolicyTrace, + sizeBytes: 10 * 1024 * 1024), + CreateItem( + id: Guid.Parse("00000000-0000-0000-0000-000000000050"), + scanId: scanId, + type: EvidenceType.Verdict, + sizeBytes: 5 * 1024 * 1024), + CreateItem( + id: Guid.Parse("00000000-0000-0000-0000-000000000051"), + scanId: scanId, + type: EvidenceType.Attestation, + sizeBytes: 5 * 1024 * 1024) }; _repository.Setup(r => r.GetByScanIdAsync(scanId, It.IsAny())) .ReturnsAsync(items); @@ -223,18 +362,21 @@ public class EvidenceBudgetServiceTests } private static EvidenceItem CreateItem( - Guid? id = null, + Guid id, + Guid? scanId = null, EvidenceType type = EvidenceType.CallGraph, - long sizeBytes = 1024) + long sizeBytes = 1024, + RetentionTier tier = RetentionTier.Hot, + DateTimeOffset? createdAt = null) { return new EvidenceItem { - Id = id ?? Guid.NewGuid(), - ScanId = Guid.NewGuid(), + Id = id, + ScanId = scanId ?? DefaultScanId, Type = type, SizeBytes = sizeBytes, - Tier = RetentionTier.Hot, - CreatedAt = DateTimeOffset.UtcNow + Tier = tier, + CreatedAt = createdAt ?? FixedNow }; } } diff --git a/src/__Libraries/__Tests/StellaOps.Evidence.Tests/EvidenceIndexTests.cs b/src/__Libraries/__Tests/StellaOps.Evidence.Tests/EvidenceIndexTests.cs index d385cdae2..63681370f 100644 --- a/src/__Libraries/__Tests/StellaOps.Evidence.Tests/EvidenceIndexTests.cs +++ b/src/__Libraries/__Tests/StellaOps.Evidence.Tests/EvidenceIndexTests.cs @@ -3,6 +3,7 @@ using FluentAssertions; using StellaOps.Evidence.Models; using StellaOps.Evidence.Serialization; using StellaOps.Evidence.Services; +using StellaOps.Evidence.Tests.TestUtilities; using StellaOps.Evidence.Validation; using Xunit; @@ -11,21 +12,76 @@ namespace StellaOps.Evidence.Tests; public class EvidenceIndexTests { + private static readonly DateTimeOffset FixedNow = new(2026, 1, 13, 0, 0, 0, TimeSpan.Zero); + private static readonly Guid FixedGuid = Guid.Parse("00000000-0000-0000-0000-000000000001"); + [Trait("Category", TestCategories.Unit)] [Fact] public void EvidenceLinker_BuildsIndexWithDigest() { - var linker = new EvidenceLinker(); + var linker = new EvidenceLinker(new FixedTimeProvider(FixedNow), new FixedGuidProvider(FixedGuid)); linker.SetToolChain(CreateToolChain()); - linker.AddSbom(new SbomEvidence("sbom-1", "cyclonedx-1.6", new string('a', 64), null, 10, DateTimeOffset.UtcNow)); - linker.AddAttestation(new AttestationEvidence("att-1", "sbom", new string('b', 64), "key", true, DateTimeOffset.UtcNow, null)); + linker.AddSbom(new SbomEvidence("sbom-1", "cyclonedx-1.6", new string('a', 64), null, 10, FixedNow)); + linker.AddAttestation(new AttestationEvidence("att-1", "sbom", new string('b', 64), "key", true, FixedNow, null)); var index = linker.Build(new VerdictReference("verdict-1", new string('c', 64), VerdictOutcome.Pass, "1.0.0"), "digest"); + index.IndexId.Should().Be(FixedGuid.ToString("D")); + index.CreatedAt.Should().Be(FixedNow); index.IndexDigest.Should().NotBeNullOrEmpty(); index.Sboms.Should().HaveCount(1); } + [Trait("Category", TestCategories.Unit)] + [Fact] + public void EvidenceLinker_SortsEvidenceDeterministically() + { + var linker = new EvidenceLinker(new FixedTimeProvider(FixedNow), new FixedGuidProvider(FixedGuid)); + linker.SetToolChain(CreateToolChain()); + linker.AddSbom(new SbomEvidence("sbom-b", "cyclonedx-1.6", new string('b', 64), null, 12, FixedNow.AddMinutes(1))); + linker.AddSbom(new SbomEvidence("sbom-a", "cyclonedx-1.6", new string('a', 64), null, 10, FixedNow)); + linker.AddAttestation(new AttestationEvidence("att-b", "sbom", new string('b', 64), "key", true, FixedNow.AddMinutes(2), null)); + linker.AddAttestation(new AttestationEvidence("att-a", "sbom", new string('a', 64), "key", true, FixedNow.AddMinutes(1), null)); + linker.AddVex(new VexEvidence( + "vex-1", + "openvex", + new string('c', 64), + "vendor", + 2, + ImmutableArray.Create("CVE-2024-0002", "CVE-2024-0001"))); + linker.AddReachabilityProof(new ReachabilityEvidence( + "proof-2", + "CVE-2024-0002", + "pkg:npm/zzz@1.0.0", + ReachabilityStatus.NotReachable, + "main", + ImmutableArray.Create("main"), + new string('e', 64))); + linker.AddReachabilityProof(new ReachabilityEvidence( + "proof-1", + "CVE-2024-0001", + "pkg:npm/aaa@1.0.0", + ReachabilityStatus.Reachable, + "init", + ImmutableArray.Create("init"), + new string('d', 64))); + linker.AddUnknown(new UnknownEvidence("unk-b", "B", "Second", null, "CVE-2024-0002", UnknownSeverity.Medium)); + linker.AddUnknown(new UnknownEvidence("unk-a", "A", "First", null, "CVE-2024-0001", UnknownSeverity.Low)); + + var index = linker.Build(new VerdictReference("verdict-1", new string('f', 64), VerdictOutcome.Pass, "1.0.0"), "digest"); + + index.Sboms.Select(s => s.Digest).Should() + .ContainInOrder(new string('a', 64), new string('b', 64)); + index.Attestations.Select(a => a.Digest).Should() + .ContainInOrder(new string('a', 64), new string('b', 64)); + index.VexDocuments[0].AffectedVulnerabilities.Should() + .ContainInOrder("CVE-2024-0001", "CVE-2024-0002"); + index.ReachabilityProofs.Select(r => r.VulnerabilityId).Should() + .ContainInOrder("CVE-2024-0001", "CVE-2024-0002"); + index.Unknowns.Select(u => u.ReasonCode).Should() + .ContainInOrder("A", "B"); + } + [Trait("Category", TestCategories.Unit)] [Fact] public void EvidenceValidator_FlagsMissingSbom() @@ -37,6 +93,70 @@ public class EvidenceIndexTests result.IsValid.Should().BeFalse(); } + [Trait("Category", TestCategories.Unit)] + [Fact] + public void EvidenceValidator_FlagsInvalidSignature() + { + var index = CreateIndex() with + { + Attestations = ImmutableArray.Create( + new AttestationEvidence("att-1", "sbom", new string('b', 64), "key", false, FixedNow, null)) + }; + var validator = new EvidenceIndexValidator(); + var result = validator.Validate(index); + + result.IsValid.Should().BeFalse(); + result.Errors.Should().Contain(e => e.Field == "Attestations"); + } + + [Trait("Category", TestCategories.Unit)] + [Fact] + public void EvidenceValidator_FlagsDigestMismatch() + { + var index = EvidenceIndexSerializer.WithDigest(CreateIndex()) with { SchemaVersion = "2.0.0" }; + var validator = new EvidenceIndexValidator(); + var result = validator.Validate(index); + + result.IsValid.Should().BeFalse(); + result.Errors.Should().Contain(e => e.Field == "IndexDigest"); + } + + [Trait("Category", TestCategories.Unit)] + [Fact] + public void EvidenceValidator_FlagsMissingUnknownForInconclusive() + { + var index = CreateIndex() with + { + ReachabilityProofs = ImmutableArray.Create( + new ReachabilityEvidence( + "proof-1", + "CVE-2024-0001", + "pkg:npm/foo@1.0.0", + ReachabilityStatus.Inconclusive, + null, + ImmutableArray.Create("main"), + new string('e', 64))), + Unknowns = [] + }; + var validator = new EvidenceIndexValidator(); + var result = validator.Validate(index); + + result.IsValid.Should().BeFalse(); + result.Errors.Should().Contain(e => e.Field == "ReachabilityProofs"); + } + + [Trait("Category", TestCategories.Unit)] + [Fact] + public void EvidenceValidator_FlagsSchemaError() + { + var index = CreateIndex() with { IndexId = null! }; + var validator = new EvidenceIndexValidator(); + var result = validator.Validate(index); + + result.IsValid.Should().BeFalse(); + result.Errors.Should().Contain(e => e.Field == "Schema"); + } + [Trait("Category", TestCategories.Unit)] [Fact] public void EvidenceSerializer_RoundTrip_PreservesFields() @@ -59,21 +179,44 @@ public class EvidenceIndexTests report.AttestationCount.Should().Be(1); } + [Trait("Category", TestCategories.Unit)] + [Fact] + public void EvidenceQueryService_FiltersAttestationsBySbomDigest() + { + var sbomA = new SbomEvidence("sbom-a", "cyclonedx-1.6", new string('a', 64), null, 10, FixedNow); + var sbomB = new SbomEvidence("sbom-b", "cyclonedx-1.6", new string('b', 64), null, 10, FixedNow); + var attA = new AttestationEvidence("att-a", "sbom", new string('a', 64), "key", true, FixedNow, null); + var attB = new AttestationEvidence("att-b", "sbom", new string('b', 64), "key", true, FixedNow, null); + var index = CreateIndex() with + { + Sboms = ImmutableArray.Create(sbomA, sbomB), + Attestations = ImmutableArray.Create(attA, attB) + }; + + var service = new EvidenceQueryService(); + var results = service.GetAttestationsForSbom(index, new string('b', 64)).ToList(); + var missing = service.GetAttestationsForSbom(index, new string('z', 64)).ToList(); + + results.Should().HaveCount(1); + results[0].AttestationId.Should().Be("att-b"); + missing.Should().BeEmpty(); + } + private static EvidenceIndex CreateIndex() { return new EvidenceIndex { - IndexId = Guid.NewGuid().ToString(), + IndexId = FixedGuid.ToString("D"), SchemaVersion = "1.0.0", Verdict = new VerdictReference("verdict-1", new string('c', 64), VerdictOutcome.Pass, "1.0.0"), - Sboms = ImmutableArray.Create(new SbomEvidence("sbom-1", "cyclonedx-1.6", new string('a', 64), null, 10, DateTimeOffset.UtcNow)), - Attestations = ImmutableArray.Create(new AttestationEvidence("att-1", "sbom", new string('b', 64), "key", true, DateTimeOffset.UtcNow, null)), + Sboms = ImmutableArray.Create(new SbomEvidence("sbom-1", "cyclonedx-1.6", new string('a', 64), null, 10, FixedNow)), + Attestations = ImmutableArray.Create(new AttestationEvidence("att-1", "sbom", new string('b', 64), "key", true, FixedNow, null)), VexDocuments = ImmutableArray.Create(new VexEvidence("vex-1", "openvex", new string('d', 64), "vendor", 1, ImmutableArray.Create("CVE-2024-0001"))), ReachabilityProofs = ImmutableArray.Create(new ReachabilityEvidence("proof-1", "CVE-2024-0001", "pkg:npm/foo@1.0.0", ReachabilityStatus.Reachable, "main", ImmutableArray.Create("main"), new string('e', 64))), Unknowns = ImmutableArray.Create(new UnknownEvidence("unk-1", "U-RCH", "Reachability inconclusive", "pkg:npm/foo", "CVE-2024-0001", UnknownSeverity.Medium)), ToolChain = CreateToolChain(), RunManifestDigest = new string('f', 64), - CreatedAt = DateTimeOffset.UtcNow + CreatedAt = FixedNow }; } diff --git a/src/__Libraries/__Tests/StellaOps.Evidence.Tests/Retention/RetentionTierManagerTests.cs b/src/__Libraries/__Tests/StellaOps.Evidence.Tests/Retention/RetentionTierManagerTests.cs new file mode 100644 index 000000000..a6022724b --- /dev/null +++ b/src/__Libraries/__Tests/StellaOps.Evidence.Tests/Retention/RetentionTierManagerTests.cs @@ -0,0 +1,127 @@ +using FluentAssertions; +using Microsoft.Extensions.Options; +using Moq; +using StellaOps.Evidence.Budgets; +using StellaOps.Evidence.Retention; +using StellaOps.Evidence.Tests.TestUtilities; +using StellaOps.TestKit; +using Xunit; + +namespace StellaOps.Evidence.Tests.Retention; + +[Trait("Category", TestCategories.Unit)] +public class RetentionTierManagerTests +{ + private static readonly DateTimeOffset FixedNow = new(2026, 1, 13, 0, 0, 0, TimeSpan.Zero); + private static readonly Guid DefaultScanId = Guid.Parse("22222222-2222-2222-2222-222222222222"); + + [Fact] + public async Task RunMigrationAsync_MigratesItemsAcrossTiers() + { + var budget = new EvidenceBudget + { + MaxScanSizeBytes = 100, + RetentionPolicies = new Dictionary + { + [RetentionTier.Hot] = new RetentionPolicy { Duration = TimeSpan.FromDays(1) }, + [RetentionTier.Warm] = new RetentionPolicy { Duration = TimeSpan.FromDays(2) }, + [RetentionTier.Cold] = new RetentionPolicy { Duration = TimeSpan.FromDays(3) }, + [RetentionTier.Archive] = new RetentionPolicy { Duration = TimeSpan.FromDays(4) } + } + }; + var options = new Mock>(); + options.Setup(o => o.CurrentValue).Returns(budget); + var repository = new Mock(); + var archiveStorage = new Mock(); + + var hotItem = CreateItem(Guid.Parse("00000000-0000-0000-0000-000000000101"), EvidenceType.CallGraph, RetentionTier.Hot); + var warmItem = CreateItem(Guid.Parse("00000000-0000-0000-0000-000000000102"), EvidenceType.Sbom, RetentionTier.Warm); + var coldItem = CreateItem(Guid.Parse("00000000-0000-0000-0000-000000000103"), EvidenceType.Vex, RetentionTier.Cold); + + repository.Setup(r => r.GetOlderThanAsync(RetentionTier.Hot, It.IsAny(), It.IsAny())) + .ReturnsAsync(new List { hotItem }); + repository.Setup(r => r.GetOlderThanAsync(RetentionTier.Warm, It.IsAny(), It.IsAny())) + .ReturnsAsync(new List { warmItem }); + repository.Setup(r => r.GetOlderThanAsync(RetentionTier.Cold, It.IsAny(), It.IsAny())) + .ReturnsAsync(new List { coldItem }); + + var manager = new RetentionTierManager( + repository.Object, + archiveStorage.Object, + options.Object, + new FixedTimeProvider(FixedNow)); + + var result = await manager.RunMigrationAsync(CancellationToken.None); + + result.MigratedCount.Should().Be(3); + result.Items.Should().Contain(i => i.ItemId == hotItem.Id && i.FromTier == RetentionTier.Hot && i.ToTier == RetentionTier.Warm); + result.Items.Should().Contain(i => i.ItemId == warmItem.Id && i.FromTier == RetentionTier.Warm && i.ToTier == RetentionTier.Cold); + result.Items.Should().Contain(i => i.ItemId == coldItem.Id && i.FromTier == RetentionTier.Cold && i.ToTier == RetentionTier.Archive); + + repository.Verify(r => r.MoveToTierAsync(hotItem.Id, RetentionTier.Warm, It.IsAny()), Times.Once); + repository.Verify(r => r.MoveToTierAsync(warmItem.Id, RetentionTier.Cold, It.IsAny()), Times.Once); + repository.Verify(r => r.MoveToTierAsync(coldItem.Id, RetentionTier.Archive, It.IsAny()), Times.Once); + repository.Verify(r => r.UpdateContentAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public async Task EnsureAuditCompleteAsync_RestoresArchiveItems() + { + var budget = new EvidenceBudget + { + MaxScanSizeBytes = 100, + RetentionPolicies = EvidenceBudget.Default.RetentionPolicies, + AlwaysPreserve = new HashSet { EvidenceType.Verdict } + }; + var options = new Mock>(); + options.Setup(o => o.CurrentValue).Returns(budget); + var repository = new Mock(); + var archiveStorage = new Mock(); + + var archiveItem = CreateItem( + Guid.Parse("00000000-0000-0000-0000-000000000201"), + EvidenceType.Verdict, + RetentionTier.Archive, + archiveKey: "archive-1"); + var coldItem = CreateItem( + Guid.Parse("00000000-0000-0000-0000-000000000202"), + EvidenceType.Verdict, + RetentionTier.Cold); + + repository.Setup(r => r.GetByScanIdAndTypeAsync(DefaultScanId, EvidenceType.Verdict, It.IsAny())) + .ReturnsAsync(new List { archiveItem, coldItem }); + archiveStorage.Setup(a => a.RetrieveAsync("archive-1", It.IsAny())) + .ReturnsAsync(new byte[] { 0x01, 0x02 }); + + var manager = new RetentionTierManager( + repository.Object, + archiveStorage.Object, + options.Object, + new FixedTimeProvider(FixedNow)); + + await manager.EnsureAuditCompleteAsync(DefaultScanId, CancellationToken.None); + + archiveStorage.Verify(a => a.RetrieveAsync("archive-1", It.IsAny()), Times.Once); + repository.Verify(r => r.UpdateContentAsync(archiveItem.Id, It.IsAny(), It.IsAny()), Times.Once); + repository.Verify(r => r.MoveToTierAsync(archiveItem.Id, RetentionTier.Hot, It.IsAny()), Times.Once); + repository.Verify(r => r.MoveToTierAsync(coldItem.Id, RetentionTier.Hot, It.IsAny()), Times.Once); + } + + private static EvidenceItem CreateItem( + Guid id, + EvidenceType type, + RetentionTier tier, + string? archiveKey = null) + { + return new EvidenceItem + { + Id = id, + ScanId = DefaultScanId, + Type = type, + SizeBytes = 1024, + Tier = tier, + CreatedAt = FixedNow.AddDays(-10), + ArchiveKey = archiveKey + }; + } +} diff --git a/src/__Libraries/__Tests/StellaOps.Evidence.Tests/StellaOps.Evidence.Tests.csproj b/src/__Libraries/__Tests/StellaOps.Evidence.Tests/StellaOps.Evidence.Tests.csproj index c9a8c2f5c..2fe889619 100644 --- a/src/__Libraries/__Tests/StellaOps.Evidence.Tests/StellaOps.Evidence.Tests.csproj +++ b/src/__Libraries/__Tests/StellaOps.Evidence.Tests/StellaOps.Evidence.Tests.csproj @@ -11,7 +11,8 @@ + - \ No newline at end of file + diff --git a/src/__Libraries/__Tests/StellaOps.Evidence.Tests/TestUtilities/FixedGuidProvider.cs b/src/__Libraries/__Tests/StellaOps.Evidence.Tests/TestUtilities/FixedGuidProvider.cs new file mode 100644 index 000000000..a9c074152 --- /dev/null +++ b/src/__Libraries/__Tests/StellaOps.Evidence.Tests/TestUtilities/FixedGuidProvider.cs @@ -0,0 +1,15 @@ +using StellaOps.Determinism; + +namespace StellaOps.Evidence.Tests.TestUtilities; + +internal sealed class FixedGuidProvider : IGuidProvider +{ + private readonly Guid _guid; + + public FixedGuidProvider(Guid guid) + { + _guid = guid; + } + + public Guid NewGuid() => _guid; +} diff --git a/src/__Libraries/__Tests/StellaOps.Evidence.Tests/TestUtilities/FixedTimeProvider.cs b/src/__Libraries/__Tests/StellaOps.Evidence.Tests/TestUtilities/FixedTimeProvider.cs new file mode 100644 index 000000000..6fc45504e --- /dev/null +++ b/src/__Libraries/__Tests/StellaOps.Evidence.Tests/TestUtilities/FixedTimeProvider.cs @@ -0,0 +1,13 @@ +namespace StellaOps.Evidence.Tests.TestUtilities; + +internal sealed class FixedTimeProvider : TimeProvider +{ + private readonly DateTimeOffset _now; + + public FixedTimeProvider(DateTimeOffset now) + { + _now = now; + } + + public override DateTimeOffset GetUtcNow() => _now; +}