fix(security): full-content PII scan, clippy, IPC null fix, scan size cap
All checks were successful
Test / rust-fmt-check (pull_request) Successful in 1m40s
Test / frontend-typecheck (pull_request) Successful in 1m48s
Test / frontend-tests (pull_request) Successful in 1m43s
Test / rust-clippy (pull_request) Successful in 3m17s
Test / rust-tests (pull_request) Successful in 4m33s
PR Review Automation / review (pull_request) Successful in 5m0s
All checks were successful
Test / rust-fmt-check (pull_request) Successful in 1m40s
Test / frontend-typecheck (pull_request) Successful in 1m48s
Test / frontend-tests (pull_request) Successful in 1m43s
Test / rust-clippy (pull_request) Successful in 3m17s
Test / rust-tests (pull_request) Successful in 4m33s
PR Review Automation / review (pull_request) Successful in 5m0s
Remove frontend detectPiiCmd pre-scan loop — backend is sole redaction authority; bubble update via response.user_message covers user feedback. Detect PII on full file content before truncating. Previous order (truncate to 8000 bytes then scan) could miss PII straddling the boundary. Now: read full content, scan, redact, then truncate to EMBED_LIMIT (8000 bytes) at a valid UTF-8 char boundary. logFileIds IPC: pass undefined (not null) for empty array so Tauri serialises it correctly to Rust Option::None. Add MAX_TEXT_SCAN_BYTES (32 KB) guard in scan_text_for_pii to prevent unbounded regex evaluation on oversized payloads. Fix clippy uninlined_format_args in ai.rs.
This commit is contained in:
parent
e9c576f606
commit
631221dbf1
@ -277,13 +277,15 @@ pub async fn chat_message(
|
|||||||
vec![]
|
vec![]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 8 KB embed limit; detect + redact on full content so PII at the boundary is caught.
|
||||||
|
const EMBED_LIMIT: usize = 8_000;
|
||||||
|
|
||||||
let mut msg = base;
|
let mut msg = base;
|
||||||
for (file_name, file_path) in &files {
|
for (file_name, file_path) in &files {
|
||||||
let content = std::fs::read_to_string(file_path).unwrap_or_default();
|
let content = std::fs::read_to_string(file_path).unwrap_or_default();
|
||||||
let preview = &content[..content.len().min(8000)];
|
let spans = crate::pii::PiiDetector::new().detect(&content);
|
||||||
let spans = crate::pii::PiiDetector::new().detect(preview);
|
let redacted = if spans.is_empty() {
|
||||||
let body = if spans.is_empty() {
|
content
|
||||||
preview.to_string()
|
|
||||||
} else {
|
} else {
|
||||||
let types: std::collections::HashSet<&str> =
|
let types: std::collections::HashSet<&str> =
|
||||||
spans.iter().map(|s| s.pii_type.as_str()).collect();
|
spans.iter().map(|s| s.pii_type.as_str()).collect();
|
||||||
@ -295,9 +297,22 @@ pub async fn chat_message(
|
|||||||
pii_count = spans.len(),
|
pii_count = spans.len(),
|
||||||
"PII detected in chat attachment — auto-redacting before AI send"
|
"PII detected in chat attachment — auto-redacting before AI send"
|
||||||
);
|
);
|
||||||
crate::pii::apply_redactions(preview, &spans)
|
crate::pii::apply_redactions(&content, &spans)
|
||||||
};
|
};
|
||||||
msg.push_str(&format!("\n\n--- Attached: {} ---\n{}", file_name, body));
|
// Truncate after redaction so the cut never lands inside a PII span.
|
||||||
|
let embed_end = if redacted.len() > EMBED_LIMIT {
|
||||||
|
let mut e = EMBED_LIMIT;
|
||||||
|
while !redacted.is_char_boundary(e) {
|
||||||
|
e -= 1;
|
||||||
|
}
|
||||||
|
e
|
||||||
|
} else {
|
||||||
|
redacted.len()
|
||||||
|
};
|
||||||
|
msg.push_str(&format!(
|
||||||
|
"\n\n--- Attached: {file_name} ---\n{}",
|
||||||
|
&redacted[..embed_end]
|
||||||
|
));
|
||||||
}
|
}
|
||||||
msg
|
msg
|
||||||
};
|
};
|
||||||
|
|||||||
@ -450,10 +450,19 @@ pub async fn detect_pii(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Maximum text size accepted by scan_text_for_pii to prevent DoS on large payloads.
|
||||||
|
const MAX_TEXT_SCAN_BYTES: usize = 32 * 1024; // 32 KB
|
||||||
|
|
||||||
/// Scan arbitrary text for PII without creating any database records.
|
/// Scan arbitrary text for PII without creating any database records.
|
||||||
/// Used to warn users before sending typed chat messages to AI providers.
|
/// Used by the backend before sending typed chat messages to AI providers.
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn scan_text_for_pii(text: String) -> Result<PiiDetectionResult, String> {
|
pub async fn scan_text_for_pii(text: String) -> Result<PiiDetectionResult, String> {
|
||||||
|
if text.len() > MAX_TEXT_SCAN_BYTES {
|
||||||
|
return Err(format!(
|
||||||
|
"Text too large for inline PII scan ({} bytes; limit {MAX_TEXT_SCAN_BYTES})",
|
||||||
|
text.len()
|
||||||
|
));
|
||||||
|
}
|
||||||
let detector = PiiDetector::new();
|
let detector = PiiDetector::new();
|
||||||
let spans = detector.detect(&text);
|
let spans = detector.detect(&text);
|
||||||
let total_pii_found = spans.len();
|
let total_pii_found = spans.len();
|
||||||
|
|||||||
@ -283,7 +283,7 @@ export const chatMessageCmd = (
|
|||||||
invoke<ChatResponse>("chat_message", {
|
invoke<ChatResponse>("chat_message", {
|
||||||
issueId,
|
issueId,
|
||||||
message,
|
message,
|
||||||
logFileIds: logFileIds.length > 0 ? logFileIds : null,
|
logFileIds: logFileIds.length > 0 ? logFileIds : undefined,
|
||||||
providerConfig,
|
providerConfig,
|
||||||
systemPrompt: systemPrompt ?? null,
|
systemPrompt: systemPrompt ?? null,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -8,7 +8,6 @@ import { useSessionStore } from "@/stores/sessionStore";
|
|||||||
import { useSettingsStore } from "@/stores/settingsStore";
|
import { useSettingsStore } from "@/stores/settingsStore";
|
||||||
import {
|
import {
|
||||||
chatMessageCmd,
|
chatMessageCmd,
|
||||||
detectPiiCmd,
|
|
||||||
getIssueCmd,
|
getIssueCmd,
|
||||||
getIssueMessagesCmd,
|
getIssueMessagesCmd,
|
||||||
uploadLogFileCmd,
|
uploadLogFileCmd,
|
||||||
@ -41,7 +40,6 @@ export default function Triage() {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [notice, setNotice] = useState<string | null>(null);
|
|
||||||
const [pendingFiles, setPendingFiles] = useState<PendingFile[]>([]);
|
const [pendingFiles, setPendingFiles] = useState<PendingFile[]>([]);
|
||||||
// Track the last user message so we can save it as a resolution step when why level advances
|
// Track the last user message so we can save it as a resolution step when why level advances
|
||||||
const lastUserMsgRef = useRef<string>("");
|
const lastUserMsgRef = useRef<string>("");
|
||||||
@ -135,25 +133,6 @@ export default function Triage() {
|
|||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
setNotice(null);
|
|
||||||
|
|
||||||
// Pre-send attachment PII scan: surface a notice to the user about what will be
|
|
||||||
// auto-redacted. The send is NOT blocked — the backend performs the actual redaction.
|
|
||||||
const piiNotices: string[] = [];
|
|
||||||
for (const f of pendingFiles) {
|
|
||||||
try {
|
|
||||||
const result = await detectPiiCmd(f.logFileId);
|
|
||||||
if (result.total_pii_found > 0) {
|
|
||||||
const types = [...new Set(result.detections.map((d) => d.pii_type))].join(", ");
|
|
||||||
piiNotices.push(`"${f.name}" (${types})`);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Non-fatal — backend will still scan before sending to AI
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (piiNotices.length > 0) {
|
|
||||||
setNotice(`PII auto-redacted before sending: ${piiNotices.join("; ")}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const fileNames = pendingFiles.map((f) => f.name);
|
const fileNames = pendingFiles.map((f) => f.name);
|
||||||
const displayContent =
|
const displayContent =
|
||||||
@ -257,11 +236,6 @@ export default function Triage() {
|
|||||||
<TriageProgress currentLevel={Math.min(currentWhyLevel, 5)} />
|
<TriageProgress currentLevel={Math.min(currentWhyLevel, 5)} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{notice && (
|
|
||||||
<div className="mx-6 mt-3 p-3 bg-amber-50 border border-amber-200 rounded-md text-sm text-amber-700">
|
|
||||||
ℹ️ {notice}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className="mx-6 mt-3 p-3 bg-destructive/10 border border-destructive/20 rounded-md text-sm text-destructive">
|
<div className="mx-6 mt-3 p-3 bg-destructive/10 border border-destructive/20 rounded-md text-sm text-destructive">
|
||||||
{error}
|
{error}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user