diff --git a/backend/secuscan/redaction.py b/backend/secuscan/redaction.py index 15594948..76b8c909 100644 --- a/backend/secuscan/redaction.py +++ b/backend/secuscan/redaction.py @@ -141,6 +141,14 @@ "stripe_key", re.compile(r"(sk_(?:live|test)_[A-Za-z0-9]{24,})", re.IGNORECASE), ), + # Vault references (e.g. vault:secret_name or vault://secret_name) + ( + "vault_ref", + re.compile( + r"((?:vault://|vault\s*:\s*)\s*)([A-Za-z0-9_\-\./]+)", + re.IGNORECASE, + ), + ), # Generic long hex secrets often used as tokens (≥ 32 hex chars after label) ( "hex_secret", @@ -152,6 +160,7 @@ ), ] + # ── Public API ──────────────────────────────────────────────────────────────── diff --git a/backend/secuscan/routes.py b/backend/secuscan/routes.py index f4b3289c..c9161d8c 100644 --- a/backend/secuscan/routes.py +++ b/backend/secuscan/routes.py @@ -297,6 +297,40 @@ async def get_plugin_schema(plugin_id: str): raise HTTPException(status_code=404, detail=f"Plugin not found: {plugin_id}") +@router.post("/plugin/{plugin_id}/preview") +async def get_plugin_preview(plugin_id: str, payload: Dict[str, Any] = Body(...)): + """Generate a preview of the command to be executed by a plugin, with sensitive values redacted.""" + plugin_manager = await get_plugin_manager_for_request() + plugin = plugin_manager.get_plugin(plugin_id) + if not plugin: + raise HTTPException(status_code=404, detail=f"Plugin not found: {plugin_id}") + + inputs = payload.get("inputs", {}) + + try: + # Check missing required fields + missing_fields = [] + for field in plugin.fields: + if field.required: + val = inputs.get(field.id) + if val is None or val == "": + missing_fields.append(field.label) + + if missing_fields: + raise ValueError(f"Missing required fields: {', '.join(missing_fields)}") + + command_args = plugin_manager.build_command(plugin_id, inputs) + if not command_args: + raise ValueError("Failed to build command") + + redacted_args = [redact(arg) for arg in command_args] + return { + "command": redacted_args + } + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + + @router.get("/presets", dependencies=[Depends(read_heavy_limiter)]) async def get_all_presets(): """Get all plugin presets""" diff --git a/frontend/src/api.ts b/frontend/src/api.ts index 58c6aae7..83dbb98f 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -565,6 +565,17 @@ export function getTaskDiff(taskId: string): Promise { return request(`/task/${taskId}/diff`) } +export function previewCommand( + plugin_id: string, + inputs: Record, +): Promise<{ command: string[] }> { + return request<{ command: string[] }>(`/plugin/${plugin_id}/preview`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ inputs }), + }) +} + export function startTask( plugin_id: string, inputs: Record, diff --git a/frontend/src/pages/ToolConfig.tsx b/frontend/src/pages/ToolConfig.tsx index 11c8dfb4..4491b2f7 100644 --- a/frontend/src/pages/ToolConfig.tsx +++ b/frontend/src/pages/ToolConfig.tsx @@ -16,6 +16,7 @@ import { PluginListItem, PluginSchemaResponse, startTask, + previewCommand, } from '../api' import { useToast } from '../components/ToastContext' import { routePath, routes } from '../routes' @@ -80,6 +81,8 @@ export default function ToolConfig() { evidence_level: 'standard', }) const fieldRefs = useRef>({}) + const [preview, setPreview] = useState('') + const [previewError, setPreviewError] = useState('') useEffect(() => { let cancelled = false @@ -162,6 +165,36 @@ export default function ToolConfig() { const missingBinaries = availability?.missing_binaries ?? [] const hasMissingBinaries = missingBinaries.length > 0 + useEffect(() => { + if (!toolId || !plugin || !schema) return + if (hasValidationErrors) { + setPreview('') + setPreviewError('Fix highlighted validation errors to preview command.') + return + } + + let active = true + async function updatePreview() { + try { + const response = await previewCommand(toolId!, inputs) + if (active) { + setPreview(response.command.join(' ')) + setPreviewError('') + } + } catch (error: any) { + if (active) { + setPreview('') + setPreviewError(error?.message || 'Failed to generate command preview.') + } + } + } + + updatePreview() + return () => { + active = false + } + }, [toolId, inputs, hasValidationErrors, plugin, schema]) + const handleFieldChange = (field: PluginFieldSchema, value: unknown) => { setInputs((prev) => ({ ...prev, [field.id]: value })) } @@ -468,6 +501,24 @@ export default function ToolConfig() { })} + +
+

Command_Preview

+ {previewError ? ( +

+ ⚠️ {previewError} +

+ ) : ( +
+
+ {preview} +
+

+ Note: This preview is generated locally/sanitized and does not guarantee copy-paste execution if runtime normalization changes values. +

+
+ )} +