diff --git a/.fallout/build.schema.json b/.fallout/build.schema.json index 74c6b5567..da3aba821 100644 --- a/.fallout/build.schema.json +++ b/.fallout/build.schema.json @@ -34,6 +34,7 @@ "GenerateTools", "Install", "Pack", + "PackageGuard", "Publish", "References", "ReportCoverage", diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0463d22b0..28a9b439c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -55,5 +55,7 @@ jobs: global-json-file: global.json - name: 'Restore: dotnet tools' run: dotnet tool restore - - name: 'Run: VerifyGeneratedTools, Test, Pack' - run: dotnet fallout VerifyGeneratedTools Test Pack + - name: 'Run: VerifyGeneratedTools, Test, Pack, PackageGuard' + run: dotnet fallout VerifyGeneratedTools Test Pack PackageGuard + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/publish-packages-release.yml b/.github/workflows/publish-packages-release.yml index 0fad258a8..f6f6d7d11 100644 --- a/.github/workflows/publish-packages-release.yml +++ b/.github/workflows/publish-packages-release.yml @@ -119,8 +119,8 @@ jobs: global-json-file: global.json - name: 'Restore: dotnet tools' run: dotnet tool restore - - name: 'Run: Test + Pack' - run: dotnet fallout Test Pack + - name: 'Run: Test + Pack + PackageGuard' + run: dotnet fallout Test Pack PackageGuard env: # We check out the tag above, so HEAD is detached and matches none of # version.json's publicReleaseRefSpec entries (they're all branch refs, @@ -128,6 +128,13 @@ jobs: # NB.GV treats the build as non-public and stamps a git-height suffix # onto every package — v10.4.0-rc.3 shipped as 10.4.0-rc.3.geabd043cc2. PublicRelease: true + # PackageGuard's own target (build/Build.PackageGuard.cs) is gated on either being on + # a long-lived branch or running in this workflow (GitHubActions.Workflow == + # "publish-packages-release") — the tag checkout above leaves HEAD detached, so the + # branch check alone can't pass here; validate-ref already proved the tag is + # reachable from a production branch. GITHUB_TOKEN lets it authenticate to GitHub + # (license lookups) instead of hitting anonymous rate limits. + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: 'Upload: package artifacts' uses: actions/upload-artifact@v7 with: @@ -135,6 +142,13 @@ jobs: path: output/packages/*.nupkg retention-days: 7 if-no-files-found: error + - name: 'Upload: PackageGuard SBOM + risk report' + uses: actions/upload-artifact@v7 + with: + name: packageguard + path: output/packageguard/* + retention-days: 7 + if-no-files-found: error # Tier 1 — production / nuget.org. **OPT-IN ONLY.** Tag pushes do NOT trigger # this job; you must invoke workflow_dispatch with publish-to-nugetorg=true. @@ -241,9 +255,11 @@ jobs: --skip-duplicate done - # Bundled artifact distribution. Attaches all nupkgs to the GitHub Release - # for the tag. Idempotent: if the release already exists (workflow_dispatch - # retry case), uploads or replaces missing assets via --clobber. + # Bundled artifact distribution. Attaches all nupkgs, plus the PackageGuard SBOM and HTML + # risk report, to the GitHub Release for the tag. Idempotent: if the release already exists + # (workflow_dispatch retry case), uploads or replaces missing assets via --clobber. The .sarif + # itself isn't attached here — it's reported to GitHub's code-scanning feature instead + # (security-scan.yml); the HTML is the human-readable counterpart of the same report. publish-github-releases: name: publish → GitHub Releases runs-on: ubuntu-latest @@ -262,6 +278,11 @@ jobs: with: name: packages path: output/packages + - name: 'Download: PackageGuard SBOM + risk report' + uses: actions/download-artifact@v8 + with: + name: packageguard + path: output/packageguard - name: 'Create or update GitHub Release with package artifacts' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -273,14 +294,21 @@ jobs: TAG="${GITHUB_REF_NAME}" fi echo "Tag: $TAG" + assets=(output/packages/*.nupkg) + if [ -f output/packageguard/sbom.json ]; then + assets+=("output/packageguard/sbom.json") + fi + if [ -f output/packageguard/risk-report.html ]; then + assets+=("output/packageguard/risk-report.html") + fi if gh release view "$TAG" > /dev/null 2>&1; then echo "Release $TAG already exists — uploading assets with --clobber." - gh release upload "$TAG" output/packages/*.nupkg --clobber + gh release upload "$TAG" "${assets[@]}" --clobber else echo "Creating new release $TAG." gh release create "$TAG" \ --title "$TAG" \ --target "$(git rev-parse HEAD)" \ --generate-notes \ - output/packages/*.nupkg + "${assets[@]}" fi diff --git a/.github/workflows/security-scan.yml b/.github/workflows/security-scan.yml new file mode 100644 index 000000000..8a389ba8f --- /dev/null +++ b/.github/workflows/security-scan.yml @@ -0,0 +1,67 @@ +# ------------------------------------------------------------------------------ +# +# +# This code was generated. +# +# - To turn off auto-generation set: +# +# [GitHubActions (AutoGenerate = false)] +# +# - To trigger manual generation invoke: +# +# fallout --generate-configuration GitHubActions_security-scan --host GitHubActions +# +# +# ------------------------------------------------------------------------------ + +name: security-scan + +on: + push: + branches: + - develop + - main + - 'release/*' + - 'support/*' + paths-ignore: + - 'docs/**' + - '.assets/**' + - '**/*.md' + +permissions: + security-events: write + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + ubuntu-latest: + name: ubuntu-latest + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + with: + fetch-depth: 0 + - name: 'Cache: .fallout/temp, ~/.nuget/packages' + uses: actions/cache@v6 + with: + path: | + .fallout/temp + ~/.nuget/packages + key: ${{ runner.os }}-${{ hashFiles('**/global.json', '**/*.csproj', '**/Directory.Packages.props') }} + - name: 'Setup: .NET SDK' + uses: actions/setup-dotnet@v6 + with: + global-json-file: global.json + - name: 'Restore: dotnet tools' + run: dotnet tool restore + - name: 'Run: PackageGuard' + run: dotnet fallout PackageGuard + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: 'Upload risk-report SARIF to GitHub code scanning' + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: output/packageguard/risk-report.sarif diff --git a/.packageguard/config.json b/.packageguard/config.json new file mode 100644 index 000000000..7a490cea3 --- /dev/null +++ b/.packageguard/config.json @@ -0,0 +1,21 @@ +{ + "Settings": { + "Allow": { + "Licenses": [ + "MIT", + "Apache-2.0", + "BSD-2-Clause", + "BSD-3-Clause", + "ISC", + "0BSD", + "MS-PL" + ], + // FluentAssertions and NetArchTest.Rules are MIT-licensed but don't publish a + // license expression/URL NuGet metadata can pick up. See #659. + "Packages": [ + "FluentAssertions", + "NetArchTest.Rules" + ] + } + } +} diff --git a/build/Build.CI.GitHubActions.cs b/build/Build.CI.GitHubActions.cs index 4fb503a1e..c31c5c081 100644 --- a/build/Build.CI.GitHubActions.cs +++ b/build/Build.CI.GitHubActions.cs @@ -1,9 +1,11 @@ +using System.Collections.Generic; using Fallout.Common.CI.GitHubActions; +using Fallout.Common.CI.GitHubActions.Configuration; using Fallout.Components; -// Two generated build workflows. Both run Test+Pack; both are GENERATED from the -// attributes below — edit here and regenerate (`./build.sh`), never hand-edit the -// `.yml`. +// Three generated workflows. build.yml and build-cross-platform.yml both run Test+Pack; all +// three are GENERATED from the attributes below — edit here and regenerate (`./build.sh`), +// never hand-edit the `.yml`. // // build.yml — the Linux PR gate, and the ONLY required status // check (job `ubuntu-latest`; branch protection keys on @@ -15,7 +17,12 @@ // merge SHA, keeping HEAD attached so // GitHubTasksTest.GitHubRepositoryFromLocalDirectoryTest // (which reads .git/HEAD via GitRepository.FromLocalDirectory) -// resolves a non-null branch. +// resolves a non-null branch. Also runs PackageGuard — its +// policy-violation check gates every PR, though the target skips +// SBOM/risk-report generation here (build/Build.PackageGuard.cs, +// IsOnLongLivedBranch — this checkout is never on one of the four). +// EnableGitHubToken avoids anonymous GitHub API rate-limiting on +// PackageGuard's license lookups, now that it runs on every PR. // // build-cross-platform.yml — macOS + Windows in ONE workflow (one job per image). // Cross-platform full Test+Pack is gated to RELEASE @@ -44,7 +51,8 @@ // long-lived and protected; all require the ubuntu-latest check. OnPullRequestBranches = new[] { DevelopBranch, MainBranch, ReleaseBranchPattern, SupportBranchPattern }, OnPullRequestExcludePaths = new[] { "docs/**", ".assets/**", "**/*.md" }, - InvokedTargets = new[] { nameof(VerifyGeneratedTools), nameof(ITest.Test), nameof(IPack.Pack) }, + InvokedTargets = new[] { nameof(VerifyGeneratedTools), nameof(ITest.Test), nameof(IPack.Pack), nameof(PackageGuard) }, + EnableGitHubToken = true, PublishArtifacts = false)] [GitHubActions( "build-cross-platform", @@ -61,7 +69,34 @@ OnPullRequestExcludePaths = new[] { "docs/**", ".assets/**", "**/*.md" }, InvokedTargets = new[] { nameof(ITest.Test), nameof(IPack.Pack) }, PublishArtifacts = false)] -partial class Build +// security-scan.yml — continuous SBOM + risk-report generation +// (build/Build.PackageGuard.cs). PackageGuard's policy-violation +// check already runs on every PR via build.yml above; this workflow +// is for the SBOM/SARIF/HTML side, which build.yml's target +// deliberately skips (its checkout is never one of the four +// long-lived branches, so IsOnLongLivedBranch is false there). +// Push-only, and only to develop/main/release/*/support/* — a push +// is when there's actually a new commit on one of those branches to +// report on. EnableGitHubToken feeds GITHUB_TOKEN to PackageGuard +// (avoids GitHub API rate-limiting on license lookups) and to the +// upload-sarif step's security-events:write use. +[GitHubActions( + "security-scan", + GitHubActionsImage.UbuntuLatest, + FetchDepth = 0, + ConcurrencyGroup = "${{ github.workflow }}-${{ github.ref }}", + ConcurrencyCancelInProgress = true, + OnPushBranches = new[] { DevelopBranch, MainBranch, ReleaseBranchPattern, SupportBranchPattern }, + OnPushExcludePaths = new[] { "docs/**", ".assets/**", "**/*.md" }, + InvokedTargets = new[] { nameof(PackageGuard) }, + EnableGitHubToken = true, + // Specifying any `permissions:` block switches the job from GitHub's default read-all to + // explicit-only — contents:read has to be listed too, or upload-sarif (and checkout) lose + // it. See GitHub's own upload-sarif docs for this exact pairing. + ReadPermissions = new[] { GitHubActionsPermissions.Contents }, + WritePermissions = new[] { GitHubActionsPermissions.SecurityEvents }, + PublishArtifacts = false)] +partial class Build : IConfigureGitHubActions { // The release workflow is intentionally hand-written at // .github/workflows/publish-packages-release.yml — that lets us name the GitHub @@ -71,4 +106,24 @@ partial class Build // workflow's `name:` — it gates ICreateGitHubRelease.CreateGitHubRelease // (Build.cs) to the release workflow only. const string ReleaseWorkflow = "publish-packages-release"; + + // Injects the SARIF upload after security-scan's "dotnet fallout PackageGuard" run step — + // GitHubActionsStepPosition.PostRun is exactly "after the run block, before the built-in + // artifact upload". Scoped to this one generated job by WorkflowName; other jobs get no + // insertions. + void IConfigureGitHubActions.ConfigureSteps(GitHubActionsStepPipeline pipeline) + { + if (pipeline.WorkflowName == "security-scan") + { + pipeline.Insert(GitHubActionsStepPosition.PostRun, new GitHubActionsCustomStep + { + Name = "Upload risk-report SARIF to GitHub code scanning", + Uses = "github/codeql-action/upload-sarif@v3", + With = new Dictionary + { + ["sarif_file"] = "output/packageguard/risk-report.sarif", + }, + }); + } + } } diff --git a/build/Build.PackageGuard.cs b/build/Build.PackageGuard.cs new file mode 100644 index 000000000..7285115a3 --- /dev/null +++ b/build/Build.PackageGuard.cs @@ -0,0 +1,60 @@ +using Fallout.Common; +using Fallout.Common.Git; +using Fallout.Common.IO; +using Fallout.Common.Tooling; +using Fallout.Common.Tools.PackageGuard; +using Fallout.Common.Utilities; +using Fallout.Components; + +partial class Build +{ + // PackageGuard's own CLI only exposes an env-var override for the risk-report path + // (see PackageGuard.json's help text on ReportRisk) — there's no `--report-risk ` + // argument in our wrapper, matching the repo-wide convention that bool CLI flags stay + // presence-only. Setting this process env var instead pins the SARIF/HTML pair to a + // deterministic path we can reference from CI (upload-sarif, the release asset step). + const string PackageGuardReportRiskPathOverrideEnvironmentVariable = "PACKAGEGUARD_REPORT_RISK_PATH_OVERRIDE"; + + AbsolutePath PackageGuardDirectory => OutputDirectory / "packageguard"; + AbsolutePath PackageGuardSbomFile => PackageGuardDirectory / "sbom.json"; + AbsolutePath PackageGuardSarifFile => PackageGuardDirectory / "risk-report.sarif"; + + // The SBOM and risk report (HTML + SARIF) are only worth generating where they're actually + // consumed: security-scan.yml uploads the SARIF on a push to one of these four branches, and + // the release workflow attaches the SBOM/HTML to the GitHub Release. Neither happens on a PR + // (build.yml checks out the contributor's own branch via github.head_ref, never one of + // these), or on a tag-triggered release checkout (detached HEAD — hence the + // GitHubActions.Workflow fallback, since that workflow's own validate-ref job already + // proved the tag is reachable from a production branch). + bool IsOnLongLivedBranch => + GitRepository.IsOnMainBranch() || + GitRepository.IsOnDevelopBranch() || + GitRepository.IsOnReleaseBranch() || + GitRepository.IsOnSupportBranch() || + GitHubActions?.Workflow == ReleaseWorkflow; + + // Runs unconditionally — this is the PR gate's policy-violation check (build.yml), so it has + // to run on every branch, including a contributor's feature branch. Only the SBOM/risk-report + // generation is restricted to the four long-lived branches, via IsOnLongLivedBranch below. + Target PackageGuard => _ => _ + .DependsOn() + .Produces(PackageGuardSbomFile) + .Produces(PackageGuardSarifFile) + .Produces(PackageGuardDirectory / "*.html") + .Executes(() => + { + var generateReports = IsOnLongLivedBranch; + + if (generateReports) + PackageGuardDirectory.CreateOrCleanDirectory(); + + PackageGuardTasks.PackageGuard(_ => _ + .SetProjectPath(Solution.Path) + .SetGitHubApiKey(From().GitHubToken) + .When(generateReports, _ => _ + .EnableReportRisk() + .SetSbom(SbomFormat.cyclonedx) + .SetSbomOutput(PackageGuardSbomFile) + .SetProcessEnvironmentVariable(PackageGuardReportRiskPathOverrideEnvironmentVariable, PackageGuardSarifFile))); + }); +} diff --git a/build/_build.csproj b/build/_build.csproj index 0b0a10b64..5c1d3324d 100644 --- a/build/_build.csproj +++ b/build/_build.csproj @@ -58,6 +58,7 @@ Build.cs as the canonical version source. Replaces GitVersion.Tool (#81). Direct dependency because _build.csproj turns off central package management. --> + diff --git a/docs/website/03-common/08-cli-tools.md b/docs/website/03-common/08-cli-tools.md index 63fe159b9..2cadfce8e 100644 --- a/docs/website/03-common/08-cli-tools.md +++ b/docs/website/03-common/08-cli-tools.md @@ -49,6 +49,7 @@ Interacting with third-party command-line interface tools (CLIs) is an essential | [Octopus](https://octopus.com/) | `build-information`, `create-release`, `deploy-release`, `pack`, `push` | | [OctoVersion](https://github.com/OctopusDeploy/OctoVersion) | `octoversion`, `octoversion` | | [OpenCover](https://github.com/OpenCover/opencover) | _Single top-level command_ | +| [PackageGuard](https://github.com/dennisdoomen/packageguard) | _Single top-level command_ | | [Paket](https://fsprojects.github.io/paket) | `pack`, `push`, `restore`, `update` | | [PowerShell](https://docs.microsoft.com/en-us/powershell/) | _Single top-level command_ | | [Pulumi](https://www.pulumi.com/) | `config`, `config cp`, `config get`, `config refresh`, `config rm`, `config set`, `destroy`, `new`, `preview`, `stack`, `stack change-secrets-provider`, `stack export`, `stack graph`, `stack history`, `stack import`, `stack init`, `stack ls`, `stack output`, `stack rename`, `stack rm`, `stack select`, `stack tag get`, `stack tag ls`, `stack tag rm`, `stack tag set`, `up` | diff --git a/src/Fallout.Build/VCS/GitRepositoryExtensions.cs b/src/Fallout.Build/VCS/GitRepositoryExtensions.cs index aa9806ae8..e65db3d45 100644 --- a/src/Fallout.Build/VCS/GitRepositoryExtensions.cs +++ b/src/Fallout.Build/VCS/GitRepositoryExtensions.cs @@ -51,4 +51,10 @@ public static bool IsOnHotfixBranch(this GitRepository repository) return (repository.Branch?.StartsWithOrdinalIgnoreCase("hotfix/") ?? false) || (repository.Branch?.StartsWithOrdinalIgnoreCase("hotfixes/") ?? false); } + + public static bool IsOnSupportBranch(this GitRepository repository) + { + return (repository.Branch?.StartsWithOrdinalIgnoreCase("support/") ?? false) || + (repository.Branch?.StartsWithOrdinalIgnoreCase("supports/") ?? false); + } } diff --git a/src/Fallout.Common/Tools/PackageGuard/PackageGuard.Generated.cs b/src/Fallout.Common/Tools/PackageGuard/PackageGuard.Generated.cs new file mode 100644 index 000000000..87b0ead28 --- /dev/null +++ b/src/Fallout.Common/Tools/PackageGuard/PackageGuard.Generated.cs @@ -0,0 +1,344 @@ +// Generated from https://github.com/Fallout-build/Fallout/blob/develop/src/Fallout.Common/Tools/PackageGuard/PackageGuard.json + +using Fallout.Common; +using Fallout.Common.Tooling; +using Fallout.Common.Tools; +using Fallout.Common.Utilities.Collections; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.Json.Serialization; + +namespace Fallout.Common.Tools.PackageGuard; + +///

