All checks were successful
CI / Format (push) Successful in 4s
CI / Clippy (push) Successful in 4m19s
CI / Security Audit (push) Successful in 1m44s
CI / Tests (push) Successful in 5m15s
CI / Detect Changes (push) Successful in 5s
CI / Deploy Agent (push) Successful in 2s
CI / Deploy Dashboard (push) Successful in 2s
CI / Deploy Docs (push) Has been skipped
CI / Deploy MCP (push) Successful in 2s
98 lines
3.0 KiB
Rust
98 lines
3.0 KiB
Rust
mod clippy;
|
|
mod eslint;
|
|
mod ruff;
|
|
|
|
use std::path::Path;
|
|
use std::time::Duration;
|
|
|
|
use compliance_core::models::ScanType;
|
|
use compliance_core::traits::{ScanOutput, Scanner};
|
|
use compliance_core::CoreError;
|
|
|
|
/// Timeout for each individual lint command
|
|
pub(crate) const LINT_TIMEOUT: Duration = Duration::from_secs(120);
|
|
|
|
pub struct LintScanner;
|
|
|
|
impl Scanner for LintScanner {
|
|
fn name(&self) -> &str {
|
|
"lint"
|
|
}
|
|
|
|
fn scan_type(&self) -> ScanType {
|
|
ScanType::Lint
|
|
}
|
|
|
|
#[tracing::instrument(skip_all)]
|
|
async fn scan(&self, repo_path: &Path, repo_id: &str) -> Result<ScanOutput, CoreError> {
|
|
let mut all_findings = Vec::new();
|
|
|
|
// Detect which languages are present and run appropriate linters
|
|
if has_rust_project(repo_path) {
|
|
match clippy::run_clippy(repo_path, repo_id).await {
|
|
Ok(findings) => all_findings.extend(findings),
|
|
Err(e) => tracing::warn!("Clippy failed: {e}"),
|
|
}
|
|
}
|
|
|
|
if has_js_project(repo_path) {
|
|
match eslint::run_eslint(repo_path, repo_id).await {
|
|
Ok(findings) => all_findings.extend(findings),
|
|
Err(e) => tracing::warn!("ESLint failed: {e}"),
|
|
}
|
|
}
|
|
|
|
if has_python_project(repo_path) {
|
|
match ruff::run_ruff(repo_path, repo_id).await {
|
|
Ok(findings) => all_findings.extend(findings),
|
|
Err(e) => tracing::warn!("Ruff failed: {e}"),
|
|
}
|
|
}
|
|
|
|
Ok(ScanOutput {
|
|
findings: all_findings,
|
|
sbom_entries: Vec::new(),
|
|
})
|
|
}
|
|
}
|
|
|
|
fn has_rust_project(repo_path: &Path) -> bool {
|
|
repo_path.join("Cargo.toml").exists()
|
|
}
|
|
|
|
fn has_js_project(repo_path: &Path) -> bool {
|
|
// Only run if eslint is actually installed in the project
|
|
repo_path.join("package.json").exists() && repo_path.join("node_modules/.bin/eslint").exists()
|
|
}
|
|
|
|
fn has_python_project(repo_path: &Path) -> bool {
|
|
repo_path.join("pyproject.toml").exists()
|
|
|| repo_path.join("setup.py").exists()
|
|
|| repo_path.join("requirements.txt").exists()
|
|
}
|
|
|
|
/// Run a command with a timeout, returning its output or an error
|
|
pub(crate) async fn run_with_timeout(
|
|
child: tokio::process::Child,
|
|
scanner_name: &str,
|
|
) -> Result<std::process::Output, CoreError> {
|
|
let result = tokio::time::timeout(LINT_TIMEOUT, child.wait_with_output()).await;
|
|
match result {
|
|
Ok(Ok(output)) => Ok(output),
|
|
Ok(Err(e)) => Err(CoreError::Scanner {
|
|
scanner: scanner_name.to_string(),
|
|
source: Box::new(e),
|
|
}),
|
|
Err(_) => {
|
|
// Process is dropped here which sends SIGKILL on Unix
|
|
Err(CoreError::Scanner {
|
|
scanner: scanner_name.to_string(),
|
|
source: Box::new(std::io::Error::new(
|
|
std::io::ErrorKind::TimedOut,
|
|
format!("{scanner_name} timed out after {}s", LINT_TIMEOUT.as_secs()),
|
|
)),
|
|
})
|
|
}
|
|
}
|
|
}
|