Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 54 additions & 17 deletions src/pkgchk-cli/Commands.fs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
namespace pkgchk

open System
open System.ComponentModel
open System.Diagnostics.CodeAnalysis
open Spectre.Console.Cli
Expand All @@ -14,10 +15,15 @@ type PackageCheckCommandSettings() =
member val ProjectPath = "" with get, set

[<CommandOption("-t|--transitives")>]
[<Description("Toggle transitive package checks. -t false to exclude transtiives, -t true to include them.")>]
[<Description("Toggle transitive package checks. -t true to include them, -t false to exclude.")>]
[<DefaultValue(true)>]
member val IncludeTransitives = true with get, set

[<CommandOption("-d|--deprecations")>]
[<Description("Check deprecated packagess. -d true to include, -d false to exclude.")>]
[<DefaultValue(false)>]
member val IncludeDeprecations = true with get, set

[<CommandOption("-o|--output")>]
[<Description("Output directory for reports.")>]
[<DefaultValue("")>]
Expand All @@ -29,10 +35,46 @@ type PackageCheckCommand() =

let console = Spectre.Console.AnsiConsole.Console |> Console.send

let genArgs (settings: PackageCheckCommandSettings) =
let projPath = settings.ProjectPath |> Io.toFullPath

[| yield projPath |> ScaArgs.scanVulnerabilities settings.IncludeTransitives
if settings.IncludeDeprecations then
yield projPath |> ScaArgs.scanDeprecations settings.IncludeTransitives |]


let runProc proc =
try
Io.run proc
finally
proc.Dispose()

let runProcParse procs =
procs
|> Array.map runProc
|> Array.map (fun r ->
match r with
| Choice1Of2 json -> Sca.parse json
| Choice2Of2 x -> Choice2Of2 x)

let returnError error =
error |> Console.error |> console
Console.sysError

let getErrors procResults =
procResults
|> Seq.map (function
| Choice2Of2 x -> x
| _ -> "")
|> Seq.filter String.isNotEmpty

let getHits procResults =
procResults
|> Seq.collect (function
| Choice1Of2 xs -> xs
| _ -> [])
|> List.ofSeq

let returnCode =
function
| [] -> Console.validationOk
Expand All @@ -53,25 +95,20 @@ type PackageCheckCommand() =
hits |> genMarkdown |> Io.writeFile reportFile
reportFile


override _.Execute(context, settings) =

use proc =
settings.ProjectPath
|> Io.toFullPath
|> Sca.scanVulnerabilitiesArgs settings.IncludeTransitives
|> Io.createProcess
let results = settings |> genArgs |> Array.map Io.createProcess |> runProcParse

let errors = getErrors results

match Io.run proc with
| Choice1Of2 json ->
match Sca.parse json with
| Choice1Of2 hits ->
genConsole hits
if Seq.isEmpty errors |> not then
errors |> String.joinLines |> returnError
else
let hits = getHits results

if settings.OutputDirectory <> "" then
hits |> genReport settings.OutputDirectory |> Console.reportFileBuilt |> console
hits |> genConsole

returnCode hits
if settings.OutputDirectory <> "" then
hits |> genReport settings.OutputDirectory |> Console.reportFileBuilt |> console

| Choice2Of2 error -> error |> returnError
| Choice2Of2 error -> error |> returnError
returnCode hits
53 changes: 44 additions & 9 deletions src/pkgchk-cli/Console.fs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,15 @@ module Console =
[<Literal>]
let sysError = 2

let joinLines (lines: seq<string>) = String.Join(Environment.NewLine, lines)
let formatHitKind =
function
| ScaHitKind.Vulnerability -> "Vulnerable package"
| ScaHitKind.Deprecated -> "Deprecated package"

let formatReason value =
match value with
| "Legacy" -> sprintf "[yellow]%s[/]" value
| _ -> sprintf "[red]%s[/]" value

let formatSeverity value =
let code =
Expand All @@ -32,22 +40,49 @@ module Console =