PackageGuard scans the NuGet, npm, Yarn and pnpm dependencies of a solution or project against an allow- or deny-list, so you can control which open-source licenses and package versions are acceptable. It can also print a colored risk summary, generate HTML/SARIF risk reports, and produce a Software Bill of Materials.

For more details, visit the official website.

+[ExcludeFromCodeCoverage] +[NuGetTool(Id = PackageId, Executable = PackageExecutable)] +public partial class PackageGuardTasks : ToolTasks, IRequireNuGetPackage +{ + public static string PackageGuardPath { get => new PackageGuardTasks().GetToolPathInternal(); set => new PackageGuardTasks().SetToolPath(value); } + public const string PackageId = "PackageGuard"; + public const string PackageExecutable = "PackageGuard.dll|PackageGuard.exe"; + ///

PackageGuard scans the NuGet, npm, Yarn and pnpm dependencies of a solution or project against an allow- or deny-list, so you can control which open-source licenses and package versions are acceptable. It can also print a colored risk summary, generate HTML/SARIF risk reports, and produce a Software Bill of Materials.

For more details, visit the official website.

+ public static IReadOnlyCollection PackageGuard(ArgumentStringHandler arguments, string workingDirectory = null, IReadOnlyDictionary environmentVariables = null, int? timeout = null, bool? logOutput = null, bool? logInvocation = null, Action logger = null, Func exitHandler = null) => new PackageGuardTasks().Run(arguments, workingDirectory, environmentVariables, timeout, logOutput, logInvocation, logger, exitHandler); + ///

PackageGuard scans the NuGet, npm, Yarn and pnpm dependencies of a solution or project against an allow- or deny-list, so you can control which open-source licenses and package versions are acceptable. It can also print a colored risk summary, generate HTML/SARIF risk reports, and produce a Software Bill of Materials.

For more details, visit the official website.

+ ///

This is a CLI wrapper with fluent API that allows to modify the following arguments:

  • <projectPath> via
  • --cache-file-path via
  • --config-path via
  • --force-restore via
  • --github-api-key via
  • --ignore-violations via
  • --npm via
  • --npm-exe-path via
  • --nuget via
  • --refresh-risk-cache via
  • --report-risk via
  • --restore-interactive via
  • --risk-cache-max-age-hours via
  • --sbom via
  • --sbom-output via
  • --skip-restore via
  • --use-caching via
  • --verbose via
+ public static IReadOnlyCollection PackageGuard(PackageGuardSettings options = null) => new PackageGuardTasks().Run(options); + /// + public static IReadOnlyCollection PackageGuard(Configure configurator) => new PackageGuardTasks().Run(configurator.Invoke(new PackageGuardSettings())); + /// + public static IEnumerable<(PackageGuardSettings Settings, IReadOnlyCollection Output)> PackageGuard(CombinatorialConfigure configurator, int degreeOfParallelism = 1, bool completeOnFailure = false) => configurator.Invoke(PackageGuard, degreeOfParallelism, completeOnFailure); +} +#region PackageGuardSettings +/// +[ExcludeFromCodeCoverage] +[Command(Type = typeof(PackageGuardTasks), Command = nameof(PackageGuardTasks.PackageGuard))] +public partial class PackageGuardSettings : ToolOptions, IToolOptionsWithFramework +{ + /// The path to a directory containing a .sln/.slnx file and/or a package.json, a specific .sln/.slnx file, a specific .csproj file, or a specific package.json. Defaults to the current working directory. + [Argument(Format = "{value}", Position = 1)] public string ProjectPath => Get(() => ProjectPath); + /// The path to the configuration file. Defaults to hierarchical discovery of packageguard.config.json or .packageguard/config.json files starting from the solution directory. + [Argument(Format = "--config-path {value}")] public string ConfigPath => Get(() => ConfigPath); + /// Enables interactive mode for dotnet restore. Enabled by default. + [Argument(Format = "--restore-interactive")] public bool? Interactive => Get(() => Interactive); + /// Don't fail the analysis if any violations are found. + [Argument(Format = "--ignore-violations")] public bool? IgnoreViolations => Get(() => IgnoreViolations); + /// Force restoring the NuGet dependencies, even if the lockfile is up-to-date. + [Argument(Format = "--force-restore")] public bool? ForceRestore => Get(() => ForceRestore); + /// Prevent the restore operation from running, even if the lock file is missing or out-of-date. + [Argument(Format = "--skip-restore")] public bool? SkipRestore => Get(() => SkipRestore); + /// GitHub API key to use for fetching package licenses. If not specified, you may run into GitHub's rate limiting issues. + [Argument(Format = "--github-api-key {value}", Secret = true)] public string GitHubApiKey => Get(() => GitHubApiKey); + /// Maintains a cache of the package information to speed up future analysis. + [Argument(Format = "--use-caching")] public bool? UseCaching => Get(() => UseCaching); + /// Overrides the file path where analysis data is cached. Defaults to a .packageguard/cache.bin file relative to the working directory. + [Argument(Format = "--cache-file-path {value}")] public string CacheFilePath => Get(() => CacheFilePath); + /// Force --report-risk to rebuild risk-related package data instead of reusing cached risk entries. + [Argument(Format = "--refresh-risk-cache")] public bool? RefreshRiskCache => Get(() => RefreshRiskCache); + /// Maximum age in hours for cached risk-related package data before --report-risk refreshes it. Defaults to 24. + [Argument(Format = "--risk-cache-max-age-hours {value}")] public int? RiskCacheMaxAgeHours => Get(() => RiskCacheMaxAgeHours); + /// Explicitly enable scanning for .csproj, .sln or .slnx files. Enabled by default. + [Argument(Format = "--nuget")] public bool? ScanNuGet => Get(() => ScanNuGet); + /// Explicitly specify the package manager to use (Npm, Yarn, Pnpm), or None to disable npm scanning entirely. If not specified, it is detected automatically. + [Argument(Format = "--npm {value}")] public NpmPackageManager NpmPackageManager => Get(() => NpmPackageManager); + /// The path to the npm, yarn or pnpm executable. If not specified, the system PATH is used. + [Argument(Format = "--npm-exe-path {value}")] public string NpmExePath => Get(() => NpmExePath); + /// Show a colored risk summary in the console and generate detailed HTML/SARIF risk reports. + [Argument(Format = "--report-risk")] public bool? ReportRisk => Get(() => ReportRisk); + /// Enable verbose (debug-level) logging output. Combine with --report-risk to see individual HTTP calls to GitHub, OSV, and npm registries. + [Argument(Format = "--verbose")] public bool? Verbose => Get(() => Verbose); + /// Generate a Software Bill of Materials for the resolved dependency graph, in the given format. Requires --sbom-output. + [Argument(Format = "--sbom {value}")] public SbomFormat Sbom => Get(() => Sbom); + /// The output file path for the generated SBOM. Required when --sbom is specified. + [Argument(Format = "--sbom-output {value}")] public string SbomOutput => Get(() => SbomOutput); +} +#endregion +#region PackageGuardSettingsExtensions +/// +[ExcludeFromCodeCoverage] +public static partial class PackageGuardSettingsExtensions +{ + #region ProjectPath + /// + [Builder(Type = typeof(PackageGuardSettings), Property = nameof(PackageGuardSettings.ProjectPath))] + public static T SetProjectPath(this T o, string v) where T : PackageGuardSettings => o.Modify(b => b.Set(() => o.ProjectPath, v)); + /// + [Builder(Type = typeof(PackageGuardSettings), Property = nameof(PackageGuardSettings.ProjectPath))] + public static T ResetProjectPath(this T o) where T : PackageGuardSettings => o.Modify(b => b.Remove(() => o.ProjectPath)); + #endregion + #region ConfigPath + /// + [Builder(Type = typeof(PackageGuardSettings), Property = nameof(PackageGuardSettings.ConfigPath))] + public static T SetConfigPath(this T o, string v) where T : PackageGuardSettings => o.Modify(b => b.Set(() => o.ConfigPath, v)); + /// + [Builder(Type = typeof(PackageGuardSettings), Property = nameof(PackageGuardSettings.ConfigPath))] + public static T ResetConfigPath(this T o) where T : PackageGuardSettings => o.Modify(b => b.Remove(() => o.ConfigPath)); + #endregion + #region Interactive + /// + [Builder(Type = typeof(PackageGuardSettings), Property = nameof(PackageGuardSettings.Interactive))] + public static T SetInteractive(this T o, bool? v) where T : PackageGuardSettings => o.Modify(b => b.Set(() => o.Interactive, v)); + /// + [Builder(Type = typeof(PackageGuardSettings), Property = nameof(PackageGuardSettings.Interactive))] + public static T ResetInteractive(this T o) where T : PackageGuardSettings => o.Modify(b => b.Remove(() => o.Interactive)); + /// + [Builder(Type = typeof(PackageGuardSettings), Property = nameof(PackageGuardSettings.Interactive))] + public static T EnableInteractive(this T o) where T : PackageGuardSettings => o.Modify(b => b.Set(() => o.Interactive, true)); + /// + [Builder(Type = typeof(PackageGuardSettings), Property = nameof(PackageGuardSettings.Interactive))] + public static T DisableInteractive(this T o) where T : PackageGuardSettings => o.Modify(b => b.Set(() => o.Interactive, false)); + /// + [Builder(Type = typeof(PackageGuardSettings), Property = nameof(PackageGuardSettings.Interactive))] + public static T ToggleInteractive(this T o) where T : PackageGuardSettings => o.Modify(b => b.Set(() => o.Interactive, !o.Interactive)); + #endregion + #region IgnoreViolations + /// + [Builder(Type = typeof(PackageGuardSettings), Property = nameof(PackageGuardSettings.IgnoreViolations))] + public static T SetIgnoreViolations(this T o, bool? v) where T : PackageGuardSettings => o.Modify(b => b.Set(() => o.IgnoreViolations, v)); + /// + [Builder(Type = typeof(PackageGuardSettings), Property = nameof(PackageGuardSettings.IgnoreViolations))] + public static T ResetIgnoreViolations(this T o) where T : PackageGuardSettings => o.Modify(b => b.Remove(() => o.IgnoreViolations)); + /// + [Builder(Type = typeof(PackageGuardSettings), Property = nameof(PackageGuardSettings.IgnoreViolations))] + public static T EnableIgnoreViolations(this T o) where T : PackageGuardSettings => o.Modify(b => b.Set(() => o.IgnoreViolations, true)); + /// + [Builder(Type = typeof(PackageGuardSettings), Property = nameof(PackageGuardSettings.IgnoreViolations))] + public static T DisableIgnoreViolations(this T o) where T : PackageGuardSettings => o.Modify(b => b.Set(() => o.IgnoreViolations, false)); + /// + [Builder(Type = typeof(PackageGuardSettings), Property = nameof(PackageGuardSettings.IgnoreViolations))] + public static T ToggleIgnoreViolations(this T o) where T : PackageGuardSettings => o.Modify(b => b.Set(() => o.IgnoreViolations, !o.IgnoreViolations)); + #endregion + #region ForceRestore + /// + [Builder(Type = typeof(PackageGuardSettings), Property = nameof(PackageGuardSettings.ForceRestore))] + public static T SetForceRestore(this T o, bool? v) where T : PackageGuardSettings => o.Modify(b => b.Set(() => o.ForceRestore, v)); + /// + [Builder(Type = typeof(PackageGuardSettings), Property = nameof(PackageGuardSettings.ForceRestore))] + public static T ResetForceRestore(this T o) where T : PackageGuardSettings => o.Modify(b => b.Remove(() => o.ForceRestore)); + /// + [Builder(Type = typeof(PackageGuardSettings), Property = nameof(PackageGuardSettings.ForceRestore))] + public static T EnableForceRestore(this T o) where T : PackageGuardSettings => o.Modify(b => b.Set(() => o.ForceRestore, true)); + /// + [Builder(Type = typeof(PackageGuardSettings), Property = nameof(PackageGuardSettings.ForceRestore))] + public static T DisableForceRestore(this T o) where T : PackageGuardSettings => o.Modify(b => b.Set(() => o.ForceRestore, false)); + /// + [Builder(Type = typeof(PackageGuardSettings), Property = nameof(PackageGuardSettings.ForceRestore))] + public static T ToggleForceRestore(this T o) where T : PackageGuardSettings => o.Modify(b => b.Set(() => o.ForceRestore, !o.ForceRestore)); + #endregion + #region SkipRestore + /// + [Builder(Type = typeof(PackageGuardSettings), Property = nameof(PackageGuardSettings.SkipRestore))] + public static T SetSkipRestore(this T o, bool? v) where T : PackageGuardSettings => o.Modify(b => b.Set(() => o.SkipRestore, v)); + /// + [Builder(Type = typeof(PackageGuardSettings), Property = nameof(PackageGuardSettings.SkipRestore))] + public static T ResetSkipRestore(this T o) where T : PackageGuardSettings => o.Modify(b => b.Remove(() => o.SkipRestore)); + /// + [Builder(Type = typeof(PackageGuardSettings), Property = nameof(PackageGuardSettings.SkipRestore))] + public static T EnableSkipRestore(this T o) where T : PackageGuardSettings => o.Modify(b => b.Set(() => o.SkipRestore, true)); + /// + [Builder(Type = typeof(PackageGuardSettings), Property = nameof(PackageGuardSettings.SkipRestore))] + public static T DisableSkipRestore(this T o) where T : PackageGuardSettings => o.Modify(b => b.Set(() => o.SkipRestore, false)); + /// + [Builder(Type = typeof(PackageGuardSettings), Property = nameof(PackageGuardSettings.SkipRestore))] + public static T ToggleSkipRestore(this T o) where T : PackageGuardSettings => o.Modify(b => b.Set(() => o.SkipRestore, !o.SkipRestore)); + #endregion + #region GitHubApiKey + /// + [Builder(Type = typeof(PackageGuardSettings), Property = nameof(PackageGuardSettings.GitHubApiKey))] + public static T SetGitHubApiKey(this T o, [Secret] string v) where T : PackageGuardSettings => o.Modify(b => b.Set(() => o.GitHubApiKey, v)); + /// + [Builder(Type = typeof(PackageGuardSettings), Property = nameof(PackageGuardSettings.GitHubApiKey))] + public static T ResetGitHubApiKey(this T o) where T : PackageGuardSettings => o.Modify(b => b.Remove(() => o.GitHubApiKey)); + #endregion + #region UseCaching + /// + [Builder(Type = typeof(PackageGuardSettings), Property = nameof(PackageGuardSettings.UseCaching))] + public static T SetUseCaching(this T o, bool? v) where T : PackageGuardSettings => o.Modify(b => b.Set(() => o.UseCaching, v)); + /// + [Builder(Type = typeof(PackageGuardSettings), Property = nameof(PackageGuardSettings.UseCaching))] + public static T ResetUseCaching(this T o) where T : PackageGuardSettings => o.Modify(b => b.Remove(() => o.UseCaching)); + /// + [Builder(Type = typeof(PackageGuardSettings), Property = nameof(PackageGuardSettings.UseCaching))] + public static T EnableUseCaching(this T o) where T : PackageGuardSettings => o.Modify(b => b.Set(() => o.UseCaching, true)); + /// + [Builder(Type = typeof(PackageGuardSettings), Property = nameof(PackageGuardSettings.UseCaching))] + public static T DisableUseCaching(this T o) where T : PackageGuardSettings => o.Modify(b => b.Set(() => o.UseCaching, false)); + /// + [Builder(Type = typeof(PackageGuardSettings), Property = nameof(PackageGuardSettings.UseCaching))] + public static T ToggleUseCaching(this T o) where T : PackageGuardSettings => o.Modify(b => b.Set(() => o.UseCaching, !o.UseCaching)); + #endregion + #region CacheFilePath + /// + [Builder(Type = typeof(PackageGuardSettings), Property = nameof(PackageGuardSettings.CacheFilePath))] + public static T SetCacheFilePath(this T o, string v) where T : PackageGuardSettings => o.Modify(b => b.Set(() => o.CacheFilePath, v)); + /// + [Builder(Type = typeof(PackageGuardSettings), Property = nameof(PackageGuardSettings.CacheFilePath))] + public static T ResetCacheFilePath(this T o) where T : PackageGuardSettings => o.Modify(b => b.Remove(() => o.CacheFilePath)); + #endregion + #region RefreshRiskCache + /// + [Builder(Type = typeof(PackageGuardSettings), Property = nameof(PackageGuardSettings.RefreshRiskCache))] + public static T SetRefreshRiskCache(this T o, bool? v) where T : PackageGuardSettings => o.Modify(b => b.Set(() => o.RefreshRiskCache, v)); + /// + [Builder(Type = typeof(PackageGuardSettings), Property = nameof(PackageGuardSettings.RefreshRiskCache))] + public static T ResetRefreshRiskCache(this T o) where T : PackageGuardSettings => o.Modify(b => b.Remove(() => o.RefreshRiskCache)); + /// + [Builder(Type = typeof(PackageGuardSettings), Property = nameof(PackageGuardSettings.RefreshRiskCache))] + public static T EnableRefreshRiskCache(this T o) where T : PackageGuardSettings => o.Modify(b => b.Set(() => o.RefreshRiskCache, true)); + /// + [Builder(Type = typeof(PackageGuardSettings), Property = nameof(PackageGuardSettings.RefreshRiskCache))] + public static T DisableRefreshRiskCache(this T o) where T : PackageGuardSettings => o.Modify(b => b.Set(() => o.RefreshRiskCache, false)); + /// + [Builder(Type = typeof(PackageGuardSettings), Property = nameof(PackageGuardSettings.RefreshRiskCache))] + public static T ToggleRefreshRiskCache(this T o) where T : PackageGuardSettings => o.Modify(b => b.Set(() => o.RefreshRiskCache, !o.RefreshRiskCache)); + #endregion + #region RiskCacheMaxAgeHours + /// + [Builder(Type = typeof(PackageGuardSettings), Property = nameof(PackageGuardSettings.RiskCacheMaxAgeHours))] + public static T SetRiskCacheMaxAgeHours(this T o, int? v) where T : PackageGuardSettings => o.Modify(b => b.Set(() => o.RiskCacheMaxAgeHours, v)); + /// + [Builder(Type = typeof(PackageGuardSettings), Property = nameof(PackageGuardSettings.RiskCacheMaxAgeHours))] + public static T ResetRiskCacheMaxAgeHours(this T o) where T : PackageGuardSettings => o.Modify(b => b.Remove(() => o.RiskCacheMaxAgeHours)); + #endregion + #region ScanNuGet + /// + [Builder(Type = typeof(PackageGuardSettings), Property = nameof(PackageGuardSettings.ScanNuGet))] + public static T SetScanNuGet(this T o, bool? v) where T : PackageGuardSettings => o.Modify(b => b.Set(() => o.ScanNuGet, v)); + /// + [Builder(Type = typeof(PackageGuardSettings), Property = nameof(PackageGuardSettings.ScanNuGet))] + public static T ResetScanNuGet(this T o) where T : PackageGuardSettings => o.Modify(b => b.Remove(() => o.ScanNuGet)); + /// + [Builder(Type = typeof(PackageGuardSettings), Property = nameof(PackageGuardSettings.ScanNuGet))] + public static T EnableScanNuGet(this T o) where T : PackageGuardSettings => o.Modify(b => b.Set(() => o.ScanNuGet, true)); + /// + [Builder(Type = typeof(PackageGuardSettings), Property = nameof(PackageGuardSettings.ScanNuGet))] + public static T DisableScanNuGet(this T o) where T : PackageGuardSettings => o.Modify(b => b.Set(() => o.ScanNuGet, false)); + /// + [Builder(Type = typeof(PackageGuardSettings), Property = nameof(PackageGuardSettings.ScanNuGet))] + public static T ToggleScanNuGet(this T o) where T : PackageGuardSettings => o.Modify(b => b.Set(() => o.ScanNuGet, !o.ScanNuGet)); + #endregion + #region NpmPackageManager + /// + [Builder(Type = typeof(PackageGuardSettings), Property = nameof(PackageGuardSettings.NpmPackageManager))] + public static T SetNpmPackageManager(this T o, NpmPackageManager v) where T : PackageGuardSettings => o.Modify(b => b.Set(() => o.NpmPackageManager, v)); + /// + [Builder(Type = typeof(PackageGuardSettings), Property = nameof(PackageGuardSettings.NpmPackageManager))] + public static T ResetNpmPackageManager(this T o) where T : PackageGuardSettings => o.Modify(b => b.Remove(() => o.NpmPackageManager)); + #endregion + #region NpmExePath + /// + [Builder(Type = typeof(PackageGuardSettings), Property = nameof(PackageGuardSettings.NpmExePath))] + public static T SetNpmExePath(this T o, string v) where T : PackageGuardSettings => o.Modify(b => b.Set(() => o.NpmExePath, v)); + /// + [Builder(Type = typeof(PackageGuardSettings), Property = nameof(PackageGuardSettings.NpmExePath))] + public static T ResetNpmExePath(this T o) where T : PackageGuardSettings => o.Modify(b => b.Remove(() => o.NpmExePath)); + #endregion + #region ReportRisk + /// + [Builder(Type = typeof(PackageGuardSettings), Property = nameof(PackageGuardSettings.ReportRisk))] + public static T SetReportRisk(this T o, bool? v) where T : PackageGuardSettings => o.Modify(b => b.Set(() => o.ReportRisk, v)); + /// + [Builder(Type = typeof(PackageGuardSettings), Property = nameof(PackageGuardSettings.ReportRisk))] + public static T ResetReportRisk(this T o) where T : PackageGuardSettings => o.Modify(b => b.Remove(() => o.ReportRisk)); + /// + [Builder(Type = typeof(PackageGuardSettings), Property = nameof(PackageGuardSettings.ReportRisk))] + public static T EnableReportRisk(this T o) where T : PackageGuardSettings => o.Modify(b => b.Set(() => o.ReportRisk, true)); + /// + [Builder(Type = typeof(PackageGuardSettings), Property = nameof(PackageGuardSettings.ReportRisk))] + public static T DisableReportRisk(this T o) where T : PackageGuardSettings => o.Modify(b => b.Set(() => o.ReportRisk, false)); + /// + [Builder(Type = typeof(PackageGuardSettings), Property = nameof(PackageGuardSettings.ReportRisk))] + public static T ToggleReportRisk(this T o) where T : PackageGuardSettings => o.Modify(b => b.Set(() => o.ReportRisk, !o.ReportRisk)); + #endregion + #region Verbose + /// + [Builder(Type = typeof(PackageGuardSettings), Property = nameof(PackageGuardSettings.Verbose))] + public static T SetVerbose(this T o, bool? v) where T : PackageGuardSettings => o.Modify(b => b.Set(() => o.Verbose, v)); + /// + [Builder(Type = typeof(PackageGuardSettings), Property = nameof(PackageGuardSettings.Verbose))] + public static T ResetVerbose(this T o) where T : PackageGuardSettings => o.Modify(b => b.Remove(() => o.Verbose)); + /// + [Builder(Type = typeof(PackageGuardSettings), Property = nameof(PackageGuardSettings.Verbose))] + public static T EnableVerbose(this T o) where T : PackageGuardSettings => o.Modify(b => b.Set(() => o.Verbose, true)); + /// + [Builder(Type = typeof(PackageGuardSettings), Property = nameof(PackageGuardSettings.Verbose))] + public static T DisableVerbose(this T o) where T : PackageGuardSettings => o.Modify(b => b.Set(() => o.Verbose, false)); + /// + [Builder(Type = typeof(PackageGuardSettings), Property = nameof(PackageGuardSettings.Verbose))] + public static T ToggleVerbose(this T o) where T : PackageGuardSettings => o.Modify(b => b.Set(() => o.Verbose, !o.Verbose)); + #endregion + #region Sbom + /// + [Builder(Type = typeof(PackageGuardSettings), Property = nameof(PackageGuardSettings.Sbom))] + public static T SetSbom(this T o, SbomFormat v) where T : PackageGuardSettings => o.Modify(b => b.Set(() => o.Sbom, v)); + /// + [Builder(Type = typeof(PackageGuardSettings), Property = nameof(PackageGuardSettings.Sbom))] + public static T ResetSbom(this T o) where T : PackageGuardSettings => o.Modify(b => b.Remove(() => o.Sbom)); + #endregion + #region SbomOutput + /// + [Builder(Type = typeof(PackageGuardSettings), Property = nameof(PackageGuardSettings.SbomOutput))] + public static T SetSbomOutput(this T o, string v) where T : PackageGuardSettings => o.Modify(b => b.Set(() => o.SbomOutput, v)); + /// + [Builder(Type = typeof(PackageGuardSettings), Property = nameof(PackageGuardSettings.SbomOutput))] + public static T ResetSbomOutput(this T o) where T : PackageGuardSettings => o.Modify(b => b.Remove(() => o.SbomOutput)); + #endregion +} +#endregion +#region NpmPackageManager +/// Used within . +[Serializable] +[ExcludeFromCodeCoverage] +[TypeConverter(typeof(TypeConverter))] +public partial class NpmPackageManager : Enumeration +{ + public static NpmPackageManager None = (NpmPackageManager) "None"; + public static NpmPackageManager Npm = (NpmPackageManager) "Npm"; + public static NpmPackageManager Yarn = (NpmPackageManager) "Yarn"; + public static NpmPackageManager Pnpm = (NpmPackageManager) "Pnpm"; + public static implicit operator NpmPackageManager(string value) + { + return new NpmPackageManager { Value = value }; + } +} +#endregion +#region SbomFormat +/// Used within . +[Serializable] +[ExcludeFromCodeCoverage] +[TypeConverter(typeof(TypeConverter))] +public partial class SbomFormat : Enumeration +{ + public static SbomFormat cyclonedx = (SbomFormat) "cyclonedx"; + public static SbomFormat spdx = (SbomFormat) "spdx"; + public static implicit operator SbomFormat(string value) + { + return new SbomFormat { Value = value }; + } +} +#endregion diff --git a/src/Fallout.Common/Tools/PackageGuard/PackageGuard.json b/src/Fallout.Common/Tools/PackageGuard/PackageGuard.json new file mode 100644 index 000000000..9a461a687 --- /dev/null +++ b/src/Fallout.Common/Tools/PackageGuard/PackageGuard.json @@ -0,0 +1,148 @@ +{ + "$schema": "https://raw.githubusercontent.com/Fallout-build/Fallout/main/src/Fallout.Tooling.Generator/schema.json", + "references": [ + "https://github.com/dennisdoomen/packageguard/blob/main/Src/PackageGuard/AnalyzeCommandSettings.cs" + ], + "name": "PackageGuard", + "officialUrl": "https://github.com/dennisdoomen/packageguard", + "help": "PackageGuard scans the NuGet, npm, Yarn and pnpm dependencies of a solution or project against an allow- or deny-list, so you can control which open-source licenses and package versions are acceptable. It can also print a colored risk summary, generate HTML/SARIF risk reports, and produce a Software Bill of Materials.", + "nugetPackageId": "PackageGuard", + "nugetFramework": true, + "packageExecutable": "PackageGuard.dll|PackageGuard.exe", + "tasks": [ + { + "settingsClass": { + "properties": [ + { + "name": "ProjectPath", + "type": "string", + "format": "{value}", + "position": 1, + "help": "The path to a directory containing a .sln/.slnx file and/or a package.json, a specific .sln/.slnx file, a specific .csproj file, or a specific package.json. Defaults to the current working directory." + }, + { + "name": "ConfigPath", + "type": "string", + "format": "--config-path {value}", + "help": "The path to the configuration file. Defaults to hierarchical discovery of packageguard.config.json or .packageguard/config.json files starting from the solution directory." + }, + { + "name": "Interactive", + "type": "bool", + "format": "--restore-interactive", + "help": "Enables interactive mode for dotnet restore. Enabled by default." + }, + { + "name": "IgnoreViolations", + "type": "bool", + "format": "--ignore-violations", + "help": "Don't fail the analysis if any violations are found." + }, + { + "name": "ForceRestore", + "type": "bool", + "format": "--force-restore", + "help": "Force restoring the NuGet dependencies, even if the lockfile is up-to-date." + }, + { + "name": "SkipRestore", + "type": "bool", + "format": "--skip-restore", + "help": "Prevent the restore operation from running, even if the lock file is missing or out-of-date." + }, + { + "name": "GitHubApiKey", + "type": "string", + "format": "--github-api-key {value}", + "secret": true, + "help": "GitHub API key to use for fetching package licenses. If not specified, you may run into GitHub's rate limiting issues." + }, + { + "name": "UseCaching", + "type": "bool", + "format": "--use-caching", + "help": "Maintains a cache of the package information to speed up future analysis." + }, + { + "name": "CacheFilePath", + "type": "string", + "format": "--cache-file-path {value}", + "help": "Overrides the file path where analysis data is cached. Defaults to a .packageguard/cache.bin file relative to the working directory." + }, + { + "name": "RefreshRiskCache", + "type": "bool", + "format": "--refresh-risk-cache", + "help": "Force --report-risk to rebuild risk-related package data instead of reusing cached risk entries." + }, + { + "name": "RiskCacheMaxAgeHours", + "type": "int", + "format": "--risk-cache-max-age-hours {value}", + "help": "Maximum age in hours for cached risk-related package data before --report-risk refreshes it. Defaults to 24." + }, + { + "name": "ScanNuGet", + "type": "bool", + "format": "--nuget", + "help": "Explicitly enable scanning for .csproj, .sln or .slnx files. Enabled by default." + }, + { + "name": "NpmPackageManager", + "type": "NpmPackageManager", + "format": "--npm {value}", + "help": "Explicitly specify the package manager to use (Npm, Yarn, Pnpm), or None to disable npm scanning entirely. If not specified, it is detected automatically." + }, + { + "name": "NpmExePath", + "type": "string", + "format": "--npm-exe-path {value}", + "help": "The path to the npm, yarn or pnpm executable. If not specified, the system PATH is used." + }, + { + "name": "ReportRisk", + "type": "bool", + "format": "--report-risk", + "help": "Show a colored risk summary in the console and generate detailed HTML/SARIF risk reports." + }, + { + "name": "Verbose", + "type": "bool", + "format": "--verbose", + "help": "Enable verbose (debug-level) logging output. Combine with --report-risk to see individual HTTP calls to GitHub, OSV, and npm registries." + }, + { + "name": "Sbom", + "type": "SbomFormat", + "format": "--sbom {value}", + "help": "Generate a Software Bill of Materials for the resolved dependency graph, in the given format. Requires --sbom-output." + }, + { + "name": "SbomOutput", + "type": "string", + "format": "--sbom-output {value}", + "help": "The output file path for the generated SBOM. Required when --sbom is specified." + } + ] + } + } + ], + "enumerations": [ + { + "name": "NpmPackageManager", + "values": [ + "None", + "Npm", + "Yarn", + "Pnpm" + ] + }, + { + "name": "SbomFormat", + "values": [ + "cyclonedx", + "spdx" + ] + } + ] +} diff --git a/tests/Fallout.Build.Specs/Utilities/ConsoleUtilitySpecs.cs b/tests/Fallout.Build.Specs/Utilities/ConsoleUtilitySpecs.cs index fec23bac3..9975809ad 100644 --- a/tests/Fallout.Build.Specs/Utilities/ConsoleUtilitySpecs.cs +++ b/tests/Fallout.Build.Specs/Utilities/ConsoleUtilitySpecs.cs @@ -1,3 +1,4 @@ +using Fallout.Common.Specs; using Fallout.Common.Utilities; using Xunit; using System; @@ -8,6 +9,7 @@ namespace Fallout.Build.Tests.Utilities; +[Collection(ProcessGlobalStateCollection.Name)] public class ConsoleUtilitySpecs { private class MockConsole : IConsole diff --git a/tests/Fallout.Common.Specs/SettingsSpecs.cs b/tests/Fallout.Common.Specs/SettingsSpecs.cs index a6dd051a0..5bcdc0fcb 100644 --- a/tests/Fallout.Common.Specs/SettingsSpecs.cs +++ b/tests/Fallout.Common.Specs/SettingsSpecs.cs @@ -14,6 +14,7 @@ using Fallout.Common.Tools.Docker; using Fallout.Common.Tools.DotNet; using Fallout.Common.Tools.Kubernetes; +using Fallout.Common.Tools.PackageGuard; using Fallout.Common.Utilities; using VerifyXunit; using Xunit; @@ -192,6 +193,21 @@ public void TestKubernetes() "exec --container=container --cluster=cluster -- command arg1 arg2"); } + [Fact] + public void TestPackageGuard() + { + Assert(new PackageGuardSettings() + .SetProjectPath("MySolution.sln") + .SetConfigPath("packageguard.config.json") + .EnableForceRestore() + .SetRiskCacheMaxAgeHours(48) + .SetNpmPackageManager(NpmPackageManager.Yarn) + .EnableReportRisk() + .SetSbom(SbomFormat.cyclonedx) + .SetSbomOutput("sbom.json"), + "MySolution.sln --config-path packageguard.config.json --force-restore --risk-cache-max-age-hours 48 --npm Yarn --report-risk --sbom cyclonedx --sbom-output sbom.json"); + } + private static void Assert(T options, string expected) where T : ToolOptions, new() {