feat(security-sidecar): implement a security tooling sidecar with tiered installer and protocol
- Add `zesdex_sec_daemon` module with main entry point for running the security daemon. - Implement `TieredInstaller` for installing security tools from various sources (pip, binaries, gems). - Create a newline-delimited JSON frame protocol for communication between the daemon and tools. - Introduce a `ToolRegistry` for managing and dispatching tool executions. - Add various tools including HTTP, SQLMap, Nuclei, and more with their respective execution logic. - Establish health check and installation commands for tool management. - Include prompts for classifier and quality reviewer to enhance code review and safety checks. - Document the system's tools and guidelines for usage.
This commit is contained in:
+3
-65
@@ -94,9 +94,6 @@ impl ReviewSystem {
|
||||
violation_window: 10,
|
||||
}
|
||||
}
|
||||
|
||||
/// Record a shadow hit for the given pattern. Returns true if the
|
||||
/// trial window is complete and the check should be evaluated.
|
||||
pub fn record_shadow_hit(&mut self, pattern: &str) -> bool {
|
||||
for check in &mut self.shadow_violations {
|
||||
if check.pattern == pattern {
|
||||
@@ -105,7 +102,6 @@ impl ReviewSystem {
|
||||
return check.trial_count >= check.trial_window;
|
||||
}
|
||||
}
|
||||
// First sighting: start a new shadow trial.
|
||||
self.shadow_violations.push(ShadowCheck {
|
||||
pattern: pattern.to_string(),
|
||||
trial_window: 10,
|
||||
@@ -115,9 +111,6 @@ impl ReviewSystem {
|
||||
});
|
||||
false
|
||||
}
|
||||
|
||||
/// Evaluate all shadow checks whose trial window is complete.
|
||||
/// Graduates those with a high enough hit ratio; demotes the rest.
|
||||
pub fn evaluate_shadow_trials(&mut self) -> Vec<String> {
|
||||
let mut graduated = Vec::new();
|
||||
let mut remaining = Vec::new();
|
||||
@@ -131,13 +124,11 @@ impl ReviewSystem {
|
||||
let tp = check.trial_passed;
|
||||
let tw = check.trial_window;
|
||||
if ratio >= 0.3 {
|
||||
// Graduation threshold: fired on at least 30% of matching writes.
|
||||
self.graduated_checks.push(crate::tool::GraduatedCheck {
|
||||
name: p.clone(),
|
||||
pattern: p.clone(),
|
||||
rule: p.clone(),
|
||||
});
|
||||
// Keep check as inactive so it doesn't re-process.
|
||||
graduated.push(format!("{} (graduated, fired {}/{} writes)", p, tp, tw));
|
||||
} else {
|
||||
check.status = ShadowStatus::Rejected;
|
||||
@@ -214,17 +205,13 @@ pub fn should_trigger_review(state: &AppStateRest, origin: Origin) -> bool {
|
||||
if !state.settings.review_enabled {
|
||||
return false;
|
||||
}
|
||||
// Always review if edits were made this turn.
|
||||
if runtime.edit_count > 0 && runtime.edit_count % 5 == 0 {
|
||||
return true;
|
||||
}
|
||||
// Adaptive skip: consecutive empty reviews throttle frequency.
|
||||
// Backoff schedule: skip 0, 0, 1, 2, 4, 8... reviews between passes.
|
||||
let base: u32 = state.settings.adaptive_review_max_skip.max(2);
|
||||
let consecutive = runtime.consecutive_empty_reviews;
|
||||
if consecutive >= base {
|
||||
let skip = 1u32 << (consecutive - base).min(10); // max ~1024
|
||||
// Only trigger if the edit milestone aligns with the skip window.
|
||||
let skip = 1u32 << (consecutive - base).min(10);
|
||||
if runtime.edit_count > 0 && (runtime.edit_count % skip == 0) {
|
||||
return true;
|
||||
}
|
||||
@@ -232,8 +219,6 @@ pub fn should_trigger_review(state: &AppStateRest, origin: Origin) -> bool {
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Result of running the project's build/test verification.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ProbeResult {
|
||||
pub command: String,
|
||||
@@ -241,12 +226,6 @@ pub struct ProbeResult {
|
||||
pub output: String,
|
||||
pub timed_out: bool,
|
||||
}
|
||||
|
||||
/// Language-agnostic build/test probe.
|
||||
///
|
||||
/// Uses settings.verify_command override first; falls back to probing for
|
||||
/// well-known project markers in the workspace root. Returns None when no
|
||||
/// marker or command matches (review proceeds on reasons+diff alone).
|
||||
pub fn probe_build_test(workspaces: &[std::path::PathBuf], verify_command: Option<&str>, timeout_ms: u64) -> Option<ProbeResult> {
|
||||
let probe_dir = workspaces.first()?;
|
||||
let cmd = resolve_verify_command(probe_dir, verify_command)?;
|
||||
@@ -304,10 +283,7 @@ fn resolve_verify_command(probe_dir: &std::path::Path, override_cmd: Option<&str
|
||||
}
|
||||
let has_file = |name: &str| probe_dir.join(name).exists();
|
||||
let has_dir = |name: &str| probe_dir.join(name).is_dir();
|
||||
|
||||
// Ordered probe: most specific/significant first.
|
||||
if has_file("Cargo.toml") {
|
||||
// Rust workspace: cargo build first, then test if that passes.
|
||||
if has_dir("src") || has_dir("tests") {
|
||||
return Some("cargo build 2>&1 && cargo test 2>&1".to_string());
|
||||
}
|
||||
@@ -320,7 +296,6 @@ fn resolve_verify_command(probe_dir: &std::path::Path, override_cmd: Option<&str
|
||||
let pkg = std::fs::read_to_string(probe_dir.join("package.json")).ok()?;
|
||||
if let Ok(v) = serde_json::from_str::<serde_json::Value>(&pkg) {
|
||||
let scripts = v.get("scripts")?;
|
||||
// Prefer a "test" script, then "build".
|
||||
if scripts.get("test").and_then(|s| s.as_str()).filter(|s| !s.is_empty()).is_some() {
|
||||
return Some("npm test 2>&1".to_string());
|
||||
}
|
||||
@@ -328,11 +303,10 @@ fn resolve_verify_command(probe_dir: &std::path::Path, override_cmd: Option<&str
|
||||
return Some("npm run build 2>&1".to_string());
|
||||
}
|
||||
}
|
||||
return Some("npm test 2>&1".to_string()); // best-effort fallback
|
||||
return Some("npm test 2>&1".to_string());
|
||||
}
|
||||
if has_file("pyproject.toml") || has_file("requirements.txt") || has_file("setup.py") || has_file("setup.cfg") || has_file("Pipfile") || has_file("poetry.lock") {
|
||||
if has_file("pyproject.toml") {
|
||||
// Check for pytest config in pyproject.toml
|
||||
let content = std::fs::read_to_string(probe_dir.join("pyproject.toml")).unwrap_or_default();
|
||||
if content.contains("[tool.pytest") {
|
||||
return Some("python -m pytest --tb=short -q 2>&1".to_string());
|
||||
@@ -341,7 +315,6 @@ fn resolve_verify_command(probe_dir: &std::path::Path, override_cmd: Option<&str
|
||||
if has_dir("tests") || has_dir("test") {
|
||||
return Some("python -m pytest --tb=short -q 2>&1".to_string());
|
||||
}
|
||||
// No test dir: maybe a library or script project, skip verification.
|
||||
return None;
|
||||
}
|
||||
if has_file("Cargo.lock") {
|
||||
@@ -415,8 +388,6 @@ pub fn trigger_review(state: &mut AppStateRest) -> anyhow::Result<()> {
|
||||
);
|
||||
let mut ctx = build_subagent_context(def);
|
||||
ctx.session_dir = state.session_dir.clone();
|
||||
|
||||
// Run build/test verification probe before spawning the reviewer.
|
||||
let probe_result = probe_build_test(
|
||||
&state.workspace_roots,
|
||||
state.settings.verify_command.as_deref(),
|
||||
@@ -480,9 +451,6 @@ pub fn trigger_review(state: &mut AppStateRest) -> anyhow::Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Called after a review subagent completes. Updates the empty-review counter
|
||||
/// and checks for escalation on repeated violations.
|
||||
pub fn record_review_outcome(
|
||||
lessons_found: usize,
|
||||
state: &mut AppStateRest,
|
||||
@@ -493,20 +461,15 @@ pub fn record_review_outcome(
|
||||
};
|
||||
|
||||
if lessons_found > 0 {
|
||||
// Lesson found: reset empty counter.
|
||||
runtime.consecutive_empty_reviews = 0;
|
||||
runtime.review_count += 1;
|
||||
None
|
||||
} else {
|
||||
// Empty review: increment counter.
|
||||
runtime.consecutive_empty_reviews += 1;
|
||||
runtime.review_count += 1;
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Check for repeated violations of a known lesson pattern and
|
||||
/// produce an escalation note if threshold is crossed.
|
||||
pub fn check_violation_escalation(
|
||||
pattern: &str,
|
||||
system: &mut ReviewSystem,
|
||||
@@ -519,8 +482,6 @@ pub fn check_violation_escalation(
|
||||
ViolationEscalation::Block => Some(level),
|
||||
}
|
||||
}
|
||||
|
||||
/// Build an escalation note message for the UI.
|
||||
pub fn format_escalation_note(pattern: &str, level: ViolationEscalation) -> String {
|
||||
let label = match level {
|
||||
ViolationEscalation::None => "none",
|
||||
@@ -549,8 +510,6 @@ pub fn format_escalation_note(pattern: &str, level: ViolationEscalation) -> Stri
|
||||
)
|
||||
}
|
||||
|
||||
// ── Lesson lifecycle: staleness sweep ──────────────────────────────
|
||||
|
||||
const STALE_AFTER_DAYS: i64 = 60;
|
||||
|
||||
pub fn run_staleness_sweep(memory_dir: &std::path::Path) -> std::io::Result<Vec<String>> {
|
||||
@@ -572,7 +531,6 @@ pub fn run_staleness_sweep(memory_dir: &std::path::Path) -> std::io::Result<Vec<
|
||||
|
||||
pub fn maybe_run_staleness_sweep(state: &mut AppStateRest) {
|
||||
let now = chrono::Utc::now().timestamp_millis();
|
||||
// Only run every 10 minutes at most.
|
||||
if now.saturating_sub(state.misc.last_staleness_sweep_ms) < 600_000 {
|
||||
return;
|
||||
}
|
||||
@@ -587,26 +545,16 @@ pub fn maybe_run_staleness_sweep(state: &mut AppStateRest) {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Contradiction detection ────────────────────────────────────────
|
||||
//
|
||||
// A cheap, no-embedding heuristic: split the new text into normalized
|
||||
// directive phrases ("always use X", "never use Y", "prefer Z") and check
|
||||
// for an existing lesson with the *opposite* directive on the same topic.
|
||||
|
||||
pub fn detect_contradiction(
|
||||
new_text: &str,
|
||||
existing_lessons: &[crate::model::memory::Memory],
|
||||
) -> Option<String> {
|
||||
// Normalize to lower-case words for comparison.
|
||||
let new_words: std::collections::HashSet<String> = new_text
|
||||
.to_lowercase()
|
||||
.split(|c: char| !c.is_alphanumeric())
|
||||
.filter(|w| w.len() >= 4 && !is_stop_word(w))
|
||||
.map(|w| w.to_string())
|
||||
.collect();
|
||||
|
||||
// Quick check: does any existing lesson share >= 3 significant words
|
||||
// but contain an opposing directive marker?
|
||||
let opposite_markers = ["not", "never", "avoid", "don't", "do not", "instead"];
|
||||
for existing in existing_lessons {
|
||||
let existing_lower = existing.content.to_lowercase();
|
||||
@@ -618,7 +566,6 @@ pub fn detect_contradiction(
|
||||
|
||||
let shared = new_words.intersection(&exist_words).count();
|
||||
if shared >= 3 {
|
||||
// Same topic -- check for opposing directive.
|
||||
let new_has_opposite = opposite_markers.iter().any(|m| new_text.to_lowercase().contains(m));
|
||||
let old_has_opposite = opposite_markers.iter().any(|m| existing_lower.contains(m));
|
||||
if new_has_opposite != old_has_opposite {
|
||||
@@ -643,8 +590,6 @@ fn is_stop_word(w: &str) -> bool {
|
||||
)
|
||||
}
|
||||
|
||||
// ── Pending lesson calibration queue ───────────────────────────────
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PendingLesson {
|
||||
pub lesson: Lesson,
|
||||
@@ -675,13 +620,10 @@ pub fn add_pending_lesson(session_dir: &std::path::Path, lesson: Lesson, auto_re
|
||||
});
|
||||
save_pending_lessons(session_dir, &pending)
|
||||
}
|
||||
|
||||
/// Process pending lessons: resolve auto-resolve ones (Auto/Yolo mode) after
|
||||
/// a grace window, return ones that need explicit keypress.
|
||||
pub fn process_pending_lessons(session_dir: &std::path::Path, memory_dir: &std::path::Path) -> std::io::Result<Vec<PendingLesson>> {
|
||||
let pending = load_pending_lessons(session_dir);
|
||||
let now = chrono::Utc::now().timestamp_millis();
|
||||
let grace_window = 5_000; // 5 seconds in Auto/Yolo mode
|
||||
let grace_window = 5_000;
|
||||
let mut remaining = Vec::new();
|
||||
let mut to_keep = Vec::new();
|
||||
|
||||
@@ -692,8 +634,6 @@ pub fn process_pending_lessons(session_dir: &std::path::Path, memory_dir: &std::
|
||||
remaining.push(p.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// Write kept lessons to memory
|
||||
for lesson in &to_keep {
|
||||
let mem = crate::model::memory::Memory {
|
||||
name: lesson.name.clone(),
|
||||
@@ -715,8 +655,6 @@ pub fn process_pending_lessons(session_dir: &std::path::Path, memory_dir: &std::
|
||||
save_pending_lessons(session_dir, &remaining)?;
|
||||
Ok(remaining)
|
||||
}
|
||||
|
||||
/// Resolve a specific pending lesson (keep or discard).
|
||||
pub fn resolve_pending_lesson(
|
||||
session_dir: &std::path::Path,
|
||||
memory_dir: &std::path::Path,
|
||||
|
||||
Reference in New Issue
Block a user