use crate::{
audit::audit,
check::{
installed::check_audit,
rustc::{MSRV, check_rustc_version},
},
config::Config,
error::AuditCheckError,
log::initialize,
utils::handle_join_error,
};
use anyhow::Result;
use regex::Regex;
use reqwest::{
Client, Version,
header::{HeaderMap, HeaderValue},
};
use rustc_version::version_meta;
use serde::{Deserialize, Serialize};
use std::{
collections::BTreeMap,
sync::{
LazyLock,
mpsc::{Receiver, channel},
},
thread::spawn,
};
use tokio::runtime::Runtime;
use tracing::{error, info, trace};
pub(crate) fn run() -> Result<()> {
let config = Config::from_env()?;
initialize(config.level)?;
info!("Auditing repository: {}", config.owner_repo);
if check_rustc_version(&version_meta()?)? {
trace!("rustc version check successful");
match check_audit("cargo audit --version") {
Ok(success) => {
if success {
trace!("cargo audit version check successful");
let (tx_stdout, rx_stdout) = channel();
let (tx_stderr, rx_stderr) = channel();
let (tx_code, rx_code) = channel();
let deny_c = config.deny.clone();
let audit_handle = spawn(move || audit(&deny_c, tx_stdout, tx_stderr, tx_code));
let stdout_handle = spawn(move || receive_stdout(&rx_stdout));
let stderr_handle = spawn(move || receive_stderr(&rx_stderr));
let code_handle = spawn(move || receive_code(&rx_code));
audit_handle.join().map_err(handle_join_error)??;
let stdout_buf = stdout_handle.join().map_err(handle_join_error)?;
let stderr_buf = stderr_handle.join().map_err(handle_join_error)?;
let code = code_handle.join().map_err(handle_join_error)?;
if code == 0 {
Ok(())
} else if config.create_issue {
let rt = Runtime::new()?;
rt.block_on(async move {
match create_issue(config, stdout_buf, stderr_buf).await {
Ok(resp) => {
info!("Issue {} created", resp.id);
}
Err(e) => error!("{e}"),
}
});
Err(AuditCheckError::RustSec.into())
} else {
Err(AuditCheckError::RustSec.into())
}
} else {
Err(AuditCheckError::AuditVersionCheck.into())
}
}
Err(e) => Err(e.context("cargo audit check has failed")),
}
} else {
Err(AuditCheckError::RustcVersionCheck { msrv: MSRV }.into())
}
}
fn receive_stdout(rx: &Receiver<String>) -> Vec<String> {
let mut buf = vec![];
while let Ok(message) = rx.recv() {
info!("{message}");
buf.push(message);
}
buf
}
fn receive_stderr(rx: &Receiver<String>) -> Vec<String> {
let mut buf = vec![];
while let Ok(message) = rx.recv() {
info!("{message}");
buf.push(message);
}
buf
}
fn receive_code(rx: &Receiver<i32>) -> i32 {
rx.recv().unwrap_or(-1)
}
#[derive(Clone, Debug, Serialize)]
struct Issue {
title: String,
#[serde(skip_serializing_if = "Option::is_none")]
body: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
milestone: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
labels: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
assignees: Option<Vec<String>>,
}
#[derive(Clone, Debug, Deserialize)]
struct Resp {
id: usize,
}
static APP_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),);
static CRATE_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"Crate: +(.*)").expect("Invalid CRATE_REGEX"));
static VERSION_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"Version: +(.*)").expect("Invalid VERSION_REGEX"));
static WARNING_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"Warning: +(.*)").expect("Invalid WARNING_REGEX"));
static TITLE_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"Title: +(.*)").expect("Invalid TITLE_REGEX"));
static DATE_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"Date: +(.*)").expect("Invalid DATE_REGEX"));
static SOLUTION_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"Solution: +(.*)").expect("Invalid SOLUTION_REGEX"));
static ID_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"ID: +(RUSTSEC.*)").expect("Invalid ID_REGEX"));
static URL_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"URL: +(https:.*)").expect("Invalid URL_REGEX"));
#[allow(dead_code)]
#[derive(Clone, Debug)]
struct Rustsec {
id: String,
url: String,
krate: String,
version: String,
warning: String,
title: String,
date: String,
solution: String,
}
async fn create_issue(
config: Config,
stdout_buf: Vec<String>,
_stderr_buf: Vec<String>,
) -> Result<Resp> {
let token = config.token;
let owner_repo = config.owner_repo;
let stdout = stdout_buf.join("\n");
let rustsec_map = parse(&stdout);
let title = generate_title(&rustsec_map);
let body = generate_body(&rustsec_map);
let mut headers = HeaderMap::new();
let _old = headers.insert(
"Accept",
HeaderValue::from_static("application/vnd.github+json"),
);
let _old = headers.insert(
"X-GitHub-Api-Version",
HeaderValue::from_static("2022-11-28"),
);
let client = Client::builder()
.user_agent(APP_USER_AGENT)
.default_headers(headers)
.build()?;
let url = format!("https://api.github.com/repos/{owner_repo}/issues");
let issue = Issue {
title,
body: Some(body),
milestone: None,
labels: None,
assignees: None,
};
let res = client
.post(&url)
.version(Version::HTTP_11)
.bearer_auth(token)
.json(&issue)
.send()
.await?;
if res.status() == 201 {
Ok(res.json::<Resp>().await?)
} else {
let body = res.bytes().await?;
error!("{}", String::from_utf8_lossy(&body));
Err(AuditCheckError::CreateIssue.into())
}
}
fn parse(output: &str) -> BTreeMap<String, (String, Rustsec)> {
output
.split("\n\n")
.filter(|s| !s.trim().is_empty())
.map(|s| {
let rustsec = parse_rustsec(s);
(rustsec.id.clone(), (s.to_string(), rustsec))
})
.collect()
}
fn parse_rustsec(rustsec_str: &str) -> Rustsec {
let id = parse_caps(&ID_REGEX, rustsec_str, "No ID");
let url = parse_caps(&URL_REGEX, rustsec_str, "No URL");
let krate = parse_caps(&CRATE_REGEX, rustsec_str, "No Crate");
let version = parse_caps(&VERSION_REGEX, rustsec_str, "No Version");
let warning = parse_caps(&WARNING_REGEX, rustsec_str, "No Warning");
let title = parse_caps(&TITLE_REGEX, rustsec_str, "No Title");
let date = parse_caps(&DATE_REGEX, rustsec_str, "No Date");
let solution = parse_caps(&SOLUTION_REGEX, rustsec_str, "No Solution");
Rustsec {
id,
url,
krate,
version,
warning,
title,
date,
solution,
}
}
fn parse_caps(regex: &Regex, rustsec_str: &str, default: &str) -> String {
regex
.captures(rustsec_str)
.map_or_else(
|| default,
|caps| caps.get(1).map_or(default, |m| m.as_str()),
)
.to_string()
}
fn generate_title(rustsec_map: &BTreeMap<String, (String, Rustsec)>) -> String {
rustsec_map.keys().fold(String::new(), |acc, key| {
if acc.is_empty() {
acc + key
} else {
acc + ", " + key
}
})
}
fn generate_body(rustsec_map: &BTreeMap<String, (String, Rustsec)>) -> String {
rustsec_map.iter().fold(String::new(), |acc, (k, v)| {
acc + &format!("# ‼️ {} ‼️\n{}\n\n````\n{}\n````\n\n", k, v.1.url, v.0)
})
}
#[cfg(test)]
mod test {
use super::{
generate_body, generate_title, parse, receive_code, receive_stderr, receive_stdout,
};
use std::sync::mpsc;
const TEST_RUSTSEC: &str = r"Crate: aovec
Version: 1.1.0
Title: Aovec<T> lacks bound on its Send and Sync traits allowing data races
Date: 2020-12-10
ID: RUSTSEC-2020-0099
URL: https://rustsec.org/advisories/RUSTSEC-2020-0099
Solution: No fixed upgrade is available!
Dependency tree:
aovec 1.1.0
└── audit-check-test 0.1.0
Crate: owning_ref
Version: 0.3.3
Title: Multiple soundness issues in `owning_ref`
Date: 2022-01-26
ID: RUSTSEC-2022-0040
URL: https://rustsec.org/advisories/RUSTSEC-2022-0040
Solution: No fixed upgrade is available!
Dependency tree:
owning_ref 0.3.3
└── parking_lot 0.4.8
└── aovec 1.1.0
└── audit-check-test 0.1.0
Crate: anymap
Version: 0.12.1
Warning: unmaintained
Title: anymap is unmaintained.
Date: 2021-05-07
ID: RUSTSEC-2021-0065
URL: https://rustsec.org/advisories/RUSTSEC-2021-0065
Dependency tree:
anymap 0.12.1
└── audit-check-test 0.1.0
Crate: smallvec
Version: 0.4.5
Warning: unsound
Title: smallvec creates uninitialized value of any type
Date: 2018-09-25
ID: RUSTSEC-2018-0018
URL: https://rustsec.org/advisories/RUSTSEC-2018-0018
Dependency tree:
smallvec 0.4.5
└── aovec 1.1.0
└── audit-check-test 0.1.0
";
#[test]
fn parse_works() {
assert_eq!(4, parse(TEST_RUSTSEC).len());
}
#[test]
fn generate_title_works() {
let rustsec_map = parse(TEST_RUSTSEC);
assert_eq!(
"RUSTSEC-2018-0018, RUSTSEC-2020-0099, RUSTSEC-2021-0065, RUSTSEC-2022-0040",
generate_title(&rustsec_map)
);
}
#[test]
fn generate_body_works() {
let rustsec_map = parse(TEST_RUSTSEC);
let body = generate_body(&rustsec_map);
assert!(body.contains("RUSTSEC-2018-0018"));
assert!(body.contains("RUSTSEC-2020-0099"));
assert!(body.contains("RUSTSEC-2021-0065"));
assert!(body.contains("RUSTSEC-2022-0040"));
}
#[test]
fn receive_stdout_works() {
let (tx, rx) = mpsc::channel();
tx.send("line1".to_string()).unwrap();
tx.send("line2".to_string()).unwrap();
drop(tx);
let result = receive_stdout(&rx);
assert_eq!(result, vec!["line1", "line2"]);
}
#[test]
fn receive_stderr_works() {
let (tx, rx) = mpsc::channel();
tx.send("error1".to_string()).unwrap();
drop(tx);
let result = receive_stderr(&rx);
assert_eq!(result, vec!["error1"]);
}
#[test]
fn receive_code_works() {
let (tx, rx) = mpsc::channel();
tx.send(42i32).unwrap();
let result = receive_code(&rx);
assert_eq!(result, 42);
}
#[test]
fn receive_code_disconnected_returns_minus_one() {
let (tx, rx) = mpsc::channel::<i32>();
drop(tx);
let result = receive_code(&rx);
assert_eq!(result, -1);
}
}