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:
asepharyana
2026-07-11 21:25:30 +07:00
parent 2ded2d8bf1
commit f6389018f5
28 changed files with 1635 additions and 200 deletions
+3 -65
View File
@@ -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,