let fmt (hit: ScaHit) =
seq {
sprintf
"Package: %s - [cyan]%s[/] version [cyan]%s[/]"
(formatSeverity hit.severity)
hit.packageId
hit.resolvedVersion
match hit.kind with
| ScaHitKind.Vulnerability ->
sprintf
"%s: %s - [cyan]%s[/] version [cyan]%s[/]"
(formatHitKind hit.kind)
(formatSeverity hit.severity)
hit.packageId
hit.resolvedVersion
| ScaHitKind.Deprecated ->
sprintf
"%s: [cyan]%s[/] version [cyan]%s[/]"
(formatHitKind hit.kind)
hit.packageId
hit.resolvedVersion

if String.isNotEmpty hit.advisoryUri then
sprintf " [italic]%s[/]" hit.advisoryUri

if String.isNotEmpty hit.reason && String.isNotEmpty hit.suggestedReplacement then
sprintf
" [italic]%s - use [cyan]%s[/][/]"
(formatReason hit.reason)
hit.suggestedReplacement
else if String.isNotEmpty hit.reason then
sprintf " [italic]%s " hit.reason

sprintf " [italic]%s[/]" hit.advisoryUri
""
}

let fmtGrp (hit: (string * seq<ScaHit>)) =
let projectPath, hits = hit

let hits =
hits
|> Seq.sortBy (fun h ->
(match h.kind with
| ScaHitKind.Vulnerability -> 0
| _ -> 1),
h.packageId)

seq {
sprintf "Project: %s" projectPath |> formatProject
yield! hits |> Seq.sortBy (fun h -> h.packageId) |> Seq.collect fmt
yield! hits |> Seq.collect fmt
}

let grps = hits |> Seq.groupBy (fun h -> h.projectPath) |> Seq.sortBy fst
Expand All @@ -61,7 +96,7 @@ module Console =
"[bold red]Vulnerabilities found![/]"
yield! formatHits hits
}
|> joinLines
|> String.joinLines

let error (error: string) = sprintf "[red]%s[/]" error

Expand Down
47 changes: 39 additions & 8 deletions src/pkgchk-cli/Markdown.fs
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,28 @@ module Markdown =
let formatHitKind (value: ScaHitKind) =
match value with
| ScaHitKind.Vulnerability -> "Vulnerable package"
| ScaHitKind.Deprecated -> "Deprecated package"

let formatSeverity value =
let (emote, colour) =
match value with
| "High" -> (":bangbang:", "red")
| "Critical" -> (":heavy_exclamation_mark:", "red")
| "Moderate" -> (":heavy_exclamation_mark:", "orange")
| "" -> ("", "")
| _ -> (":heavy_exclamation_mark:", "yellow")

sprintf "%s <span style='color:%s'>%s</span>" emote colour value

let formatReason value =
let colour =
function
| "Legacy" -> "yellow"
| "Critical Bugs" -> "red"
| _ -> ""

sprintf "<span style='color:%s'>%s</span>" (colour value) value

let formatProject value = sprintf "## **%s**" value

let footer =
Expand Down Expand Up @@ -47,13 +58,25 @@ module Markdown =

let fmt (hit: ScaHit) =
seq {
sprintf
"| %s | %s | %s %s | [Advisory](%s) | "
(formatHitKind hit.kind)
(formatSeverity hit.severity)
hit.packageId
hit.resolvedVersion
hit.advisoryUri
match hit.kind with
| ScaHitKind.Vulnerability ->
sprintf
"| %s | %s | %s %s | [Advisory](%s) | "
(formatHitKind hit.kind)
(formatSeverity hit.severity)
hit.packageId
hit.resolvedVersion
hit.advisoryUri
| ScaHitKind.Deprecated ->
sprintf
"| %s | %s | %s %s | %s | "
(formatHitKind hit.kind)
(formatReason hit.reason)
hit.packageId
hit.resolvedVersion
(match hit.suggestedReplacement with
| "" -> ""
| x -> sprintf "Use %s" x)
}

