Current section

Files

Jump to
go_over src go_over.gleam
Raw

src/go_over.gleam

import filepath
import gleam/int
import gleam/io
import gleam/json
import gleam/list
import gleam/option
import gleam/result
import gleam/string
import go_over/advisories/advisories
import go_over/config.{type Config, type Flags}
import go_over/packages
import go_over/sarif
import go_over/sources
import go_over/util/constants
import go_over/util/globals
import go_over/util/print
import go_over/util/spinner
import go_over/util/util
import go_over/warning.{type Warning}
import go_over/workspace
import gxyz/function as gfunction
import shellout
import simplifile
pub type AuditResult {
AuditResult(
project_root: String,
fatal_warnings: List(Warning),
info_warnings: List(Warning),
outdated_failed: Bool,
format: config.Format,
)
}
fn prefix_label(prefix: option.Option(String), label: String) -> String {
case prefix {
option.Some(path) -> "[" <> path <> "] " <> label
option.None -> label
}
}
fn print_warnings_output(
vulns: List(Warning),
conf: Config,
label: String,
prefix: option.Option(String),
) -> Nil {
let label = prefix_label(prefix, label)
io.print_error(label)
case conf.format {
config.Minimal ->
vulns
|> list.map(warning.format_as_string_minimal)
|> string.join("")
|> io.print_error()
config.JSON -> Nil
config.SARIF -> Nil
_ ->
vulns
|> list.map(warning.format_as_string)
|> string.join(constants.long_ass_dashes)
|> io.print_error()
}
}
fn warnings_label(vulns: List(Warning), kind: String) -> String {
"⛔ "
<> int.to_string(list.length(vulns))
<> " "
<> kind
<> "(s) FOUND!"
<> constants.long_ass_dashes
}
fn info_label(vulns: List(Warning)) -> String {
"ℹ️ "
<> int.to_string(list.length(vulns))
<> " Item(s) of Note"
<> constants.long_ass_dashes
}
fn warnings_for_json(result: AuditResult) -> List(Warning) {
list.append(result.info_warnings, result.fatal_warnings)
}
pub fn warnings_for_json_results(results: List(AuditResult)) -> List(Warning) {
list.flat_map(results, warnings_for_json)
}
pub fn print_json_warnings(warnings: List(Warning)) -> Nil {
warnings
|> list.map(warning.format_as_json)
|> json.preprocessed_array()
|> json.to_string()
|> io.print_error()
}
fn write_sarif(
results: List(AuditResult),
output: option.Option(String),
) -> Nil {
let runs =
list.map(results, fn(result) {
#(result.project_root, warnings_for_json(result))
})
let content =
sarif.to_sarif_log(runs)
|> json.to_string()
case output {
option.None -> content |> io.println()
option.Some(path) ->
case path |> simplifile.write(content) {
Ok(Nil) -> Nil
Error(_) -> {
io.println_error("could not write SARIF output to " <> path)
shellout.exit(1)
}
}
}
}
pub fn skipped_workspace_warnings(skipped: List(String)) -> List(Warning) {
list.map(skipped, fn(path) {
warning.info_to_warning(
path,
"Info: project at '"
<> path
<> "' was skipped (exceeds workspace_max_depth)",
)
})
}
pub fn print_warnings(
vulns: List(Warning),
conf: Config,
prefix: option.Option(String),
) -> Nil {
print_warnings_output(vulns, conf, warnings_label(vulns, "WARNING"), prefix)
shellout.exit(1)
}
fn read_project_config(project_root: String) -> Config {
let gleam_toml = filepath.join(project_root, "gleam.toml")
case simplifile.read(gleam_toml) {
Ok(_) -> config.read_config(gleam_toml)
Error(_) -> config.default_config()
}
}
pub fn audit_project(
flags: Flags,
project_root: String,
) -> Result(AuditResult, String) {
let manifest_toml = filepath.join(project_root, "manifest.toml")
let project_config = read_project_config(project_root)
use conf <- result.try(config.merge_flags_and_config(flags, project_config))
globals.set_project_root(project_root)
globals.set_verbose(conf.verbose)
globals.set_use_global_cache(conf.global)
globals.set_force(conf.force)
let machine_output = conf.format == config.SARIF || conf.format == config.JSON
let spinner = case machine_output {
True -> option.None
False -> spinner.new_spinner("Let's do this!")
}
gfunction.ignore_result(
conf.force,
gfunction.freeze1(simplifile.delete, globals.go_over_path()),
)
spinner.set_text_spinner(spinner, "Reading manifest")
let manifest_pkgs = packages.read_manifest(manifest_toml)
let pkgs_audited =
manifest_pkgs
|> config.filter_dev_dependencies(conf, _)
|> config.filter_indirect(conf, _)
let hex_pkgs =
list.filter(pkgs_audited, fn(p) { p.source == packages.PackageSourceHex })
spinner.set_text_spinner(
spinner,
"Checking packages: " <> print.raw("vulnerable", "red"),
)
let all_advisories = advisories.fetch_all()
let vulnerable_warnings =
sources.get_vulnerable_warnings(pkgs_audited, conf, all_advisories)
spinner.set_text_spinner(
spinner,
"Checking packages: " <> print.raw("retired", "yellow"),
)
let retired_warnings = sources.get_retired_warnings(hex_pkgs, conf)
let #(hex_warnings, dependency_licenses) = case conf.allowed_licenses {
[] -> #([], [])
_ -> {
spinner.set_text_spinner(
spinner,
"Checking packages: " <> print.raw("licenses", "brightmagenta"),
)
sources.get_hex_warnings(hex_pkgs, conf)
}
}
spinner.set_text_spinner(spinner, "Filtering warnings")
let audit_warnings =
list.append(retired_warnings, vulnerable_warnings)
|> list.append(hex_warnings)
let unnecessary_warnings =
config.unnecessary_ignore_warnings(
conf,
manifest_pkgs,
audit_warnings,
dependency_licenses,
all_advisories,
)
let fatal_warnings =
audit_warnings
|> config.filter_package_warnings(conf, _)
|> config.filter_severity(conf, _)
spinner.stop_spinner(spinner)
let outdated_failed = case conf.outdated {
False -> False
True -> run_deps_outdated(project_root)
}
let info_warnings =
list.append(
unnecessary_warnings,
warning.git_deps_to_warnings(manifest_pkgs),
)
Ok(AuditResult(
project_root:,
fatal_warnings:,
info_warnings:,
outdated_failed:,
format: conf.format,
))
}
pub fn main() {
case config.parse_flags(shellout.arguments()) {
Error(message) -> io.println(message)
Ok(flags) -> run(flags)
}
}
fn print_no_issues_success(info_count: Int) -> Nil {
case info_count {
0 -> print.success("✅ No warnings found!")
_ ->
print.success(
"✅ No security issues found ("
<> int.to_string(info_count)
<> " item(s) of note)",
)
}
}
fn run(flags: config.Flags) -> Nil {
let workspace_mode = option.is_some(flags.workspace_root)
let #(results, workspace_skipped) = case flags.workspace_root {
option.Some(scan_root) -> {
let scan_config = read_project_config(scan_root)
let max_depth = scan_config.workspace_max_depth
case workspace.discover_or_error(scan_root, max_depth) {
Ok(workspace.DiscoverResult(projects, skipped)) -> #(
list.map(projects, fn(project_root) {
case audit_project(flags, project_root) {
Ok(result) -> result
Error(e) -> {
io.println_error(e)
shellout.exit(1)
util.do_panic()
}
}
}),
skipped,
)
Error(e) -> {
io.println_error(e)
shellout.exit(1)
util.do_panic()
}
}
}
option.None -> {
let project_root = option.unwrap(flags.single_root, ".")
let assert Ok(result) = audit_project(flags, project_root)
#([result], [])
}
}
let skipped_warnings = skipped_workspace_warnings(workspace_skipped)
let results = case skipped_warnings {
[] -> results
_ -> {
case results {
[first, ..rest] -> [
AuditResult(
..first,
info_warnings: list.append(skipped_warnings, first.info_warnings),
),
..rest
]
[] -> results
}
}
}
case workspace_mode, flags.format {
True, option.None ->
case
config.validate_workspace_formats(
list.map(results, fn(r) { #(r.format, r.project_root) }),
)
{
Error(e) -> {
io.println_error(e)
shellout.exit(1)
util.do_panic()
}
Ok(Nil) -> Nil
}
_, _ -> Nil
}
let prefix_for = fn(result: AuditResult) {
case workspace_mode {
True -> option.Some(result.project_root)
False -> option.None
}
}
let display_conf = fn(result: AuditResult) {
config.Config(..config.default_config(), format: result.format)
}
let output_format = case results {
[] -> config.Minimal
[first, ..] -> first.format
}
let sarif_output = output_format == config.SARIF
let json_output = output_format == config.JSON
case flags.sarif_output, sarif_output {
option.Some(_), False -> {
io.println_error("--sarif-output requires --format sarif")
shellout.exit(1)
}
_, _ -> Nil
}
case sarif_output, json_output {
False, False ->
list.each(results, fn(result) {
case result.info_warnings {
[] -> Nil
info ->
print_warnings_output(
info,
display_conf(result),
info_label(info),
prefix_for(result),
)
}
})
False, True -> print_json_warnings(warnings_for_json_results(results))
True, _ -> Nil
}
let any_fatal = list.any(results, fn(r) { !list.is_empty(r.fatal_warnings) })
let any_outdated_failed = list.any(results, fn(r) { r.outdated_failed })
let info_count =
list.flat_map(results, fn(r) { r.info_warnings }) |> list.length
case any_fatal, any_outdated_failed, sarif_output, json_output {
False, False, True, _ -> {
write_sarif(results, flags.sarif_output)
Nil
}
False, False, False, _ -> {
print_no_issues_success(info_count)
Nil
}
False, True, True, _ -> {
write_sarif(results, flags.sarif_output)
shellout.exit(1)
}
False, True, False, _ -> shellout.exit(1)
True, _, True, _ -> {
write_sarif(results, flags.sarif_output)
shellout.exit(1)
}
True, _, False, True -> shellout.exit(1)
True, _, False, False -> {
list.each(results, fn(result) {
case result.fatal_warnings {
[] -> Nil
vulns ->
print_warnings_output(
vulns,
display_conf(result),
warnings_label(vulns, "WARNING"),
prefix_for(result),
)
}
})
shellout.exit(1)
}
}
}
fn run_deps_outdated(project_root: String) -> Bool {
print.high(
"The --outdated flag is deprecated. Use `gleam deps outdated` instead.",
)
case
shellout.command(
run: "gleam",
with: ["deps", "outdated"],
in: project_root,
opt: [
shellout.LetBeStdout,
],
)
{
Ok(_) -> False
Error(_) -> True
}
}