diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 33ec5c2..1988864 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,7 +28,7 @@ jobs: run: cargo fmt -- --check - name: Clippy - run: cargo clippy -- -D warnings + run: cargo clippy --all-features -- -D warnings - name: Tests - run: cargo test + run: cargo test --all-features diff --git a/AGENTS.md b/AGENTS.md index 485f26b..acb2746 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -36,7 +36,7 @@ cargo clippy -- -D warnings - **Scanner** (`src/scanner/`): Tree-sitter AST parsing and pattern matching across languages - **Rules** (`rules/`): TOML-based pattern definitions with tree-sitter queries and policy templates (Rego today) - **Rego** (`src/rego/`): Policy-as-Code generation from scan findings (Rego/OPA today; additional engines like Cedar planned) -- **Output** (`src/output/`): Formatters (JSON, text; SARIF planned) +- **Output** (`src/output/`): Formatters (JSON, text) ### Design principles diff --git a/Cargo.toml b/Cargo.toml index d0f8720..1e7e60f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,11 @@ readme = "README.md" keywords = ["authorization", "policy-as-code", "opa", "rego", "static-analysis"] categories = ["command-line-utilities", "development-tools"] +[features] +# Exposes internal implementation modules (config, deep, mcp, output, scanner) +# as public API. No semver stability guarantee; opt in explicitly. +unstable = [] + [dependencies] clap = { version = "4", features = ["derive", "env"] } serde = { version = "1", features = ["derive"] } @@ -41,6 +46,26 @@ libc = "0.2" tempfile = "3" mockito = "1" +# Integration tests in `tests/` reach into modules gated behind `unstable` +# (deep, mcp, scanner, config). Mark them as requiring the feature so +# default `cargo test` skips them cleanly; CI runs `cargo test --features +# unstable` to exercise them. +[[test]] +name = "deep_http_integration" +required-features = ["unstable"] + +[[test]] +name = "deep_subprocess_integration" +required-features = ["unstable"] + +[[test]] +name = "mcp_stdio_integration" +required-features = ["unstable"] + +[[test]] +name = "scanner_enforcement_points" +required-features = ["unstable"] + [package.metadata.binstall] pkg-url = "{ repo }/releases/download/v{ version }/zift-{ target }{ archive-suffix }" bin-dir = "{ bin }{ binary-ext }" diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 40815e5..dcc5dcd 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -254,7 +254,7 @@ SCAN OPTIONS: --category, -c Filter to specific auth category(s) --confidence Minimum confidence level (high|medium|low) --exclude, -e Glob patterns to exclude - --format, -f Output format (text|json|sarif) + --format, -f Output format (text|json) --output, -o Write findings to file (default: stdout) --rules-dir Additional pattern rules directory --config Path to config file (default: .zift.toml) @@ -344,7 +344,7 @@ additional = ["./custom-rules"] - Integration with findings pipeline ### Phase 5: CI integration + SARIF (3-5 days) -- SARIF output format (for GitHub Code Scanning, VS Code) +- SARIF output format (for GitHub Code Scanning, VS Code) — planned for v0.2 - Exit codes for CI (findings above threshold → non-zero) - Baseline/diff mode (only report new findings since last scan) - GitHub Actions example workflow diff --git a/src/cli.rs b/src/cli.rs index 648ac9e..a23c8a9 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -217,7 +217,6 @@ pub enum OutputFormat { #[default] Text, Json, - Sarif, } #[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)] diff --git a/src/commands/scan.rs b/src/commands/scan.rs index 14d3af0..182630c 100644 --- a/src/commands/scan.rs +++ b/src/commands/scan.rs @@ -7,12 +7,6 @@ use crate::rules; use crate::scanner; pub fn execute(args: ScanArgs, config: ZiftConfig) -> Result<()> { - if matches!(args.format, OutputFormat::Sarif) { - return Err(ZiftError::General( - "SARIF output not yet implemented".into(), - )); - } - let path = args.path.canonicalize().map_err(|e| { ZiftError::General(format!( "failed to resolve path '{}': {e}", @@ -85,7 +79,6 @@ pub fn execute(args: ScanArgs, config: ZiftConfig) -> Result<()> { result.enforcement_points, &mut writer, )?, - OutputFormat::Sarif => unreachable!("pre-checked above"), } Ok(()) diff --git a/src/deep/candidate.rs b/src/deep/candidate.rs index 26ded77..4579050 100644 --- a/src/deep/candidate.rs +++ b/src/deep/candidate.rs @@ -118,6 +118,7 @@ pub enum CandidateKind { } #[derive(Debug, Clone)] +#[allow(dead_code)] // `kind`/`seed_category` are read in tests + Debug output pub struct Candidate { pub kind: CandidateKind, /// Path relative to scan root. diff --git a/src/error.rs b/src/error.rs index c8ac6dc..a23c15a 100644 --- a/src/error.rs +++ b/src/error.rs @@ -3,6 +3,7 @@ use std::path::PathBuf; use thiserror::Error; use crate::deep::error::DeepError; +use crate::types::Language; pub type Result = std::result::Result; @@ -26,6 +27,9 @@ pub enum ZiftError { #[error("deep scan: {0}")] Deep(#[from] DeepError), + #[error("{0} is not yet supported; see the README for the current language matrix")] + UnsupportedLanguage(Language), + #[error("{0}")] General(String), } diff --git a/src/lib.rs b/src/lib.rs index d6576c8..09c2414 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,18 +1,63 @@ //! Zift — static analysis for embedded authorization logic. //! -//! This crate is published as both a binary (`zift`) and a library. The -//! binary at `src/main.rs` is a thin shim over the library; downstream -//! consumers (e.g. the MCP server in PR 2) can depend on the library. +//! This crate is published as both a binary (`zift`) and a library. +//! +//! # Stable public API +//! +//! The types below form the semver-committed surface. Everything else is +//! internal or opt-in via `--features unstable`. +//! +//! - [`cli`] — CLI argument types (`Cli`, `ScanArgs`, …) +//! - [`error`] — `ZiftError` and `Result` +//! - [`types`] — core data types (`Finding`, `Language`, `AuthCategory`, …) +//! - [`rules`] — rule loading (read-only) +//! - [`rego`] — policy generation; `rego::validator` is the stable surface +//! - [`run`] — binary entry point +// Stable public API pub mod cli; -pub mod commands; +pub mod error; +pub mod rego; +pub mod rules; +pub mod types; + +// Internal implementation — not accessible to downstream crates; the binary +// reaches these only through `run()` below. +mod commands; +mod logging; + +// Internal modules exposed conditionally when the `unstable` feature is +// enabled. Within this crate they are always reachable via `crate::…`. +#[cfg(feature = "unstable")] pub mod config; +#[cfg(not(feature = "unstable"))] +mod config; + +#[cfg(feature = "unstable")] pub mod deep; -pub mod error; -pub mod logging; +#[cfg(not(feature = "unstable"))] +mod deep; + +#[cfg(feature = "unstable")] pub mod mcp; +#[cfg(not(feature = "unstable"))] +mod mcp; + +#[cfg(feature = "unstable")] pub mod output; -pub mod rego; -pub mod rules; +#[cfg(not(feature = "unstable"))] +mod output; + +#[cfg(feature = "unstable")] pub mod scanner; -pub mod types; +#[cfg(not(feature = "unstable"))] +mod scanner; + +/// Entry point used by the `zift` binary. +/// +/// Initialises logging, loads config, and dispatches the CLI command. +pub fn run(cli: cli::Cli) -> error::Result<()> { + logging::init(cli.verbose); + let cfg = config::load_config(&cli.config)?; + commands::dispatch(cli, cfg) +} diff --git a/src/main.rs b/src/main.rs index 2dfb69d..59bb3a0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,19 +1,10 @@ use clap::Parser; use zift::cli::Cli; -use zift::{commands, config, error, logging}; fn main() { - let cli = Cli::parse(); - logging::init(cli.verbose); - - if let Err(e) = run(cli) { + if let Err(e) = zift::run(Cli::parse()) { eprintln!("Error: {e}"); std::process::exit(1); } } - -fn run(cli: Cli) -> error::Result<()> { - let config = config::load_config(&cli.config)?; - commands::dispatch(cli, config) -} diff --git a/src/mcp/jsonrpc.rs b/src/mcp/jsonrpc.rs index 08c0403..d701150 100644 --- a/src/mcp/jsonrpc.rs +++ b/src/mcp/jsonrpc.rs @@ -9,6 +9,7 @@ use serde_json::Value; use std::io::{BufRead, Write}; /// Standard JSON-RPC 2.0 error codes (-32700 … -32600 reserved). +#[allow(dead_code)] // full set kept for protocol completeness pub mod error_code { pub const PARSE_ERROR: i32 = -32700; pub const INVALID_REQUEST: i32 = -32600; @@ -120,6 +121,7 @@ pub fn write_response(writer: &mut W, resp: &Response) -> std::io::Res } #[derive(Debug)] +#[allow(dead_code)] // `raw` retained for Debug-trace diagnostics pub enum FrameError { Io(std::io::Error), Parse { message: String, raw: String }, diff --git a/src/mcp/server.rs b/src/mcp/server.rs index 28e4587..abbdc0e 100644 --- a/src/mcp/server.rs +++ b/src/mcp/server.rs @@ -135,6 +135,7 @@ pub fn handle_request(ctx: &ServerContext, req: &Request, id: Value) -> Response /// Errors a handler can return at the JSON-RPC layer (distinct from tool /// errors, which are surfaced inside a `tools/call` result with `isError: true`). #[derive(Debug)] +#[allow(dead_code)] // `Internal` matched in dispatch; reserved for future handlers pub enum HandlerError { InvalidParams(String), Internal(String), diff --git a/src/scanner/parser.rs b/src/scanner/parser.rs index ec9fb72..963b508 100644 --- a/src/scanner/parser.rs +++ b/src/scanner/parser.rs @@ -16,9 +16,7 @@ pub fn get_language(lang: Language, is_tsx_jsx: bool) -> Result Ok(tree_sitter_java::LANGUAGE.into()), (Language::Python, _) => Ok(tree_sitter_python::LANGUAGE.into()), (Language::Go, _) => Ok(tree_sitter_go::LANGUAGE.into()), - _ => Err(ZiftError::General(format!( - "language {lang:?} not yet supported" - ))), + _ => Err(ZiftError::UnsupportedLanguage(lang)), } } @@ -41,6 +39,7 @@ pub fn parse_source( #[cfg(test)] mod tests { use super::*; + use crate::error::ZiftError; #[test] fn parse_typescript() { @@ -110,7 +109,11 @@ mod tests { // C# has no structural grammar wired up yet — kept as the canary // that `unsupported_language_returns_error` keeps testing what its // name says it does. (Was Go before v0.2 added Go support.) - assert!(get_language(Language::CSharp, false).is_err()); + let err = get_language(Language::CSharp, false).unwrap_err(); + assert!(matches!( + err, + ZiftError::UnsupportedLanguage(Language::CSharp) + )); assert!(!is_language_supported(Language::CSharp)); } }