Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
25 changes: 25 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand Down Expand Up @@ -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 }"
Expand Down
4 changes: 2 additions & 2 deletions docs/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,6 @@ pub enum OutputFormat {
#[default]
Text,
Json,
Sarif,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
Expand Down
7 changes: 0 additions & 7 deletions src/commands/scan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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}",
Expand Down Expand Up @@ -85,7 +79,6 @@ pub fn execute(args: ScanArgs, config: ZiftConfig) -> Result<()> {
result.enforcement_points,
&mut writer,
)?,
OutputFormat::Sarif => unreachable!("pre-checked above"),
}

Ok(())
Expand Down
1 change: 1 addition & 0 deletions src/deep/candidate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 4 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use std::path::PathBuf;
use thiserror::Error;

use crate::deep::error::DeepError;
use crate::types::Language;

pub type Result<T> = std::result::Result<T, ZiftError>;

Expand All @@ -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),
}
63 changes: 54 additions & 9 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -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<T>`
//! - [`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)
}
11 changes: 1 addition & 10 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -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)
}
2 changes: 2 additions & 0 deletions src/mcp/jsonrpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -120,6 +121,7 @@ pub fn write_response<W: Write>(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 },
Expand Down
1 change: 1 addition & 0 deletions src/mcp/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
11 changes: 7 additions & 4 deletions src/scanner/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,7 @@ pub fn get_language(lang: Language, is_tsx_jsx: bool) -> Result<tree_sitter::Lan
(Language::Java, _) => 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)),
}
}

Expand All @@ -41,6 +39,7 @@ pub fn parse_source(
#[cfg(test)]
mod tests {
use super::*;
use crate::error::ZiftError;

#[test]
fn parse_typescript() {
Expand Down Expand Up @@ -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));
}
}