diff --git a/README.md b/README.md index 73dc0e8f..619f7436 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,7 @@ typically in 1-2 seconds. Only the executable is stored on disk. Execution is - [scc](https://github.com/boyter/scc) - [shellcheck](https://www.shellcheck.net) - [shfmt](https://github.com/mvdan/sh) +- [snyk](https://github.com/snyk/cli) - [staticcheck](https://staticcheck.dev) - [taplo](https://github.com/tamasfe/taplo) - [tikibase](https://github.com/kevgo/tikibase) diff --git a/src/applications/mod.rs b/src/applications/mod.rs index 25252ffe..26c3fb89 100644 --- a/src/applications/mod.rs +++ b/src/applications/mod.rs @@ -39,6 +39,7 @@ mod rumdl; mod scc; mod shellcheck; mod shfmt; +mod snyk; mod staticcheck; mod taplo; mod tikibase; @@ -96,6 +97,7 @@ pub(crate) fn all() -> Apps { Box::new(scc::Scc {}), Box::new(shellcheck::ShellCheck {}), Box::new(shfmt::Shfmt {}), + Box::new(snyk::Snyk {}), Box::new(staticcheck::StaticCheck {}), Box::new(taplo::Taplo {}), Box::new(tikibase::Tikibase {}), diff --git a/src/applications/snyk.rs b/src/applications/snyk.rs new file mode 100644 index 00000000..a9953361 --- /dev/null +++ b/src/applications/snyk.rs @@ -0,0 +1,175 @@ +use super::{AnalyzeResult, AppDefinition, ApplicationName}; +use crate::configuration::Version; +use crate::error::Result; +use crate::executables::{Executable, RunMethod}; +use crate::hosting::github_releases; +use crate::installation::Method; +use crate::platform::{Cpu, Os, Platform}; +use crate::{Log, strings}; +use const_format::formatcp; + +#[derive(Clone)] +pub(crate) struct Snyk {} + +const ORG: &str = "snyk"; +const REPO: &str = "cli"; +const TAG_PREFIX: &str = "v"; + +impl AppDefinition for Snyk { + fn name(&self) -> ApplicationName { + "snyk".into() + } + + fn homepage(&self) -> &'static str { + formatcp!("https://github.com/{ORG}/{REPO}") + } + + fn run_method(&self, version: &Version, platform: Platform) -> RunMethod { + let os = match platform.os { + Os::Linux => "linux", + Os::MacOS => "macos", + Os::Windows => "win", + }; + let cpu = match platform.cpu { + Cpu::Arm64 => "-arm64", + Cpu::Intel64 => "", + }; + let ext = match platform.os { + Os::Linux | Os::MacOS => "", + Os::Windows => ".exe", + }; + RunMethod::ThisApp { + install_methods: vec![Method::DownloadExecutable { + url: format!("https://github.com/{ORG}/{REPO}/releases/download/{TAG_PREFIX}{version}/snyk-{os}{cpu}{ext}").into(), + }], + } + } + + fn latest_installable_version(&self, log: Log) -> Result { + github_releases::latest(ORG, REPO, TAG_PREFIX, log) + } + + fn installable_versions(&self, amount: usize, log: Log) -> Result> { + github_releases::versions(ORG, REPO, amount, TAG_PREFIX, log) + } + + fn analyze_executable(&self, executable: &Executable, log: Log) -> Result { + let output = executable.run_output(&["-h"], log)?; + if !output.contains("Snyk CLI scans and monitors your projects for security vulnerabilities and license issues") { + return Ok(AnalyzeResult::NotIdentified { output }); + } + match extract_version(&executable.run_output(&["--version"], log)?) { + Ok(version) => Ok(AnalyzeResult::IdentifiedWithVersion(version.into())), + Err(_) => Ok(AnalyzeResult::IdentifiedButUnknownVersion), + } + } +} + +fn extract_version(output: &str) -> Result<&str> { + strings::first_capture(output, r"^(\d+\.\d+\.\d+)$") +} + +#[cfg(test)] +mod tests { + use crate::UserError; + + mod install_methods { + use crate::applications::AppDefinition; + use crate::applications::snyk::Snyk; + use crate::configuration::Version; + use crate::executables::RunMethod; + use crate::installation::Method; + use crate::platform::{Cpu, Os, Platform}; + + #[test] + fn linux_arm() { + let have = (Snyk {}).run_method( + &Version::from("1.1304.1"), + Platform { + os: Os::Linux, + cpu: Cpu::Arm64, + }, + ); + let want = RunMethod::ThisApp { + install_methods: vec![Method::DownloadExecutable { + url: "https://github.com/snyk/cli/releases/download/v1.1304.1/snyk-linux-arm64".into(), + }], + }; + assert_eq!(have, want); + } + + #[test] + fn linux_intel() { + let have = (Snyk {}).run_method( + &Version::from("1.1304.1"), + Platform { + os: Os::Linux, + cpu: Cpu::Intel64, + }, + ); + let want = RunMethod::ThisApp { + install_methods: vec![Method::DownloadExecutable { + url: "https://github.com/snyk/cli/releases/download/v1.1304.1/snyk-linux".into(), + }], + }; + assert_eq!(have, want); + } + + #[test] + fn macos_arm() { + let have = (Snyk {}).run_method( + &Version::from("1.1304.1"), + Platform { + os: Os::MacOS, + cpu: Cpu::Arm64, + }, + ); + let want = RunMethod::ThisApp { + install_methods: vec![Method::DownloadExecutable { + url: "https://github.com/snyk/cli/releases/download/v1.1304.1/snyk-macos-arm64".into(), + }], + }; + assert_eq!(have, want); + } + + #[test] + fn macos_intel() { + let have = (Snyk {}).run_method( + &Version::from("1.1304.1"), + Platform { + os: Os::MacOS, + cpu: Cpu::Intel64, + }, + ); + let want = RunMethod::ThisApp { + install_methods: vec![Method::DownloadExecutable { + url: "https://github.com/snyk/cli/releases/download/v1.1304.1/snyk-macos".into(), + }], + }; + assert_eq!(have, want); + } + + #[test] + fn windows_intel() { + let have = (Snyk {}).run_method( + &Version::from("1.1304.1"), + Platform { + os: Os::Windows, + cpu: Cpu::Intel64, + }, + ); + let want = RunMethod::ThisApp { + install_methods: vec![Method::DownloadExecutable { + url: "https://github.com/snyk/cli/releases/download/v1.1304.1/snyk-win.exe".into(), + }], + }; + assert_eq!(have, want); + } + } + + #[test] + fn extract_version() { + assert_eq!(super::extract_version("1.1304.1"), Ok("1.1304.1")); + assert_eq!(super::extract_version("other"), Err(UserError::RegexDoesntMatch)); + } +}