Skip to content

Commit 2ae978a

Browse files
authored
Merge pull request #217 from syncable-dev/develop
Develop
2 parents 233fade + e727c4e commit 2ae978a

6 files changed

Lines changed: 84 additions & 28 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,6 @@ docs/phase2/
3434
syncable-ide-companion/*.vsix
3535
syncable-ide-companion/node_modules/
3636
syncable-ide-companion/dist/
37+
38+
syncable-cli.tape
39+
syncable-cli-demo.gif

src/agent/session.rs

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,37 @@ impl ChatSession {
406406
input.trim().starts_with('/')
407407
}
408408

409+
/// Strip @ prefix from file/folder references for AI consumption
410+
/// Keeps the path but removes the leading @ that was used for autocomplete
411+
/// e.g., "check @src/main.rs for issues" -> "check src/main.rs for issues"
412+
fn strip_file_references(input: &str) -> String {
413+
let mut result = String::with_capacity(input.len());
414+
let chars: Vec<char> = input.chars().collect();
415+
let mut i = 0;
416+
417+
while i < chars.len() {
418+
if chars[i] == '@' {
419+
// Check if this @ is at start or after whitespace (valid file reference trigger)
420+
let is_valid_trigger = i == 0 || chars[i - 1].is_whitespace();
421+
422+
if is_valid_trigger {
423+
// Check if there's a path after @ (not just @ followed by space/end)
424+
let has_path = i + 1 < chars.len() && !chars[i + 1].is_whitespace();
425+
426+
if has_path {
427+
// Skip the @ but keep the path
428+
i += 1;
429+
continue;
430+
}
431+
}
432+
}
433+
result.push(chars[i]);
434+
i += 1;
435+
}
436+
437+
result
438+
}
439+
409440
/// Read user input with prompt - with interactive file picker support
410441
/// Uses custom terminal handling for @ file references and / commands
411442
pub fn read_input(&self) -> io::Result<String> {
@@ -422,7 +453,9 @@ impl ChatSession {
422453
return Ok(cmd.to_string());
423454
}
424455
}
425-
Ok(trimmed.to_string())
456+
// Strip @ prefix from file references before sending to AI
457+
// The @ is for UI autocomplete, but the AI should see just the path
458+
Ok(Self::strip_file_references(trimmed))
426459
}
427460
InputResult::Cancel => Ok("exit".to_string()), // Ctrl+C exits
428461
InputResult::Exit => Ok("exit".to_string()),

src/analyzer/security/turbo/file_discovery.rs

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ impl FileDiscovery {
131131
Ok(paths)
132132
}
133133

134-
/// Get untracked files that might contain secrets
134+
/// Get untracked files that might contain secrets (including gitignored files)
135135
fn get_untracked_secret_files(&self, project_root: &Path) -> Result<Vec<PathBuf>, SecurityError> {
136136
// Common secret file patterns that might not be tracked
137137
let secret_patterns = vec![
@@ -144,26 +144,46 @@ impl FileDiscovery {
144144
"config/*.json",
145145
"config/*.yml",
146146
];
147-
147+
148148
let mut untracked_files = Vec::new();
149-
149+
150150
for pattern in secret_patterns {
151+
// First, get untracked files that are NOT gitignored (potential accidental exposure)
151152
let output = Command::new("git")
152153
.args(&["ls-files", "--others", "--exclude-standard", pattern])
153154
.current_dir(project_root)
154155
.output();
155-
156+
157+
if let Ok(output) = output {
158+
if output.status.success() {
159+
let paths: Vec<PathBuf> = String::from_utf8_lossy(&output.stdout)
160+
.lines()
161+
.filter(|line| !line.is_empty())
162+
.map(|line| project_root.join(line))
163+
.collect();
164+
untracked_files.extend(paths);
165+
}
166+
}
167+
168+
// Also get gitignored files - these should be scanned to verify they exist
169+
// and contain real secrets (important for security audit completeness)
170+
let output = Command::new("git")
171+
.args(&["ls-files", "--others", "--ignored", "--exclude-standard", pattern])
172+
.current_dir(project_root)
173+
.output();
174+
156175
if let Ok(output) = output {
157176
if output.status.success() {
158177
let paths: Vec<PathBuf> = String::from_utf8_lossy(&output.stdout)
159178
.lines()
179+
.filter(|line| !line.is_empty())
160180
.map(|line| project_root.join(line))
161181
.collect();
162182
untracked_files.extend(paths);
163183
}
164184
}
165185
}
166-
186+
167187
Ok(untracked_files)
168188
}
169189

src/analyzer/security/turbo/mod.rs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -164,12 +164,12 @@ impl TurboSecurityAnalyzer {
164164

165165
// Phase 3: Parallel scanning with work-stealing
166166
let scan_start = Instant::now();
167-
let findings = self.parallel_scan(filtered_files)?;
168-
info!("🔍 Scanned files in {:?}, found {} findings",
167+
let (findings, files_scanned) = self.parallel_scan(filtered_files)?;
168+
info!("🔍 Scanned files in {:?}, found {} findings",
169169
scan_start.elapsed(), findings.len());
170-
170+
171171
// Phase 4: Result aggregation and report generation
172-
let report = ResultAggregator::aggregate(findings, start.elapsed());
172+
let report = ResultAggregator::aggregate(findings, start.elapsed(), files_scanned);
173173

174174
info!("✅ Turbo analysis completed in {:?}", start.elapsed());
175175
Ok(report)
@@ -226,7 +226,7 @@ impl TurboSecurityAnalyzer {
226226
}
227227

228228
/// Parallel scan with work-stealing and early termination
229-
fn parallel_scan(&self, files: Vec<FileMetadata>) -> Result<Vec<SecurityFinding>, SecurityError> {
229+
fn parallel_scan(&self, files: Vec<FileMetadata>) -> Result<(Vec<SecurityFinding>, usize), SecurityError> {
230230
let thread_count = if self.config.worker_threads == 0 {
231231
num_cpus::get()
232232
} else {
@@ -333,10 +333,10 @@ impl TurboSecurityAnalyzer {
333333
handle.join().unwrap();
334334
}
335335

336-
info!("Scan complete: {} files scanned, {} skipped, {} findings",
336+
info!("Scan complete: {} files scanned, {} skipped, {} findings",
337337
files_scanned, files_skipped, all_findings.len());
338-
339-
Ok(all_findings)
338+
339+
Ok((all_findings, files_scanned))
340340
}
341341
}
342342

src/analyzer/security/turbo/results.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ pub struct ResultAggregator;
4444

4545
impl ResultAggregator {
4646
/// Aggregate findings into a comprehensive report
47-
pub fn aggregate(mut findings: Vec<SecurityFinding>, scan_duration: Duration) -> SecurityReport {
47+
pub fn aggregate(mut findings: Vec<SecurityFinding>, scan_duration: Duration, files_scanned: usize) -> SecurityReport {
4848
// Deduplicate findings
4949
findings = Self::deduplicate_findings(findings);
5050

@@ -77,7 +77,7 @@ impl ResultAggregator {
7777
overall_score,
7878
risk_level,
7979
total_findings,
80-
files_scanned: 0, // TODO: Track actual count
80+
files_scanned,
8181
findings_by_severity,
8282
findings_by_category,
8383
findings,
@@ -329,7 +329,7 @@ mod tests {
329329
},
330330
];
331331

332-
let report = ResultAggregator::aggregate(findings, Duration::from_secs(5));
332+
let report = ResultAggregator::aggregate(findings, Duration::from_secs(5), 10);
333333

334334
assert_eq!(report.total_findings, 2);
335335
assert_eq!(report.risk_level, SecuritySeverity::Critical);

src/handlers/security.rs

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ fn format_security_table(
153153
output.push_str(&format_security_findings_box(security_report, path));
154154
output.push_str(&format_gitignore_legend());
155155
} else {
156-
output.push_str(&format_no_findings_box());
156+
output.push_str(&format_no_findings_box(security_report.files_scanned));
157157
}
158158

159159
// Recommendations
@@ -176,13 +176,7 @@ fn format_security_summary_box(
176176
TurboSecuritySeverity::Info => "blue",
177177
}), true);
178178
score_box.add_line("Total Findings:", &security_report.total_findings.to_string().cyan(), true);
179-
180-
// Analysis scope
181-
let config_files = security_report.findings.iter()
182-
.filter_map(|f| f.file_path.as_ref())
183-
.collect::<std::collections::HashSet<_>>()
184-
.len();
185-
score_box.add_line("Files Analyzed:", &config_files.max(1).to_string().green(), true);
179+
score_box.add_line("Files Scanned:", &security_report.files_scanned.to_string().green(), true);
186180
score_box.add_line("Scan Mode:", &format!("{:?}", scan_mode).green(), true);
187181

188182
format!("\n{}\n", score_box.draw())
@@ -402,10 +396,16 @@ fn format_gitignore_legend() -> String {
402396
format!("\n{}\n", legend_box.draw())
403397
}
404398

405-
fn format_no_findings_box() -> String {
399+
fn format_no_findings_box(files_scanned: usize) -> String {
406400
let mut no_findings_box = BoxDrawer::new("Security Status");
407-
no_findings_box.add_value_only(&"✅ No security issues detected".green());
408-
no_findings_box.add_value_only("💡 Regular security scanning recommended");
401+
if files_scanned == 0 {
402+
no_findings_box.add_value_only(&"⚠️ No files were scanned".yellow());
403+
no_findings_box.add_value_only("This may indicate that all files were filtered out or the scan failed.");
404+
no_findings_box.add_value_only("💡 Try running with --mode thorough or --mode paranoid for a deeper scan");
405+
} else {
406+
no_findings_box.add_value_only(&"✅ No security issues detected".green());
407+
no_findings_box.add_value_only("💡 Regular security scanning recommended");
408+
}
409409
format!("\n{}\n", no_findings_box.draw())
410410
}
411411

0 commit comments

Comments
 (0)