From 58068ce58638de170b7ca24c56e3c8805664e838 Mon Sep 17 00:00:00 2001 From: tonycknight Date: Thu, 21 Dec 2023 14:20:02 +0000 Subject: [PATCH 01/19] first cut --- .config/dotnet-tools.json | 12 ++++ .gitignore | 2 + pkgchk-cli.sln | 37 ++++++++++ src/pkgchk-cli/Program.fs | 32 +++++++++ src/pkgchk-cli/Sca.fs | 115 +++++++++++++++++++++++++++++++ src/pkgchk-cli/ScaSample.json | 104 ++++++++++++++++++++++++++++ src/pkgchk-cli/pkgchk-cli.fsproj | 24 +++++++ src/pkgchk-cli/pkgchk-cli.sln | 37 ++++++++++ 8 files changed, 363 insertions(+) create mode 100644 .config/dotnet-tools.json create mode 100644 pkgchk-cli.sln create mode 100644 src/pkgchk-cli/Program.fs create mode 100644 src/pkgchk-cli/Sca.fs create mode 100644 src/pkgchk-cli/ScaSample.json create mode 100644 src/pkgchk-cli/pkgchk-cli.fsproj create mode 100644 src/pkgchk-cli/pkgchk-cli.sln diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json new file mode 100644 index 00000000..81ed6786 --- /dev/null +++ b/.config/dotnet-tools.json @@ -0,0 +1,12 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "fantomas": { + "version": "6.2.3", + "commands": [ + "fantomas" + ] + } + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 8a30d258..16978f10 100644 --- a/.gitignore +++ b/.gitignore @@ -396,3 +396,5 @@ FodyWeavers.xsd # JetBrains Rider *.sln.iml + +launchSettings.json \ No newline at end of file diff --git a/pkgchk-cli.sln b/pkgchk-cli.sln new file mode 100644 index 00000000..ffabb70d --- /dev/null +++ b/pkgchk-cli.sln @@ -0,0 +1,37 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.8.34330.188 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "pkgchk-cli", "src\pkgchk-cli\pkgchk-cli.fsproj", "{8FF6F838-0619-400B-B5EA-79597E00E8B7}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{9030FD14-8EA0-4FD1-A8FA-2639C11CB540}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{CC70A61A-416F-48AD-9BCA-902150B48636}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{F2E3E9A8-73C7-4E10-A391-5ACAA235174C}" + ProjectSection(SolutionItems) = preProject + README.md = README.md + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {8FF6F838-0619-400B-B5EA-79597E00E8B7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8FF6F838-0619-400B-B5EA-79597E00E8B7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8FF6F838-0619-400B-B5EA-79597E00E8B7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8FF6F838-0619-400B-B5EA-79597E00E8B7}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {8FF6F838-0619-400B-B5EA-79597E00E8B7} = {9030FD14-8EA0-4FD1-A8FA-2639C11CB540} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {59CE1C89-868D-4DAB-A9EC-BE3E1A1207EF} + EndGlobalSection +EndGlobal diff --git a/src/pkgchk-cli/Program.fs b/src/pkgchk-cli/Program.fs new file mode 100644 index 00000000..4a656cdd --- /dev/null +++ b/src/pkgchk-cli/Program.fs @@ -0,0 +1,32 @@ +namespace pkgchk + +open System + +module Program = + [] + let main args = + + let path = + match args with + | [||] -> "" + | [| p |] -> p + | [| p; _ |] -> p + + let r = Sca.createProcess path true |> Sca.get + + match r with + | Choice1Of2 json -> + match Sca.parse json with + | Choice1Of2 [] -> + Console.Out.WriteLine("No vulnerabilities found!") + 0 + | Choice1Of2 hits -> + Console.Out.WriteLine("Vulnerabilities found!") + hits |> Sca.formatHits |> Console.Out.WriteLine + 1 + | Choice2Of2 error -> + Console.Error.WriteLine error + 99 + | Choice2Of2 err -> + Console.Error.WriteLine err + 99 diff --git a/src/pkgchk-cli/Sca.fs b/src/pkgchk-cli/Sca.fs new file mode 100644 index 00000000..da75e137 --- /dev/null +++ b/src/pkgchk-cli/Sca.fs @@ -0,0 +1,115 @@ +namespace pkgchk + +open System +open System.Diagnostics +open FSharp.Data + +type ScaData = JsonProvider<"ScaSample.json"> + +type ScaHit = + { framework: string + projectPath: string + packageId: string + resolvedVersion: string + severity: string + advisoryUri: string } + +module Sca = + + let createProcess path includeTransitive = + let p = new Process() + + p.StartInfo.UseShellExecute <- false + p.StartInfo.RedirectStandardOutput <- true + p.StartInfo.FileName <- "dotnet" + p.StartInfo.CreateNoWindow <- true + p.StartInfo.WindowStyle <- ProcessWindowStyle.Hidden + p.StartInfo.RedirectStandardError <- true + p.StartInfo.RedirectStandardOutput <- true + + let transitives = + match includeTransitive with + | true -> "--include-transitive" + | _ -> "" + + let args = + if String.IsNullOrWhiteSpace path then + sprintf " list package --vulnerable %s --format json --output-version 1 " transitives + else + sprintf " list %s package --vulnerable %s --format json --output-version 1 " path transitives + + p.StartInfo.Arguments <- args + + p + + + let get (proc: Process) = + try + if proc.Start() then + let out = proc.StandardOutput.ReadToEnd() + let err = proc.StandardError.ReadToEnd() + proc.WaitForExit() + + if (String.IsNullOrWhiteSpace(err)) then + Choice1Of2(out) + else + Choice2Of2(err) + else + Choice2Of2("Cannot start process") + with ex -> + Choice2Of2(ex.Message) + + let parse json = + + try + let r = ScaData.Parse(json) + + let topLevelVuls = + r.Projects + |> Seq.collect (fun p -> + p.Frameworks + |> Seq.collect (fun f -> + f.TopLevelPackages + |> Seq.collect (fun tp -> + tp.Vulnerabilities + |> Seq.map (fun v -> + { ScaHit.projectPath = p.Path + framework = f.Framework + packageId = tp.Id + resolvedVersion = tp.ResolvedVersion + severity = v.Severity + advisoryUri = v.Advisoryurl })))) + + let transitiveVuls = + r.Projects + |> Seq.collect (fun p -> + p.Frameworks + |> Seq.collect (fun f -> + f.TransitivePackages + |> Seq.collect (fun tp -> + tp.Vulnerabilities + |> Seq.map (fun v -> + { ScaHit.projectPath = p.Path + framework = f.Framework + packageId = tp.Id + resolvedVersion = tp.ResolvedVersion + severity = v.Severity + advisoryUri = v.Advisoryurl })))) + + let hits = topLevelVuls |> Seq.append transitiveVuls |> List.ofSeq + Choice1Of2 hits + with + | ex -> Choice2Of2 ("An error occurred parsing results" + Environment.NewLine + ex.Message) + + let formatHits (hits: seq) = + let fmt (hit: ScaHit) = + seq { + sprintf "Severity: %s" hit.severity + sprintf "Package: %s" hit.packageId + sprintf "Resolved version: %s" hit.resolvedVersion + sprintf "Advisory URL: %s" hit.advisoryUri + sprintf "Project: %s" hit.projectPath + } + + let lines = hits |> Seq.collect fmt + String.Join(Environment.NewLine, lines) diff --git a/src/pkgchk-cli/ScaSample.json b/src/pkgchk-cli/ScaSample.json new file mode 100644 index 00000000..439e8598 --- /dev/null +++ b/src/pkgchk-cli/ScaSample.json @@ -0,0 +1,104 @@ +{ + "version": 1, + "parameters": "--vulnerable --include-transitive", + "sources": [ + "https://api.nuget.org/v3/index.json", + "C:/Program Files (x86)/Microsoft SDKs/NuGetPackages/" + ], + "projects": [ + { + "path": "/testapp/src/testapp/testapp.fsproj", + "frameworks": [ + { + "framework": "net7.0", + "topLevelPackages": [ + { + "id": "System.Net.Http", + "requestedVersion": "vsn 4.3.0", + "resolvedVersion": "vsn 4.3.0", + "vulnerabilities": [ + { + "severity": "High", + "advisoryurl": "https://github.com/advisories/GHSA-7jgj-8wvc-jh57" + } + ] + } + ] + } + ] + }, + { + "path": "/testapp/src/testapp.common/testapp.common.fsproj", + "frameworks": [ + { + "framework": "net7.0", + "transitivePackages": [ + { + "id": "System.Net.Http", + "resolvedVersion": "vsn 4.3.0", + "vulnerabilities": [ + { + "severity": "High", + "advisoryurl": "https://github.com/advisories/GHSA-7jgj-8wvc-jh57" + } + ] + }, + { + "id": "System.Text.RegularExpressions", + "resolvedVersion": "aaa 4.3.0", + "vulnerabilities": [ + { + "severity": "High", + "advisoryurl": "https://github.com/advisories/GHSA-cmhx-cq75-c4mj" + } + ] + } + ] + } + ] + }, + { + "path": "/testapp/tests/testapp.tests.unit/testapp.tests.unit.fsproj", + "frameworks": [ + { + "framework": "net7.0", + "topLevelPackages": [ + { + "id": "System.Net.Http", + "requestedVersion": "vsn 4.3.0", + "resolvedVersion": "vsn 4.3.0", + "vulnerabilities": [ + { + "severity": "High", + "advisoryurl": "https://github.com/advisories/GHSA-7jgj-8wvc-jh57" + } + ] + } + ], + "transitivePackages": [ + { + "id": "System.Net.Http", + "resolvedVersion": "vsn 4.3.0", + "vulnerabilities": [ + { + "severity": "High", + "advisoryurl": "https://github.com/advisories/GHSA-7jgj-8wvc-jh57" + } + ] + }, + { + "id": "System.Text.RegularExpressions", + "resolvedVersion": "aaa 4.3.0", + "vulnerabilities": [ + { + "severity": "High", + "advisoryurl": "https://github.com/advisories/GHSA-cmhx-cq75-c4mj" + } + ] + } + ] + } + ] + } + ] +} diff --git a/src/pkgchk-cli/pkgchk-cli.fsproj b/src/pkgchk-cli/pkgchk-cli.fsproj new file mode 100644 index 00000000..e79777c7 --- /dev/null +++ b/src/pkgchk-cli/pkgchk-cli.fsproj @@ -0,0 +1,24 @@ + + + + Exe + net7.0 + pkgchk + true + pkgchk + pkgchk_cli + + + + + + + + + + + + + + + diff --git a/src/pkgchk-cli/pkgchk-cli.sln b/src/pkgchk-cli/pkgchk-cli.sln new file mode 100644 index 00000000..46333c45 --- /dev/null +++ b/src/pkgchk-cli/pkgchk-cli.sln @@ -0,0 +1,37 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.8.34330.188 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "pkgchk-cli", "pkgchk-cli.fsproj", "{8FF6F838-0619-400B-B5EA-79597E00E8B7}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{9030FD14-8EA0-4FD1-A8FA-2639C11CB540}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{CC70A61A-416F-48AD-9BCA-902150B48636}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{F2E3E9A8-73C7-4E10-A391-5ACAA235174C}" + ProjectSection(SolutionItems) = preProject + ..\..\README.md = ..\..\README.md + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {8FF6F838-0619-400B-B5EA-79597E00E8B7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8FF6F838-0619-400B-B5EA-79597E00E8B7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8FF6F838-0619-400B-B5EA-79597E00E8B7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8FF6F838-0619-400B-B5EA-79597E00E8B7}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {8FF6F838-0619-400B-B5EA-79597E00E8B7} = {9030FD14-8EA0-4FD1-A8FA-2639C11CB540} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {59CE1C89-868D-4DAB-A9EC-BE3E1A1207EF} + EndGlobalSection +EndGlobal From ce60702971be58ec71ce4e8bce3cd17704cb4585 Mon Sep 17 00:00:00 2001 From: tonycknight Date: Thu, 21 Dec 2023 14:49:38 +0000 Subject: [PATCH 02/19] Add colouration --- src/pkgchk-cli/pkgchk-cli.fsproj | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pkgchk-cli/pkgchk-cli.fsproj b/src/pkgchk-cli/pkgchk-cli.fsproj index e79777c7..c662b396 100644 --- a/src/pkgchk-cli/pkgchk-cli.fsproj +++ b/src/pkgchk-cli/pkgchk-cli.fsproj @@ -19,6 +19,7 @@ + From 352c45cecda2b184e5645bba6ac6afed319bd7f1 Mon Sep 17 00:00:00 2001 From: tonycknight Date: Thu, 21 Dec 2023 14:50:21 +0000 Subject: [PATCH 03/19] add colouration --- src/pkgchk-cli/Program.fs | 10 ++++++++-- src/pkgchk-cli/Sca.fs | 34 ++++++++++++++++++++++---------- src/pkgchk-cli/pkgchk-cli.fsproj | 4 +--- 3 files changed, 33 insertions(+), 15 deletions(-) diff --git a/src/pkgchk-cli/Program.fs b/src/pkgchk-cli/Program.fs index 4a656cdd..9186c789 100644 --- a/src/pkgchk-cli/Program.fs +++ b/src/pkgchk-cli/Program.fs @@ -1,6 +1,7 @@ namespace pkgchk open System +open Spectre.Console module Program = [] @@ -18,10 +19,15 @@ module Program = | Choice1Of2 json -> match Sca.parse json with | Choice1Of2 [] -> - Console.Out.WriteLine("No vulnerabilities found!") + "[bold green]No vulnerabilities found![/]" + |> AnsiConsole.Markup + |> Console.Out.WriteLine 0 | Choice1Of2 hits -> - Console.Out.WriteLine("Vulnerabilities found!") + "[bold red]Vulnerabilities found![/]" + |> AnsiConsole.Markup + |> Console.Out.WriteLine + hits |> Sca.formatHits |> Console.Out.WriteLine 1 | Choice2Of2 error -> diff --git a/src/pkgchk-cli/Sca.fs b/src/pkgchk-cli/Sca.fs index da75e137..2e6650b2 100644 --- a/src/pkgchk-cli/Sca.fs +++ b/src/pkgchk-cli/Sca.fs @@ -3,6 +3,7 @@ open System open System.Diagnostics open FSharp.Data +open Spectre.Console type ScaData = JsonProvider<"ScaSample.json"> @@ -60,7 +61,7 @@ module Sca = Choice2Of2(ex.Message) let parse json = - + try let r = ScaData.Parse(json) @@ -97,19 +98,32 @@ module Sca = advisoryUri = v.Advisoryurl })))) let hits = topLevelVuls |> Seq.append transitiveVuls |> List.ofSeq - Choice1Of2 hits - with - | ex -> Choice2Of2 ("An error occurred parsing results" + Environment.NewLine + ex.Message) + Choice1Of2 hits + with ex -> + Choice2Of2("An error occurred parsing results" + Environment.NewLine + ex.Message) let formatHits (hits: seq) = + let formatSeverity value = + let code = + match value with + | "High" -> "red" + | "Critical" -> "italic red" + | "Moderate" -> "#d75f00" + | _ -> "yellow" + + sprintf "[%s]%s[/]" code value + + let formatProject value = + sprintf "[bold yellow]%s[/]" value + let fmt (hit: ScaHit) = seq { - sprintf "Severity: %s" hit.severity - sprintf "Package: %s" hit.packageId - sprintf "Resolved version: %s" hit.resolvedVersion - sprintf "Advisory URL: %s" hit.advisoryUri - sprintf "Project: %s" hit.projectPath + "" + sprintf "Project: %s" hit.projectPath |> formatProject + sprintf "Severity: %s" (formatSeverity hit.severity) + sprintf "Package: [cyan]%s[/] version [cyan]%s[/]" hit.packageId hit.resolvedVersion + sprintf "Advisory URL: %s" hit.advisoryUri } let lines = hits |> Seq.collect fmt - String.Join(Environment.NewLine, lines) + String.Join(Environment.NewLine, lines) |> AnsiConsole.MarkupLine diff --git a/src/pkgchk-cli/pkgchk-cli.fsproj b/src/pkgchk-cli/pkgchk-cli.fsproj index c662b396..ceac0559 100644 --- a/src/pkgchk-cli/pkgchk-cli.fsproj +++ b/src/pkgchk-cli/pkgchk-cli.fsproj @@ -15,10 +15,8 @@ - - - + From 3378ed8bd360fbeee5273fc5fc18b64d6de21fae Mon Sep 17 00:00:00 2001 From: tonycknight Date: Thu, 21 Dec 2023 15:17:01 +0000 Subject: [PATCH 04/19] refactor to spectre cli --- src/pkgchk-cli/Commands.fs | 51 ++++++++++++++++++++++++++++++++ src/pkgchk-cli/Program.fs | 36 ++++------------------ src/pkgchk-cli/Sca.fs | 9 +++--- src/pkgchk-cli/pkgchk-cli.fsproj | 1 + 4 files changed, 61 insertions(+), 36 deletions(-) create mode 100644 src/pkgchk-cli/Commands.fs diff --git a/src/pkgchk-cli/Commands.fs b/src/pkgchk-cli/Commands.fs new file mode 100644 index 00000000..62312f2a --- /dev/null +++ b/src/pkgchk-cli/Commands.fs @@ -0,0 +1,51 @@ +namespace pkgchk + +open System +open System.ComponentModel +open Spectre.Console +open Spectre.Console.Cli + +type PackageCheckCommandSettings() = + inherit CommandSettings() + + [")>] + [] + member val ProjectPath = "" with get, set + + [] + [] + member val IncludeTransitives = false with get, set + +type PackageCheckCommand() = + inherit Command() + + let returnNoVulnerabilities () = + "[bold green]No vulnerabilities found![/]" + |> AnsiConsole.Markup + |> Console.Out.WriteLine + + 0 + + let returnVulnerabilities hits = + "[bold red]Vulnerabilities found![/]" + |> AnsiConsole.Markup + |> Console.Out.WriteLine + + hits |> Sca.formatHits |> Console.Out.WriteLine + 1 + + let returnError (error: string) = + Console.Error.WriteLine error + 2 + + override _.Execute(context, settings) = + let r = + Sca.createProcess settings.ProjectPath settings.IncludeTransitives |> Sca.get + + match r with + | Choice1Of2 json -> + match Sca.parse json with + | Choice1Of2 [] -> returnNoVulnerabilities () + | Choice1Of2 hits -> returnVulnerabilities hits + | Choice2Of2 error -> returnError error + | Choice2Of2 error -> returnError error diff --git a/src/pkgchk-cli/Program.fs b/src/pkgchk-cli/Program.fs index 9186c789..2be0ccdc 100644 --- a/src/pkgchk-cli/Program.fs +++ b/src/pkgchk-cli/Program.fs @@ -1,38 +1,12 @@ namespace pkgchk -open System -open Spectre.Console +open Spectre.Console.Cli module Program = [] - let main args = + let main argv = + let app = CommandApp() - let path = - match args with - | [||] -> "" - | [| p |] -> p - | [| p; _ |] -> p + app.Configure(fun c -> c.PropagateExceptions().ValidateExamples() |> ignore) - let r = Sca.createProcess path true |> Sca.get - - match r with - | Choice1Of2 json -> - match Sca.parse json with - | Choice1Of2 [] -> - "[bold green]No vulnerabilities found![/]" - |> AnsiConsole.Markup - |> Console.Out.WriteLine - 0 - | Choice1Of2 hits -> - "[bold red]Vulnerabilities found![/]" - |> AnsiConsole.Markup - |> Console.Out.WriteLine - - hits |> Sca.formatHits |> Console.Out.WriteLine - 1 - | Choice2Of2 error -> - Console.Error.WriteLine error - 99 - | Choice2Of2 err -> - Console.Error.WriteLine err - 99 + app.Run(argv) diff --git a/src/pkgchk-cli/Sca.fs b/src/pkgchk-cli/Sca.fs index 2e6650b2..9a008bc9 100644 --- a/src/pkgchk-cli/Sca.fs +++ b/src/pkgchk-cli/Sca.fs @@ -113,16 +113,15 @@ module Sca = sprintf "[%s]%s[/]" code value - let formatProject value = - sprintf "[bold yellow]%s[/]" value + let formatProject value = sprintf "[bold yellow]%s[/]" value let fmt (hit: ScaHit) = seq { "" - sprintf "Project: %s" hit.projectPath |> formatProject - sprintf "Severity: %s" (formatSeverity hit.severity) + sprintf "Project: %s" hit.projectPath |> formatProject + sprintf "Severity: %s" (formatSeverity hit.severity) sprintf "Package: [cyan]%s[/] version [cyan]%s[/]" hit.packageId hit.resolvedVersion - sprintf "Advisory URL: %s" hit.advisoryUri + sprintf "Advisory URL: %s" hit.advisoryUri } let lines = hits |> Seq.collect fmt diff --git a/src/pkgchk-cli/pkgchk-cli.fsproj b/src/pkgchk-cli/pkgchk-cli.fsproj index ceac0559..73634ae4 100644 --- a/src/pkgchk-cli/pkgchk-cli.fsproj +++ b/src/pkgchk-cli/pkgchk-cli.fsproj @@ -12,6 +12,7 @@ + From 947c8639b677bdc179710ab4419b01f2394e2acb Mon Sep 17 00:00:00 2001 From: tonycknight Date: Thu, 21 Dec 2023 15:44:31 +0000 Subject: [PATCH 05/19] first GH build workflow --- .github/workflows/build.yml | 138 ++++++++++++++++++++++++++++++++++++ .gitignore | 3 +- pkgchk-cli.sln | 3 + 3 files changed, 143 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/build.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..ff34c22c --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,138 @@ +name: Build & Release + + +on: + push: + pull_request: + branches: [ main ] + workflow_dispatch: + +env: + build-version-number: 0.1.${{ github.run_number }} + dotnet_version: 7.x + +jobs: + sca: + name: Check SCA + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Setup .NET Core SDK + uses: actions/setup-dotnet@v3 + with: + dotnet-version: "${{ env.dotnet_version }}" + + - name: dotnet SCA + run: | + dotnet tool restore + dotnet restore + dotnet list package --vulnerable --include-transitive | tee results.log + + FOUND_VULN=`grep -c 'has the following vulnerable packages' results.log` || true + FOUND_CRIT=`grep -c 'Critical' results.log` || true + FOUND_HIGH=`grep -c 'High' results.log` || true + + if [[ "$FOUND_VULN" != "0" ]] + then + if [ "$FOUND_CRIT" == "0" -a "$FOUND_HIGH" == "0"] + then + echo "### Vulnerable packages found ###" + exit 0 + fi + echo "### Critical/High vulnerable packages found ###" + exit 1 + fi + echo "## No problems found ##" + exit 0 + + style-rules: + name: Check style rules + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Setup NET SDK + uses: actions/setup-dotnet@v3 + with: + dotnet-version: "${{ env.dotnet_version }}" + + - name: Tool restore + run: dotnet tool restore + + - name: App restore + run: dotnet restore + + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Setup .NET Core SDK + uses: actions/setup-dotnet@v3 + with: + dotnet-version: "${{ env.dotnet_version }}" + + - name: Tool restore + run: dotnet tool restore + + - name: App restore + run: dotnet restore + + - name: Build + run: dotnet build -c Release + + + + + + nuget-release: + name: nuget release + runs-on: ubuntu-latest + needs: [ sca, build, style-rules ] + + steps: + - uses: actions/checkout@v3 + + - name: Setup .NET Core SDK + uses: actions/setup-dotnet@v3 + with: + dotnet-version: "${{ env.dotnet_version }}" + + - name: Tool restore + run: dotnet tool restore + + - name: App restore + run: dotnet restore + + - name: Build package for Preview + if: ${{ github.ref != 'refs/heads/main'}} + run: dotnet pack -c Release -o ./package/ -p:PackageVersion=${{ env.build-version-number }}-preview -p:Version=${{ env.build-version-number }}-preview + + - name: Build package for Release + if: ${{ github.ref == 'refs/heads/main'}} + run: dotnet pack -c Release -o ./package/ -p:PackageVersion=${{ env.build-version-number }} -p:Version=${{ env.build-version-number }} + + - name: Push nuget package + if: github.event_name == 'push' + run: dotnet nuget push "package/*.nupkg" --api-key ${{ secrets.NUGET_PAT }} --source "nuget.org" + + gh-release: + name: gh release + runs-on: ubuntu-latest + needs: [ nuget-release ] + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + + steps: + - uses: actions/checkout@v3 + + - name: Create Release + uses: ncipollo/release-action@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + tag: v${{ env.build-version-number }} + prerelease: true + generateReleaseNotes: true \ No newline at end of file diff --git a/.gitignore b/.gitignore index 16978f10..d6f74b40 100644 --- a/.gitignore +++ b/.gitignore @@ -397,4 +397,5 @@ FodyWeavers.xsd # JetBrains Rider *.sln.iml -launchSettings.json \ No newline at end of file +launchSettings.json +package/* \ No newline at end of file diff --git a/pkgchk-cli.sln b/pkgchk-cli.sln index ffabb70d..916db00f 100644 --- a/pkgchk-cli.sln +++ b/pkgchk-cli.sln @@ -8,6 +8,9 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{9030FD14-8EA0-4FD1-A8FA-2639C11CB540}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{CC70A61A-416F-48AD-9BCA-902150B48636}" + ProjectSection(SolutionItems) = preProject + .github\workflows\build.yml = .github\workflows\build.yml + EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{F2E3E9A8-73C7-4E10-A391-5ACAA235174C}" ProjectSection(SolutionItems) = preProject From daab820a5bed5e04ba14daf33b18634f7384b5ba Mon Sep 17 00:00:00 2001 From: tonycknight Date: Thu, 21 Dec 2023 15:47:41 +0000 Subject: [PATCH 06/19] fix style check --- .github/workflows/build.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ff34c22c..acbf4389 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -65,6 +65,9 @@ jobs: - name: App restore run: dotnet restore + - name: Check style + run: dotnet fantomas ./ --check + build: runs-on: ubuntu-latest From dcf7f35ef7e8150b3129f9c212cf29ccc5705b5e Mon Sep 17 00:00:00 2001 From: tonycknight Date: Thu, 21 Dec 2023 15:52:50 +0000 Subject: [PATCH 07/19] fix project package attrs --- src/pkgchk-cli/pkgchk-cli.fsproj | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/pkgchk-cli/pkgchk-cli.fsproj b/src/pkgchk-cli/pkgchk-cli.fsproj index 73634ae4..ae1a6f48 100644 --- a/src/pkgchk-cli/pkgchk-cli.fsproj +++ b/src/pkgchk-cli/pkgchk-cli.fsproj @@ -3,10 +3,15 @@ Exe net7.0 + Copyright 2023 Tony Knight + Tony Knight pkgchk + Tooling for .net package checks true pkgchk - pkgchk_cli + pkgchk-cli + https://github.com/tonycknight/pkgchk-cli + https://github.com/tonycknight/pkgchk-cli From 2fb46e89b55b310b24ba42ca150b97ef7129a01e Mon Sep 17 00:00:00 2001 From: tonycknight Date: Thu, 21 Dec 2023 15:58:14 +0000 Subject: [PATCH 08/19] add readme to package --- src/pkgchk-cli/pkgchk-cli.fsproj | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pkgchk-cli/pkgchk-cli.fsproj b/src/pkgchk-cli/pkgchk-cli.fsproj index ae1a6f48..f57e26ba 100644 --- a/src/pkgchk-cli/pkgchk-cli.fsproj +++ b/src/pkgchk-cli/pkgchk-cli.fsproj @@ -12,6 +12,7 @@ pkgchk-cli https://github.com/tonycknight/pkgchk-cli https://github.com/tonycknight/pkgchk-cli + README.md From ec8e834867bfbf85090d80fa645d52d21b26086e Mon Sep 17 00:00:00 2001 From: tonycknight Date: Thu, 21 Dec 2023 15:58:53 +0000 Subject: [PATCH 09/19] tweak wf --- .github/workflows/build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index acbf4389..9bc2ea41 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -69,6 +69,7 @@ jobs: run: dotnet fantomas ./ --check build: + name: Build runs-on: ubuntu-latest steps: From c33c9d83345b2d5423d56d5cff34efd0be37a6a5 Mon Sep 17 00:00:00 2001 From: tonycknight Date: Thu, 21 Dec 2023 16:00:13 +0000 Subject: [PATCH 10/19] wf tweaks --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9bc2ea41..c237b81f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -94,7 +94,7 @@ jobs: nuget-release: - name: nuget release + name: Nuget package & release runs-on: ubuntu-latest needs: [ sca, build, style-rules ] From a914082ba2a671b660bce5613a2c3966fc95c573 Mon Sep 17 00:00:00 2001 From: tonycknight Date: Thu, 21 Dec 2023 16:01:28 +0000 Subject: [PATCH 11/19] fix pkg readme --- src/pkgchk-cli/README.md | 2 ++ src/pkgchk-cli/pkgchk-cli.fsproj | 3 +++ 2 files changed, 5 insertions(+) create mode 100644 src/pkgchk-cli/README.md diff --git a/src/pkgchk-cli/README.md b/src/pkgchk-cli/README.md new file mode 100644 index 00000000..d089fda1 --- /dev/null +++ b/src/pkgchk-cli/README.md @@ -0,0 +1,2 @@ +# pkgchk-cli +A dotnet tool for package dependency checks diff --git a/src/pkgchk-cli/pkgchk-cli.fsproj b/src/pkgchk-cli/pkgchk-cli.fsproj index f57e26ba..062e2ae5 100644 --- a/src/pkgchk-cli/pkgchk-cli.fsproj +++ b/src/pkgchk-cli/pkgchk-cli.fsproj @@ -16,12 +16,15 @@ + + + From 0926b7aaf4003bd67654b2a54de272066435942c Mon Sep 17 00:00:00 2001 From: tonycknight Date: Thu, 21 Dec 2023 16:03:05 +0000 Subject: [PATCH 12/19] add CODEOWENERS --- .github/CODEOWNERS | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .github/CODEOWNERS diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..27963c85 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,3 @@ +# These owners will be the default owners for everything in +# the repo. Unless a later match takes precedence +* @tonycknight From 1ad613d8f61d99fb9fd0b20af414e76585f584f8 Mon Sep 17 00:00:00 2001 From: tonycknight Date: Thu, 21 Dec 2023 16:21:05 +0000 Subject: [PATCH 13/19] update README --- README.md | 46 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d089fda1..eb906f41 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,46 @@ # pkgchk-cli -A dotnet tool for package dependency checks + +A dotnet tool for package dependency checks. + +`dotnet list package` is a wonderful tool, and with its `--vulnerable` switch it is essential for code provenance. + +Unfortunately, simple integration into CI pipelines isn't feasible: the tool does not return a non-zero return code when vulnerabilities are found; and CI pipelines rely on return codes. Users are left to parse the tool's console output, and so must maintain different scripts for different environments. + +This tool is a wrapper around `dotnet list package` and interprets the output for vulnerabilities. Anything found will return in a non-zero return code. CI integration is as easy as local use. + +## Installation into your repository + +Create a tool manifest for your reepository: + +```dotnet new tool-manifest``` + +Add the tool to your repository's toolset: + +```dotnet tool install pkgchk-cli``` + +## Use + +To check for top-level dependency vulnerabilities: + +```pkgchk ``` + +To check for top-level and transitive dependency vulnerabilities: + +```pkgchk --transitive``` + + +## Integration within Github actions + +Simply: + +```dotnet tool restore``` + +```pkgchk --transitive``` + +## Licence + +`pkgchk-cli` is licenced under MIT. + +`pkgchk-cli` uses [Spectre.Console](https://spectreconsole.net/) - please check their licence. + +`pkgchk-cli` uses [`dotnet list package`](https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-list-package) published by Microsoft. \ No newline at end of file From adbc175bd7701204f8dbe3154497c2e31446acac Mon Sep 17 00:00:00 2001 From: tonycknight Date: Thu, 21 Dec 2023 16:31:18 +0000 Subject: [PATCH 14/19] set working dir --- src/pkgchk-cli/Sca.fs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pkgchk-cli/Sca.fs b/src/pkgchk-cli/Sca.fs index 9a008bc9..de01c7a4 100644 --- a/src/pkgchk-cli/Sca.fs +++ b/src/pkgchk-cli/Sca.fs @@ -27,6 +27,7 @@ module Sca = p.StartInfo.WindowStyle <- ProcessWindowStyle.Hidden p.StartInfo.RedirectStandardError <- true p.StartInfo.RedirectStandardOutput <- true + p.StartInfo.WorkingDirectory <- Environment.CurrentDirectory let transitives = match includeTransitive with From eeebebf8fcdac3e90887f8cd9b93a5a338e61520 Mon Sep 17 00:00:00 2001 From: tonycknight Date: Thu, 21 Dec 2023 16:33:50 +0000 Subject: [PATCH 15/19] make project name optional --- src/pkgchk-cli/Commands.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pkgchk-cli/Commands.fs b/src/pkgchk-cli/Commands.fs index 62312f2a..4cf3582f 100644 --- a/src/pkgchk-cli/Commands.fs +++ b/src/pkgchk-cli/Commands.fs @@ -8,7 +8,7 @@ open Spectre.Console.Cli type PackageCheckCommandSettings() = inherit CommandSettings() - [")>] + [] [] member val ProjectPath = "" with get, set From 673a1e4d20e6f88433c62d481187ed5ca69f59d8 Mon Sep 17 00:00:00 2001 From: tonycknight Date: Thu, 21 Dec 2023 16:45:41 +0000 Subject: [PATCH 16/19] More readme updates --- README.md | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index eb906f41..2f977f14 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,18 @@ A dotnet tool for package dependency checks. -`dotnet list package` is a wonderful tool, and with its `--vulnerable` switch it is essential for code provenance. +`dotnet list package` is a wonderful tool, and with its `--vulnerable` switch it is essential for code provenance. If you're not famlilar with it or why it's recommended, [see this blog post](https://devblogs.microsoft.com/nuget/how-to-scan-nuget-packages-for-security-vulnerabilities/). Unfortunately, simple integration into CI pipelines isn't feasible: the tool does not return a non-zero return code when vulnerabilities are found; and CI pipelines rely on return codes. Users are left to parse the tool's console output, and so must maintain different scripts for different environments. -This tool is a wrapper around `dotnet list package` and interprets the output for vulnerabilities. Anything found will return in a non-zero return code. CI integration is as easy as local use. +There are long-lived issues on the Dotnet & Nuget boards, which seem to be stuck: +- [Dotnet issue 16852](https://github.com/dotnet/sdk/issues/16852) +- [Dotnet issue 25091](https://github.com/dotnet/sdk/issues/25091) +- [Nuget issue 11781](https://github.com/NuGet/Home/issues/11781) + +So until those issues are resolved, `dotnet list package` needs some workarounds in CI pipelines. + +This tool wraps `dotnet list package` and interprets the output for vulnerabilities. Anything found will return in a non-zero return code. CI integration is as easy as local use. ## Installation into your repository @@ -28,14 +35,31 @@ To check for top-level and transitive dependency vulnerabilities: ```pkgchk --transitive``` +If there's only one project or solution file in your directory, omit the `` argument. + ## Integration within Github actions Simply: -```dotnet tool restore``` +``` +name: run SCA +runs: | + dotnet tool restore + pkgchk --transitive +``` + +## Integration within other CI platforms + +Most CI platforms fail on non-zero return codes from steps. + +Simply ensure your repository has `pkgchk-cli` in its tools manifest, your CI includes `nuget.org` as a package source and run: + +``` +dotnet tool restore +pkgchk --transitive +``` -```pkgchk --transitive``` ## Licence From 50a06834f43521fac32e7b8bb18e35c360e21ba3 Mon Sep 17 00:00:00 2001 From: tonycknight Date: Thu, 21 Dec 2023 17:01:53 +0000 Subject: [PATCH 17/19] fix error handling --- src/pkgchk-cli/Commands.fs | 31 +++++-------------------------- src/pkgchk-cli/Console.fs | 24 ++++++++++++++++++++++++ src/pkgchk-cli/Program.fs | 7 ++++++- src/pkgchk-cli/pkgchk-cli.fsproj | 5 ++--- 4 files changed, 37 insertions(+), 30 deletions(-) create mode 100644 src/pkgchk-cli/Console.fs diff --git a/src/pkgchk-cli/Commands.fs b/src/pkgchk-cli/Commands.fs index 4cf3582f..9370d6ed 100644 --- a/src/pkgchk-cli/Commands.fs +++ b/src/pkgchk-cli/Commands.fs @@ -1,8 +1,6 @@ namespace pkgchk -open System open System.ComponentModel -open Spectre.Console open Spectre.Console.Cli type PackageCheckCommandSettings() = @@ -18,26 +16,7 @@ type PackageCheckCommandSettings() = type PackageCheckCommand() = inherit Command() - - let returnNoVulnerabilities () = - "[bold green]No vulnerabilities found![/]" - |> AnsiConsole.Markup - |> Console.Out.WriteLine - - 0 - - let returnVulnerabilities hits = - "[bold red]Vulnerabilities found![/]" - |> AnsiConsole.Markup - |> Console.Out.WriteLine - - hits |> Sca.formatHits |> Console.Out.WriteLine - 1 - - let returnError (error: string) = - Console.Error.WriteLine error - 2 - + override _.Execute(context, settings) = let r = Sca.createProcess settings.ProjectPath settings.IncludeTransitives |> Sca.get @@ -45,7 +24,7 @@ type PackageCheckCommand() = match r with | Choice1Of2 json -> match Sca.parse json with - | Choice1Of2 [] -> returnNoVulnerabilities () - | Choice1Of2 hits -> returnVulnerabilities hits - | Choice2Of2 error -> returnError error - | Choice2Of2 error -> returnError error + | Choice1Of2 [] -> Console.returnNoVulnerabilities () + | Choice1Of2 hits -> Console.returnVulnerabilities hits + | Choice2Of2 error -> Console.returnError error + | Choice2Of2 error -> Console.returnError error diff --git a/src/pkgchk-cli/Console.fs b/src/pkgchk-cli/Console.fs new file mode 100644 index 00000000..eb8bf899 --- /dev/null +++ b/src/pkgchk-cli/Console.fs @@ -0,0 +1,24 @@ +namespace pkgchk + +open System +open Spectre.Console + +module Console= + let returnNoVulnerabilities () = + "[bold green]No vulnerabilities found![/]" + |> AnsiConsole.Markup + |> Console.Out.WriteLine + 0 + + let returnVulnerabilities hits = + "[bold red]Vulnerabilities found![/]" + |> AnsiConsole.Markup + |> Console.Out.WriteLine + + hits |> Sca.formatHits |> Console.Out.WriteLine + 1 + + let returnError (error: string) = + Console.Error.WriteLine error + 2 + diff --git a/src/pkgchk-cli/Program.fs b/src/pkgchk-cli/Program.fs index 2be0ccdc..ddfe2aca 100644 --- a/src/pkgchk-cli/Program.fs +++ b/src/pkgchk-cli/Program.fs @@ -9,4 +9,9 @@ module Program = app.Configure(fun c -> c.PropagateExceptions().ValidateExamples() |> ignore) - app.Run(argv) + try + app.Run(argv) + with + | ex -> + Console.returnError ex.Message + diff --git a/src/pkgchk-cli/pkgchk-cli.fsproj b/src/pkgchk-cli/pkgchk-cli.fsproj index 062e2ae5..137fd637 100644 --- a/src/pkgchk-cli/pkgchk-cli.fsproj +++ b/src/pkgchk-cli/pkgchk-cli.fsproj @@ -16,15 +16,14 @@ - + + - - From ebcd2e2b3e0d5e7a7033aaf2f9cc9268c689cacf Mon Sep 17 00:00:00 2001 From: tonycknight Date: Thu, 21 Dec 2023 17:03:31 +0000 Subject: [PATCH 18/19] style... --- src/pkgchk-cli/Commands.fs | 2 +- src/pkgchk-cli/Console.fs | 4 ++-- src/pkgchk-cli/Program.fs | 6 ++---- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/pkgchk-cli/Commands.fs b/src/pkgchk-cli/Commands.fs index 9370d6ed..e1fe66ca 100644 --- a/src/pkgchk-cli/Commands.fs +++ b/src/pkgchk-cli/Commands.fs @@ -16,7 +16,7 @@ type PackageCheckCommandSettings() = type PackageCheckCommand() = inherit Command() - + override _.Execute(context, settings) = let r = Sca.createProcess settings.ProjectPath settings.IncludeTransitives |> Sca.get diff --git a/src/pkgchk-cli/Console.fs b/src/pkgchk-cli/Console.fs index eb8bf899..7b5f2ff3 100644 --- a/src/pkgchk-cli/Console.fs +++ b/src/pkgchk-cli/Console.fs @@ -3,11 +3,12 @@ open System open Spectre.Console -module Console= +module Console = let returnNoVulnerabilities () = "[bold green]No vulnerabilities found![/]" |> AnsiConsole.Markup |> Console.Out.WriteLine + 0 let returnVulnerabilities hits = @@ -21,4 +22,3 @@ module Console= let returnError (error: string) = Console.Error.WriteLine error 2 - diff --git a/src/pkgchk-cli/Program.fs b/src/pkgchk-cli/Program.fs index ddfe2aca..698c0125 100644 --- a/src/pkgchk-cli/Program.fs +++ b/src/pkgchk-cli/Program.fs @@ -11,7 +11,5 @@ module Program = try app.Run(argv) - with - | ex -> - Console.returnError ex.Message - + with ex -> + Console.returnError ex.Message From 7b2f84645e6e5aa738e66f4734c4ef247ab6c62f Mon Sep 17 00:00:00 2001 From: tonycknight Date: Thu, 21 Dec 2023 17:25:42 +0000 Subject: [PATCH 19/19] resolve file name to full path - nuget doesn't like unrooted names --- src/pkgchk-cli/Commands.fs | 5 ++++- src/pkgchk-cli/Io.fs | 14 ++++++++++++++ src/pkgchk-cli/Sca.fs | 2 +- src/pkgchk-cli/pkgchk-cli.fsproj | 1 + 4 files changed, 20 insertions(+), 2 deletions(-) create mode 100644 src/pkgchk-cli/Io.fs diff --git a/src/pkgchk-cli/Commands.fs b/src/pkgchk-cli/Commands.fs index e1fe66ca..3ce1c3e3 100644 --- a/src/pkgchk-cli/Commands.fs +++ b/src/pkgchk-cli/Commands.fs @@ -19,7 +19,10 @@ type PackageCheckCommand() = override _.Execute(context, settings) = let r = - Sca.createProcess settings.ProjectPath settings.IncludeTransitives |> Sca.get + settings.ProjectPath + |> Io.toFullPath + |> Sca.createProcess settings.IncludeTransitives + |> Sca.get match r with | Choice1Of2 json -> diff --git a/src/pkgchk-cli/Io.fs b/src/pkgchk-cli/Io.fs new file mode 100644 index 00000000..88842bf7 --- /dev/null +++ b/src/pkgchk-cli/Io.fs @@ -0,0 +1,14 @@ +namespace pkgchk + +open System +open System.IO + +module Io = + + let toFullPath (path: string) = + if not <| Path.IsPathRooted(path) then + let wd = Environment.CurrentDirectory + + Path.Combine(wd, path) + else + path diff --git a/src/pkgchk-cli/Sca.fs b/src/pkgchk-cli/Sca.fs index de01c7a4..38240c01 100644 --- a/src/pkgchk-cli/Sca.fs +++ b/src/pkgchk-cli/Sca.fs @@ -17,7 +17,7 @@ type ScaHit = module Sca = - let createProcess path includeTransitive = + let createProcess includeTransitive path = let p = new Process() p.StartInfo.UseShellExecute <- false diff --git a/src/pkgchk-cli/pkgchk-cli.fsproj b/src/pkgchk-cli/pkgchk-cli.fsproj index 137fd637..b87fbf4c 100644 --- a/src/pkgchk-cli/pkgchk-cli.fsproj +++ b/src/pkgchk-cli/pkgchk-cli.fsproj @@ -19,6 +19,7 @@ +