let fmtGrp (hit: (string * seq<ScaHit>)) =
Expand All @@ -63,7 +86,15 @@ module Markdown =
projectPath |> formatProject
""
yield! grpHdr
yield! hits |> Seq.sortBy (fun h -> h.packageId) |> Seq.collect fmt

yield!
hits
|> Seq.sortBy (fun h ->
((match h.kind with
| ScaHitKind.Vulnerability -> 0
| _ -> 1),
h.packageId))
|> Seq.collect fmt
}

footer |> Seq.append (grps |> Seq.collect fmtGrp) |> Seq.append hdr
6 changes: 4 additions & 2 deletions src/pkgchk-cli/Program.fs
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ module Program =

[<EntryPoint>]
let main argv =
let app = CommandApp<PackageCheckCommand>()
let app =
CommandApp<PackageCheckCommand>()
.WithDescription("Check project dependency packages for vulnerabilities and deprecations.")

app.Configure(fun c -> c.PropagateExceptions().ValidateExamples() |> ignore)
app.Configure(fun c -> c.PropagateExceptions().ValidateExamples().TrimTrailingPeriods(false) |> ignore)

try
app.Run(argv)
Expand Down
71 changes: 57 additions & 14 deletions src/pkgchk-cli/Sca.fs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ open FSharp.Data

type ScaData = JsonProvider<"ScaSample.json">

type ScaHitKind = | Vulnerability
type ScaHitKind =
| Vulnerability
| Deprecated

type ScaHit =
{ kind: ScaHitKind
Expand All @@ -14,21 +16,29 @@ type ScaHit =
packageId: string
resolvedVersion: string
severity: string
advisoryUri: string }
advisoryUri: string
reason: string
suggestedReplacement: string }

module Sca =
module ScaArgs =

let scanArgs vulnerable includeTransitive path =
sprintf
"%s %s %s %s"
(sprintf "list %s package" path)
(match vulnerable with
| true -> "--vulnerable"
| false -> "--deprecated")
(match includeTransitive with
| true -> "--include-transitive"
| _ -> "")
"--format json --output-version 1"

let scanVulnerabilitiesArgs includeTransitive path =
let transitives =
match includeTransitive with
| true -> "--include-transitive"
| _ -> ""
let scanVulnerabilities = scanArgs true

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
let scanDeprecations = scanArgs false

module Sca =

let parse json =

Expand All @@ -50,8 +60,34 @@ module Sca =
packageId = tp.Id
resolvedVersion = tp.ResolvedVersion
severity = v.Severity
reason = ""
suggestedReplacement = ""
advisoryUri = v.Advisoryurl }))))

let topLevelDeprecations =
r.Projects
|> Seq.collect (fun p ->
p.Frameworks
|> Seq.collect (fun f ->
f.TopLevelPackages
|> Seq.collect (fun tp ->
tp.DeprecationReasons
|> Seq.filter String.isNotEmpty
|> Seq.map (fun d ->
{ ScaHit.projectPath = System.IO.Path.GetFullPath(p.Path)
kind = ScaHitKind.Deprecated
framework = f.Framework
packageId = tp.Id
resolvedVersion = tp.ResolvedVersion
severity = ""
suggestedReplacement =
match tp.AlternativePackage with
| Some ap -> sprintf "%s %s" ap.Id ap.VersionRange
| None -> ""
reason = d

advisoryUri = "" }))))

let transitiveVuls =
r.Projects
|> Seq.collect (fun p ->
Expand All @@ -67,9 +103,16 @@ module Sca =
packageId = tp.Id
resolvedVersion = tp.ResolvedVersion
severity = v.Severity
reason = ""
suggestedReplacement = ""
advisoryUri = v.Advisoryurl }))))

let hits = topLevelVuls |> Seq.append transitiveVuls |> List.ofSeq
let hits =
topLevelDeprecations
|> Seq.append transitiveVuls
|> Seq.append topLevelVuls
|> List.ofSeq

Choice1Of2 hits
with ex ->
Choice2Of2("An error occurred parsing results" + Environment.NewLine + ex.Message)
Choice2Of2("An error occurred parsing results." + Environment.NewLine)
Loading