Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

0.3.0 #2

Merged
merged 11 commits into from
Jun 1, 2024
Merged
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
26 changes: 22 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -3,7 +3,7 @@
[![Package Version](https://img.shields.io/hexpm/v/go_over)](https://hex.pm/packages/go_over)
[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/go_over/)

A tool to check for Erlang & Elixir dependency advisories as well as retired hex packages, to make sure your gleam projects really sparkle! ✨
A tool to audit Erlang & Elixir dependency advisories as well as retired hex packages, to make sure your gleam projects really sparkle! ✨

🚨 _**NOTE**_: security advisories are _NOT_ currently monitored for gleam dependencies. The language, while excellent, is far too new and niche

@@ -33,10 +33,28 @@ gleam run -m go_over
- `--skip`: will skip checking the cache and used the stored data no matter what
- `--force`: will force pulling new data even if the cached data is still valid

### Caching
### ⚙️ Config

- Security advisory data is cached for six hours
- hex.pm retired package data is cached for one hour
Optional settings that can be added to your project's gleam.toml

```toml
[go-over]
# disables caching (default: true)
cache = true

[go-over.ignore]
# list of package names to skip when checking for advisories & warnings (default: [])
packages = ["example_package"]
# list of warning severities to skip when checking for advisories & warnings (case insensitive) (default: [])
severity = ["example_moderate"]
# list of advisory IDs to skip when checking for advisories & warnings (default: [])
ids = ["GHSA-xxxx-yyyy-zzzz"]
```

### ⌛ Caching

- Security advisory data is cached for **_six_** hours
- hex.pm retired package data is cached for **_one_** hour

# 🖌️ Other Art

21 changes: 10 additions & 11 deletions gleam.toml
Original file line number Diff line number Diff line change
@@ -2,23 +2,14 @@ name = "go_over"
version = "0.2.0"
licences = ["MIT"]
repository = { type = "github", user = "bwireman", repo = "go-over" }
description = "Auditor for erlang and elixir dependencies in gleam projects"
gleam = ">= 1.2.0"
description = "A tool to audit Erlang & Elixir dependency advisories as well as retired hex packages, to make sure your gleam projects really sparkle! ✨"
gleam = ">= 0.34.1"

internal_modules = [
"go_over/*",
]

# Fill out these fields if you intend to generate HTML documentation or publish
# your project to the Hex package manager.
#
# description = ""
# licences = ["Apache-2.0"]
# repository = { type = "github", user = "username", repo = "project" }
# links = [{ title = "Website", href = "https://gleam.run" }]
#
# For a full reference of all the available options, you can have a look at
# https://gleam.run/writing-gleam/gleam-toml/.

[dependencies]
gleam_stdlib = ">= 0.34.0 and < 2.0.0"
@@ -37,3 +28,11 @@ gleamsver = ">= 1.0.0 and < 2.0.0"

[dev-dependencies]
gleeunit = ">= 1.0.0 and < 2.0.0"

[go-over]
cache = true

[go-over.ignore]
packages = []
severity = []
ids = []
Binary file modified images/demo.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
77 changes: 60 additions & 17 deletions src/go_over.gleam
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@ import gleam/list
import gleam/option
import gleam/string
import go_over/advisories
import go_over/config.{type Config}
import go_over/constants
import go_over/packages
import go_over/print
@@ -38,12 +39,24 @@ fn spin_up() -> Flags {

fn get_vulnerable_packages(
pkgs: List(packages.Package),
conf: Config,
flags: Flags,
) -> List(Warning) {
advisories.check_for_advisories(pkgs, !flags.skip)
|> list.map(fn(p) {
let #(pkg, adv) = p

#(pkg, config.filter_advisory_ids(conf, adv))
})
|> list.filter(fn(p) {
case p {
#(_, []) -> False
_ -> True
}
})
|> list.map(fn(p) {
let #(pkg, adv) = p

warning.adv_to_warning(pkg, adv)
})
}
@@ -67,55 +80,85 @@ fn get_retired_packges(
}

fn print_warnings(vulns: List(Warning)) -> Nil {
let len = list.length(vulns)
{
"⛔ "
<> int.to_string(len)
<> int.to_string(list.length(vulns))
<> " WARNING(s) FOUND!"
<> constants.long_ass_dashes
}
|> io.print

vulns
|> list.map(fn(w) {
w
|> warning.format_as_string()
|> print.format_warning()
})
|> list.map(warning.format_as_string)
|> string.join(constants.long_ass_dashes)
|> io.print
shellout.exit(1)
}

pub fn main() {
let flags = spin_up()
throwaway(flags.force, fn() { simplifile.delete(constants.go_over_path()) })
let conf = config.read_config("./gleam.toml")
throwaway(flags.force || !conf.cache, fn() {
simplifile.delete(constants.go_over_path())
})
let pkgs =
packages.read_manifest("./manifest.toml")
|> config.filter_packages(conf, _)

let vulnerable_packages = get_vulnerable_packages(pkgs, conf, flags)
let retired_packages = get_retired_packges(pkgs, flags)

iffnil(flags.fake, fn() {
print_warnings([
Warning(
"Fake Retired Package",
"0.1.2",
"fake",
"x.y.z",
"Retired",
warning.Retired,
warning.Vulnerable,
"Critical",
warning.Direct,
),
Warning(
"Fake Vulnerable Package",
"another_fake",
"1.2.3",
"Vulnerabe",
warning.Vulnerable,
"High",
warning.Direct,
),
Warning(
"and_another",
"4.5.6",
"Vulnerabe",
warning.Vulnerable,
"Moderate",
warning.Direct,
),
Warning(
"one_more",
"7.8.9",
"Vulnerabe",
warning.Vulnerable,
"LOW",
warning.Indirect,
),
Warning(
"this_one_was_retired",
"10.11.12",
"Retired",
warning.Retired,
"Package Retired",
warning.Indirect,
),
])
})

let pkgs = packages.read_manifest("./manifest.toml")
let vulnerable_packages = get_vulnerable_packages(pkgs, flags)
let retired_packages = get_retired_packges(pkgs, flags)
let warnings =
list.append(retired_packages, vulnerable_packages)
|> config.filter_severity(conf, _)

case list.append(retired_packages, vulnerable_packages) {
case warnings {
[] -> print.success("✅ All good! ✨")

vulns -> print_warnings(vulns)
}
}
44 changes: 31 additions & 13 deletions src/go_over/advisories.gleam
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import filepath
import gleam/list
import gleam/option
import gleam/result.{unwrap}
import go_over/cache
import go_over/comparisons
import go_over/constants.{go_over_path, six_hours}
@@ -11,35 +12,52 @@ import go_over/yaml
import shellout
import simplifile

pub type ADV {
ADV(name: String, vulnerable_version_ranges: List(String), file: String)
pub type Advisory {
Advisory(
id: String,
name: String,
severity: String,
vulnerable_version_ranges: List(String),
desciption: String,
)
}

fn path() -> String {
go_over_path()
|> filepath.join("mirego-elixir-security-advisories")
}

fn read_adv(path: String) -> ADV {
let #(name, vulnerable_version_ranges) = yaml.parse(path)
ADV(name, vulnerable_version_ranges, path)
fn read_adv(path: String) -> Advisory {
let #(id, name, severity, desc, vulnerable_version_ranges) = yaml.parse(path)

Advisory(
id: id,
name: name,
severity: severity,
vulnerable_version_ranges: vulnerable_version_ranges,
desciption: desc,
)
}

fn read_all_adv() -> List(ADV) {
fn read_all_adv() -> List(Advisory) {
let packages_path = filepath.join(path(), "packages")

let assert Ok(packages) = simplifile.read_directory(packages_path)
let packages =
simplifile.read_directory(packages_path)
|> unwrap([])
list.flat_map(packages, fn(dir) {
let dir_path = filepath.join(packages_path, dir)

let assert Ok(adv_names) = simplifile.read_directory(dir_path)
let adv_names =
simplifile.read_directory(dir_path)
|> unwrap([])
list.map(adv_names, fn(adv_name) {
read_adv(filepath.join(dir_path, adv_name))
})
})
}

fn is_vulnerable(p: packages.Package, advs: List(ADV)) -> List(ADV) {
fn is_vulnerable(p: packages.Package, advs: List(Advisory)) -> List(Advisory) {
list.map(advs, fn(adv) {
case adv.name == p.name {
False -> option.None
@@ -63,7 +81,7 @@ fn is_vulnerable(p: packages.Package, advs: List(ADV)) -> List(ADV) {
}

fn clone() -> Nil {
print.progress("Cloning mirego/elixir-security-advisories...")
print.progress("Cloning: " <> constants.advisories_repo <> "...")

let assert Ok(Nil) =
path()
@@ -74,7 +92,7 @@ fn clone() -> Nil {
run: "git",
with: [
"clone",
"https://github.com/mirego/elixir-security-advisories.git",
"https://github.com/" <> constants.advisories_repo <> ".git",
path(),
],
in: ".",
@@ -98,13 +116,13 @@ fn delete_and_clone() -> Nil {
pub fn check_for_advisories(
packages: List(packages.Package),
pull: Bool,
) -> List(#(Package, List(ADV))) {
) -> List(#(Package, List(Advisory))) {
iffnil(pull, fn() {
cache.pull_if_not_cached(
path(),
six_hours,
delete_and_clone,
"mirego/elixir-security-advisories",
constants.advisories_repo,
)
})

7 changes: 5 additions & 2 deletions src/go_over/cache.gleam
Original file line number Diff line number Diff line change
@@ -14,7 +14,7 @@ fn file_cached(path: String, max_age_seconds: Int) -> Result(Bool, Nil) {
path
|> cache_name()
|> simplifile.read()
|> result.map_error(fn(_) { Nil })
|> result.replace_error(Nil)
|> result.try(int.base_parse(_, 10))
|> result.map(fn(v) {
let cutoff = birl.from_unix(v + max_age_seconds)
@@ -42,7 +42,10 @@ pub fn pull_if_not_cached(
_ -> {
pullfn()

let now = birl.utc_now() |> birl.to_unix() |> int.to_string()
let now =
birl.utc_now()
|> birl.to_unix()
|> int.to_string()
let assert Ok(_) =
path
|> cache_name()
Loading