use std::fs;
use std::path::Path;
pub mod auditors;
pub mod config;
pub mod error;
pub mod fix;
pub mod models;
pub mod parsers;
pub mod tui;
pub use config::load as load_config;
pub use config::Config;
pub use error::{PipecheckError, Result};
pub use models::{AuditOptions, AuditResult, Issue, Severity};
#[must_use = "audit results should be handled"]
pub fn audit_file(path: &str, options: AuditOptions) -> Result<AuditResult> {
let content = std::fs::read_to_string(path)?;
audit_content(&content, options)
}
#[must_use = "audit results should be handled"]
pub fn audit_content(content: &str, options: AuditOptions) -> Result<AuditResult> {
let provider = parsers::detect_provider(content)?;
let pipeline = parsers::parse(content, provider)?;
let mut issues = Vec::new();
issues.extend(auditors::syntax::audit(&pipeline)?);
issues.extend(auditors::dag::audit(&pipeline)?);
issues.extend(auditors::secrets::audit(&pipeline)?);
if options.check_docker_images {
#[cfg(feature = "network")]
{
issues.extend(auditors::pinning::audit(&pipeline)?);
}
#[cfg(not(feature = "network"))]
{
issues.push(Issue::new(
Severity::Info,
"Docker image pinning checks are disabled because the 'network' feature is not enabled.",
Some("Enable the 'network' feature to run image checks".to_string()),
));
}
}
let summary = generate_summary(&issues);
Ok(AuditResult {
provider,
issues,
summary,
})
}
fn generate_summary(issues: &[Issue]) -> String {
let errors = issues
.iter()
.filter(|i| i.severity == Severity::Error)
.count();
let warnings = issues
.iter()
.filter(|i| i.severity == Severity::Warning)
.count();
format!("{} errors, {} warnings", errors, warnings)
}
#[derive(Debug, Clone)]
pub struct DiscoveryOptions {
pub include_github: bool,
pub include_gitlab: bool,
pub include_circleci: bool,
}
impl Default for DiscoveryOptions {
fn default() -> Self {
Self {
include_github: true,
include_gitlab: true,
include_circleci: true,
}
}
}
pub fn discover_workflows(base: &Path, options: &DiscoveryOptions) -> Vec<String> {
let mut files = Vec::new();
if options.include_github {
let wf_dir = base.join(".github/workflows");
if let Ok(entries) = fs::read_dir(&wf_dir) {
for entry in entries.flatten() {
let path = entry.path();
if let Some(ext) = path.extension() {
if ext == "yml" || ext == "yaml" {
if let Some(s) = path.to_str() {
files.push(s.to_string());
}
}
}
}
}
}
if options.include_gitlab {
let p = base.join(".gitlab-ci.yml");
if p.exists() {
if let Some(s) = p.to_str() {
files.push(s.to_string());
}
}
}
if options.include_circleci {
let p = base.join(".circleci/config.yml");
if p.exists() {
if let Some(s) = p.to_str() {
files.push(s.to_string());
}
}
}
files
}
pub fn find_line(content: &str, key: &str) -> (usize, usize) {
for (idx, line) in content.lines().enumerate() {
let trimmed = line.trim_start();
if trimmed.starts_with(key) {
let column = line.len() - trimmed.len() + 1;
return (idx + 1, column);
}
}
(0, 0)
}
pub fn find_line_with_prefix(content: &str, key_prefix: &str, search: &str) -> (usize, usize) {
for (idx, line) in content.lines().enumerate() {
let trimmed = line.trim();
if trimmed.contains(key_prefix) && trimmed.contains(search) {
let column = line.len() - line.trim_start().len() + 1;
return (idx + 1, column);
}
}
(0, 0)
}