Add files via upload

This commit is contained in:
Joas A Santos authored and GitHub committed 2026-02-11 10:52:07 -03:00
1 parent aac5b8f365
commit e1ff8a8355
24 files changed
+9927 -78

No files matched your search

+1
View File
@@ -0,0 +1 @@
*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.container{width:100%}@media (min-width: 640px){.container{max-width:640px}}@media (min-width: 768px){.container{max-width:768px}}@media (min-width: 1024px){.container{max-width:1024px}}@media (min-width: 1280px){.container{max-width:Line truncated
+641
View File
@@ -0,0 +1,641 @@
var H0=Object.defineProperty;var W0=(e,t,n)=>t in e?H0(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n;var Hn=(e,t,n)=>W0(e,typeof t!="symbol"?t+"":t,n);function q0(e,t){for(var n=0;n<t.length;n++){const r=t[n];if(typeof r!="string"&&!Array.isArray(r)){for(const a in r)if(a!=="default"&&!(a in e)){const l=Object.getOwnPropertyDescriptor(r,a);l&&Object.defineProperty(e,a,l.get?l:{enumerable:!0,get:()=>r[a]})}}}return Object.freeze(Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}))}(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const a of document.querySelectorAll('link[rel="modulepreload"]'))r(a);new MutationObserver(a=>{for(const l of a)if(l.type==="childList")for(const i of l.addedNodes)i.tagName==="LINK"&&i.rel==="modulepreload"&&r(i)}).observe(document,{childList:!0,subtree:!0});function n(a){const l={};return a.integrity&&(l.integrity=a.integrity),a.referrerPolicy&&(l.referrerPolicy=a.referrerPolicy),a.crossOrigin==="use-credentials"?l.credentials="include":a.crossOrigin==="anonymous"?l.credentials="omit":l.credentials="same-origin",l}function r(a){if(a.ep)return;a.ep=!0;const l=n(a);fetch(a.href,l)}})();function Iu(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var Mu={exports:{}},pl={},Du={exports:{}},fe={};/**
* @license React
* react.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/var Br=Symbol.for("react.element"),K0=Symbol.for("react.portal"),Q0=Symbol.for("react.fragment"),J0=Symbol.for("react.strict_mode"),G0=Symbol.for("react.profiler"),X0=Symbol.for("react.provider"),Y0=Symbol.for("react.context"),Z0=Symbol.for("react.forward_ref"),ef=Symbol.for("react.suspense"),tf=Symbol.for("react.memo"),nf=Symbol.for("react.lazy"),Pc=Symbol.iterator;function sf(e){return e===null||typeof e!="object"?null:(e=Pc&&e[Pc]||e["@@iterator"],typeof e=="function"?e:null)}var zu={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},Fu=Object.assign,Uu={};function Bs(e,t,n){this.props=e,this.context=t,this.refs=Uu,this.updater=n||zu}Bs.prototype.isReactComponent={};Bs.prototype.setState=function(e,t){if(typeof e!="object"&&typeof e!="function"&&e!=null)throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,e,t,"setState")};Bs.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,"forceUpdate")};function Bu(){}Bu.prototype=Bs.prototype;function So(e,t,n){this.props=e,this.context=t,this.refs=Uu,this.updater=n||zu}var _o=So.prototype=new Bu;_o.constructor=So;Fu(_o,Bs.prototype);_o.isPureReactComponent=!0;var Lc=Array.isArray,Vu=Object.prototype.hasOwnProperty,Co={current:null},Hu={key:!0,ref:!0,__self:!0,__source:!0};function Wu(e,t,n){var r,a={},l=null,i=null;if(t!=null)for(r in t.ref!==void 0&&(i=t.ref),t.key!==void 0&&(l=""+t.key),t)Vu.call(t,r)&&!Hu.hasOwnProperty(r)&&(a[r]=t[r]);var o=arguments.length-2;if(o===1)a.children=n;else if(1<o){for(var c=Array(o),d=0;d<o;d++)c[d]=arguments[d+2];a.children=c}if(e&&e.defaultProps)for(r in o=e.defaultProps,o)a[r]===void 0&&(a[r]=o[r]);return{$$typeof:Br,type:e,key:l,ref:i,props:a,_owner:Co.current}}function rf(e,t){return{$$typeof:Br,type:e.type,key:t,ref:e.ref,props:e.props,_owner:e._owner}}function Eo(e){return typeof e=="object"&&e!==null&&e.$$typeof===Br}function af(e){var t={"=":"=0",":":"=2"};return"$"+e.replace(/[=:]/g,function(n){return t[n]})}var Ac=/\/+/g;function Il(e,t){return typeof e=="object"&&e!==null&&e.key!=null?af(""+e.key):t.toString(36)}function ja(e,t,n,r,a){var l=typeof e;(l==="undefined"||l==="boolean")&&(e=null);var i=!1;if(e===null)i=!0;else switch(l){case"string":case"number":i=!0;break;case"object":switch(e.$$typeof){case Br:case K0:i=!0}}if(i)return i=e,a=a(i),e=r===""?"."+Il(i,0):r,Lc(a)?(n="",e!=null&&(n=e.replace(Ac,"$&/")+"/"),ja(a,t,n,"",function(d){return d})):a!=null&&(Eo(a)&&(a=rf(a,n+(!a.key||i&&i.key===a.key?"":(""+a.key).replace(Ac,"$&/")+"/")+e)),t.push(a)),1;if(i=0,r=r===""?".":r+":",Lc(e))for(var o=0;o<e.length;o++){l=e[o];var c=r+Il(l,o);i+=ja(l,t,n,c,a)}else if(c=sf(e),typeof c=="function")for(e=c.call(e),o=0;!(l=e.next()).done;)l=l.value,c=r+Il(l,o++),i+=ja(l,t,n,c,a);else if(l==="object")throw t=String(e),Error("Objects are not valid as a React child (found: "+(t==="[object Object]"?"object with keys {"+Object.keys(e).join(", ")+"}":t)+"). If you meant to render a collection of children, use an array instead.");return i}function na(e,t,n){if(e==null)return e;var r=[],a=0;return ja(e,r,"","",function(l){return t.call(n,l,a++)}),r}function lf(e){if(e._status===-1){var t=e._result;t=t(),t.then(function(n){(e._status===0||e._status===-1)&&(e._status=1,e._result=n)},function(n){(e._status===0||e._status===-1)&&(e._status=2,e._result=n)}),e._status===-1&&(e._status=0,e._result=t)}if(e._status===1)return e._result.default;throw e._result}var at={current:null},ka={transition:null},of={ReactCurrentDispatcher:at,ReactCurrentBatchConfig:ka,ReactCurrentOwner:Co};function qu(){throw Error("act(...) is not supported in production builds of React.")}fe.Children={map:na,forEach:function(e,t,n){na(e,function(){t.apply(this,arguments)},n)},count:function(e){var t=0;return na(e,function(){t++}),t},toArray:function(e){return na(e,function(t){return t})||[]},only:function(e){if(!Eo(e))throw Error("React.Children.only expected to receive a single React element child.");return e}};fe.Component=Bs;fe.Fragment=Q0;fe.Profiler=G0;fe.PureComponent=So;fe.StrictMode=J0;fe.Suspense=ef;fe.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED=of;fe.act=qu;fe.cloneElement=function(e,t,n){if(e==null)throw Error("React.cloneElement(...): The argument must be a React element, but you passed "+e+".");var r=Fu({},e.props),a=e.key,l=e.ref,i=e._owner;if(t!=null){if(t.ref!==void 0&&(l=t.ref,i=Co.current),t.key!==void 0&&(a=""+t.key),e.type&&e.type.defaultProps)var o=e.type.defaultProps;for(c in t)Vu.call(t,c)&&!Hu.hasOwnProperty(c)&&(r[c]=t[c]===void 0&&o!==void 0?o[c]:t[c])}var c=arguments.length-2;if(c===1)r.children=n;else if(1<c){o=Array(c);for(var d=0;d<c;d++)o[d]=arguments[d+2];r.children=o}return{$$typeof:Br,type:e.type,key:a,ref:l,props:r,_owner:i}};fe.createContext=function(e){return e={$$typeof:Y0,_currentValue:e,_currentValue2:e,_threadCountLine truncated
* @license React
* react-jsx-runtime.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/var df=x,uf=Symbol.for("react.element"),pf=Symbol.for("react.fragment"),mf=Object.prototype.hasOwnProperty,ff=df.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,xf={key:!0,ref:!0,__self:!0,__source:!0};function Ku(e,t,n){var r,a={},l=null,i=null;n!==void 0&&(l=""+n),t.key!==void 0&&(l=""+t.key),t.ref!==void 0&&(i=t.ref);for(r in t)mf.call(t,r)&&!xf.hasOwnProperty(r)&&(a[r]=t[r]);if(e&&e.defaultProps)for(r in t=e.defaultProps,t)a[r]===void 0&&(a[r]=t[r]);return{$$typeof:uf,type:e,key:l,ref:i,props:a,_owner:ff.current}}pl.Fragment=pf;pl.jsx=Ku;pl.jsxs=Ku;Mu.exports=pl;var s=Mu.exports,ji={},Qu={exports:{}},wt={},Ju={exports:{}},Gu={};/**
* @license React
* scheduler.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/(function(e){function t(z,Y){var se=z.length;z.push(Y);e:for(;0<se;){var V=se-1>>>1,G=z[V];if(0<a(G,Y))z[V]=Y,z[se]=G,se=V;else break e}}function n(z){return z.length===0?null:z[0]}function r(z){if(z.length===0)return null;var Y=z[0],se=z.pop();if(se!==Y){z[0]=se;e:for(var V=0,G=z.length,ge=G>>>1;V<ge;){var he=2*(V+1)-1,ke=z[he],P=he+1,X=z[P];if(0>a(ke,se))P<G&&0>a(X,ke)?(z[V]=X,z[P]=se,V=P):(z[V]=ke,z[he]=se,V=he);else if(P<G&&0>a(X,se))z[V]=X,z[P]=se,V=P;else break e}}return Y}function a(z,Y){var se=z.sortIndex-Y.sortIndex;return se!==0?se:z.id-Y.id}if(typeof performance=="object"&&typeof performance.now=="function"){var l=performance;e.unstable_now=function(){return l.now()}}else{var i=Date,o=i.now();e.unstable_now=function(){return i.now()-o}}var c=[],d=[],m=1,f=null,p=3,N=!1,v=!1,w=!1,k=typeof setTimeout=="function"?setTimeout:null,y=typeof clearTimeout=="function"?clearTimeout:null,u=typeof setImmediate<"u"?setImmediate:null;typeof navigator<"u"&&navigator.scheduling!==void 0&&navigator.scheduling.isInputPending!==void 0&&navigator.scheduling.isInputPending.bind(navigator.scheduling);function g(z){for(var Y=n(d);Y!==null;){if(Y.callback===null)r(d);else if(Y.startTime<=z)r(d),Y.sortIndex=Y.expirationTime,t(c,Y);else break;Y=n(d)}}function _(z){if(w=!1,g(z),!v)if(n(c)!==null)v=!0,ce(L);else{var Y=n(d);Y!==null&&xe(_,Y.startTime-z)}}function L(z,Y){v=!1,w&&(w=!1,y(E),E=-1),N=!0;var se=p;try{for(g(Y),f=n(c);f!==null&&(!(f.expirationTime>Y)||z&&!A());){var V=f.callback;if(typeof V=="function"){f.callback=null,p=f.priorityLevel;var G=V(f.expirationTime<=Y);Y=e.unstable_now(),typeof G=="function"?f.callback=G:f===n(c)&&r(c),g(Y)}else r(c);f=n(c)}if(f!==null)var ge=!0;else{var he=n(d);he!==null&&xe(_,he.startTime-Y),ge=!1}return ge}finally{f=null,p=se,N=!1}}var D=!1,R=null,E=-1,U=5,q=-1;function A(){return!(e.unstable_now()-q<U)}function te(){if(R!==null){var z=e.unstable_now();q=z;var Y=!0;try{Y=R(!0,z)}finally{Y?Q():(D=!1,R=null)}}else D=!1}var Q;if(typeof u=="function")Q=function(){u(te)};else if(typeof MessageChannel<"u"){var ne=new MessageChannel,ae=ne.port2;ne.port1.onmessage=te,Q=function(){ae.postMessage(null)}}else Q=function(){k(te,0)};function ce(z){R=z,D||(D=!0,Q())}function xe(z,Y){E=k(function(){z(e.unstable_now())},Y)}e.unstable_IdlePriority=5,e.unstable_ImmediatePriority=1,e.unstable_LowPriority=4,e.unstable_NormalPriority=3,e.unstable_Profiling=null,e.unstable_UserBlockingPriority=2,e.unstable_cancelCallback=function(z){z.callback=null},e.unstable_continueExecution=function(){v||N||(v=!0,ce(L))},e.unstable_forceFrameRate=function(z){0>z||125<z?console.error("forceFrameRate takes a positive int between 0 and 125, forcing frame rates higher than 125 fps is not supported"):U=0<z?Math.floor(1e3/z):5},e.unstable_getCurrentPriorityLevel=function(){return p},e.unstable_getFirstCallbackNode=function(){return n(c)},e.unstable_next=function(z){switch(p){case 1:case 2:case 3:var Y=3;break;default:Y=p}var se=p;p=Y;try{return z()}finally{p=se}},e.unstable_pauseExecution=function(){},e.unstable_requestPaint=function(){},e.unstable_runWithPriority=function(z,Y){switch(z){case 1:case 2:case 3:case 4:case 5:break;default:z=3}var se=p;p=z;try{return Y()}finally{p=se}},e.unstable_scheduleCallback=function(z,Y,se){var V=e.unstable_now();switch(typeof se=="object"&&se!==null?(se=se.delay,se=typeof se=="number"&&0<se?V+se:V):se=V,z){case 1:var G=-1;break;case 2:G=250;break;case 5:G=1073741823;break;case 4:G=1e4;break;default:G=5e3}return G=se+G,z={id:m++,callback:Y,priorityLevel:z,startTime:se,expirationTime:G,sortIndex:-1},se>V?(z.sortIndex=se,t(d,z),n(c)===null&&z===n(d)&&(w?(y(E),E=-1):w=!0,xe(_,se-V))):(z.sortIndex=G,t(c,z),v||N||(v=!0,ce(L))),z},e.unstable_shouldYield=A,e.unstable_wrapCallback=function(z){var Y=p;return function(){var se=p;p=Y;try{return z.apply(this,arguments)}finally{p=se}}}})(Gu);Ju.exports=Gu;var hf=Ju.exports;/**
* @license React
* react-dom.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/var gf=x,vt=hf;function B(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,n=1;n<arguments.length;n++)t+="&args[]="+encodeURIComponent(arguments[n]);return"Minified React error #"+e+"; visit "+t+" for the full message or use the non-minified dev environment for full errors and additional helpful warnings."}var Xu=new Set,wr={};function ds(e,t){As(e,t),As(e+"Capture",t)}function As(e,t){for(wr[e]=t,e=0;e<t.length;e++)Xu.add(t[e])}var on=!(typeof window>"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),ki=Object.prototype.hasOwnProperty,yf=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,$c={},Oc={};function vf(e){return ki.call(Oc,e)?!0:ki.call($c,e)?!1:yf.test(e)?Oc[e]=!0:($c[e]=!0,!1)}function wf(e,t,n,r){if(n!==null&&n.type===0)return!1;switch(typeof t){case"function":case"symbol":return!0;case"boolean":return r?!1:n!==null?!n.acceptsBooleans:(e=e.toLowerCase().slice(0,5),e!=="data-"&&e!=="aria-");default:return!1}}function jf(e,t,n,r){if(t===null||typeof t>"u"||wf(e,t,n,r))return!0;if(r)return!1;if(n!==null)switch(n.type){case 3:return!t;case 4:return t===!1;case 5:return isNaN(t);case 6:return isNaN(t)||1>t}return!1}function lt(e,t,n,r,a,l,i){this.acceptsBooleans=t===2||t===3||t===4,this.attributeName=r,this.attributeNamespace=a,this.mustUseProperty=n,this.propertyName=e,this.type=t,this.sanitizeURL=l,this.removeEmptyString=i}var Qe={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(e){Qe[e]=new lt(e,0,!1,e,null,!1,!1)});[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(e){var t=e[0];Qe[t]=new lt(t,1,!1,e[1],null,!1,!1)});["contentEditable","draggable","spellCheck","value"].forEach(function(e){Qe[e]=new lt(e,2,!1,e.toLowerCase(),null,!1,!1)});["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(e){Qe[e]=new lt(e,2,!1,e,null,!1,!1)});"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(e){Qe[e]=new lt(e,3,!1,e.toLowerCase(),null,!1,!1)});["checked","multiple","muted","selected"].forEach(function(e){Qe[e]=new lt(e,3,!0,e,null,!1,!1)});["capture","download"].forEach(function(e){Qe[e]=new lt(e,4,!1,e,null,!1,!1)});["cols","rows","size","span"].forEach(function(e){Qe[e]=new lt(e,6,!1,e,null,!1,!1)});["rowSpan","start"].forEach(function(e){Qe[e]=new lt(e,5,!1,e.toLowerCase(),null,!1,!1)});var To=/[\-:]([a-z])/g;function Po(e){return e[1].toUpperCase()}"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(e){var t=e.replace(To,Po);Qe[t]=new lt(t,1,!1,e,null,!1,!1)});"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(e){var t=e.replace(To,Po);Qe[t]=new lt(t,1,!1,e,"http://www.w3.org/1999/xlink",!1,!1)});["xml:base","xml:lang","xml:space"].forEach(function(e){var t=e.replace(To,Po);Qe[t]=new lt(t,1,!1,e,"http://www.w3.org/XML/1998/namespace",!1,!1)});["tabIndex","crossOrigin"].forEach(function(e){Qe[e]=new lt(e,1,!1,e.toLowerCase(),null,!1,!1)});Qe.xlinkHref=new lt("xlinkHref",1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1);["src","href","action","formAction"].forEach(function(e){Qe[e]=new lt(e,1,!1,e.toLowerCase(),null,!0,!0)});function Lo(e,t,n,r){var a=Qe.hasOwnProperty(t)?Qe[t]:null;(a!==null?a.type!==0:r||!(2<t.length)||t[0]!=="o"&&t[0]!==" Line truncated
`+Ml+e}var Dl=!1;function zl(e,t){if(!e||Dl)return"";Dl=!0;var n=Error.prepareStackTrace;Error.prepareStackTrace=void 0;try{if(t)if(t=function(){throw Error()},Object.defineProperty(t.prototype,"props",{set:function(){throw Error()}}),typeof Reflect=="object"&&Reflect.construct){try{Reflect.construct(t,[])}catch(d){var r=d}Reflect.construct(e,[],t)}else{try{t.call()}catch(d){r=d}e.call(t.prototype)}else{try{throw Error()}catch(d){r=d}e()}}catch(d){if(d&&r&&typeof d.stack=="string"){for(var a=d.stack.split(`
`),l=r.stack.split(`
`),i=a.length-1,o=l.length-1;1<=i&&0<=o&&a[i]!==l[o];)o--;for(;1<=i&&0<=o;i--,o--)if(a[i]!==l[o]){if(i!==1||o!==1)do if(i--,o--,0>o||a[i]!==l[o]){var c=`
`+a[i].replace(" at new "," at ");return e.displayName&&c.includes("<anonymous>")&&(c=c.replace("<anonymous>",e.displayName)),c}while(1<=i&&0<=o);break}}}finally{Dl=!1,Error.prepareStackTrace=n}return(e=e?e.displayName||e.name:"")?ar(e):""}function kf(e){switch(e.tag){case 5:return ar(e.type);case 16:return ar("Lazy");case 13:return ar("Suspense");case 19:return ar("SuspenseList");case 0:case 2:case 15:return e=zl(e.type,!1),e;case 11:return e=zl(e.type.render,!1),e;case 1:return e=zl(e.type,!0),e;default:return""}}function _i(e){if(e==null)return null;if(typeof e=="function")return e.displayName||e.name||null;if(typeof e=="string")return e;switch(e){case hs:return"Fragment";case xs:return"Portal";case bi:return"Profiler";case Ao:return"StrictMode";case Ni:return"Suspense";case Si:return"SuspenseList"}if(typeof e=="object")switch(e.$$typeof){case Zu:return(e.displayName||"Context")+".Consumer";case Yu:return(e._context.displayName||"Context")+".Provider";case $o:var t=e.render;return e=e.displayName,e||(e=t.displayName||t.name||"",e=e!==""?"ForwardRef("+e+")":"ForwardRef"),e;case Oo:return t=e.displayName||null,t!==null?t:_i(e.type)||"Memo";case gn:t=e._payload,e=e._init;try{return _i(e(t))}catch{}}return null}function bf(e){var t=e.type;switch(e.tag){case 24:return"Cache";case 9:return(t.displayName||"Context")+".Consumer";case 10:return(t._context.displayName||"Context")+".Provider";case 18:return"DehydratedFragment";case 11:return e=t.render,e=e.displayName||e.name||"",t.displayName||(e!==""?"ForwardRef("+e+")":"ForwardRef");case 7:return"Fragment";case 5:return t;case 4:return"Portal";case 3:return"Root";case 6:return"Text";case 16:return _i(t);case 8:return t===Ao?"StrictMode":"Mode";case 22:return"Offscreen";case 12:return"Profiler";case 21:return"Scope";case 13:return"Suspense";case 19:return"SuspenseList";case 25:return"TracingMarker";case 1:case 0:case 17:case 2:case 14:case 15:if(typeof t=="function")return t.displayName||t.name||null;if(typeof t=="string")return t}return null}function $n(e){switch(typeof e){case"boolean":case"number":case"string":case"undefined":return e;case"object":return e;default:return""}}function tp(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()==="input"&&(t==="checkbox"||t==="radio")}function Nf(e){var t=tp(e)?"checked":"value",n=Object.getOwnPropertyDescriptor(e.constructor.prototype,t),r=""+e[t];if(!e.hasOwnProperty(t)&&typeof n<"u"&&typeof n.get=="function"&&typeof n.set=="function"){var a=n.get,l=n.set;return Object.defineProperty(e,t,{configurable:!0,get:function(){return a.call(this)},set:function(i){r=""+i,l.call(this,i)}}),Object.defineProperty(e,t,{enumerable:n.enumerable}),{getValue:function(){return r},setValue:function(i){r=""+i},stopTracking:function(){e._valueTracker=null,delete e[t]}}}}function ra(e){e._valueTracker||(e._valueTracker=Nf(e))}function np(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var n=t.getValue(),r="";return e&&(r=tp(e)?e.checked?"true":"false":e.value),e=r,e!==n?(t.setValue(e),!0):!1}function Ia(e){if(e=e||(typeof document<"u"?document:void 0),typeof e>"u")return null;try{return e.activeElement||e.body}catch{return e.body}}function Ci(e,t){var n=t.checked;return Ae({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:n??e._wrapperState.initialChecked})}function Mc(e,t){var n=t.defaultValue==null?"":t.defaultValue,r=t.checked!=null?t.checked:t.defaultChecked;n=$n(t.value!=null?t.value:n),e._wrapperState={initialChecked:r,initialValue:n,controlled:t.type==="checkbox"||t.type==="radio"?t.checked!=null:t.value!=null}}function sp(e,t){t=t.checked,t!=null&&Lo(e,"checked",t,!1)}function Ei(e,t){sp(e,t);var n=$n(t.value),r=t.type;if(n!=null)r==="number"?(n===0&&e.value===""||e.value!=n)&&(e.value=""+n):e.value!==""+n&&(e.value=""+n);else if(r==="submit"||r==="reset"){e.removeAttribute("value");return}t.hasOwnProperty("value")?Ri(e,t.type,n):t.hasOwnProperty("defaultValue")&&Ri(e,t.type,$n(t.defaultValue)),t.checked==null&&t.defaultChecked!=null&&(e.defaultChecked=!!t.defaultChecked)}function Dc(e,t,n){if(t.hasOwnProperty("value")||t.hasOwnProperty("defaultValue")){var r=t.type;if(!(r!=="submit"&&r!=="reset"||t.value!==void 0&&t.value!==null))return;t=""+e._wrapperState.initialValue,n||t===e.value||(e.value=t),e.defaultValue=t}n=e.name,n!==""&&(e.name=""),e.defaultChecked=!!e._wrapperState.initialChecked,n!==""&&(e.name=n)}function Ri(e,t,n){(t!=="number"||Ia(e.ownerDocument)!==e)&&(n==null?e.defaultValue=""+e._wrapperState.initialValue:e.defaultValue!==""+n&&(e.defaultValue=""+n))}var lr=Array.isArray;function Cs(e,t,n,r){if(e=e.options,t){t={};for(var a=0;a<n.length;a++)t["$"+n[a]]=!0;for(n=0;n<e.length;n++)a=t.hasOwnProperty("$"+e[n].value),e[n].selected!==a&&(e[n].selected=a),a&&r&&(e[n].defaultSelected=!0)}else{for(n=""+$n(n),t=null,a=0;a<e.length;a++){if(e[a].value===n){e[a].selected=!0,r&&(e[a].defaultSelected=!0);return}t!==null||e[a].disabled||(t=e[a])}t!==null&&(t.selected=!0)}}function Ti(e,t){if(t.dan Line truncated
`).replace($x,"")}function pa(e,t,n){if(t=id(t),id(e)!==t&&n)throw Error(B(425))}function Ha(){}var Vi=null,Hi=null;function Wi(e,t){return e==="textarea"||e==="noscript"||typeof t.children=="string"||typeof t.children=="number"||typeof t.dangerouslySetInnerHTML=="object"&&t.dangerouslySetInnerHTML!==null&&t.dangerouslySetInnerHTML.__html!=null}var qi=typeof setTimeout=="function"?setTimeout:void 0,Ox=typeof clearTimeout=="function"?clearTimeout:void 0,od=typeof Promise=="function"?Promise:void 0,Ix=typeof queueMicrotask=="function"?queueMicrotask:typeof od<"u"?function(e){return od.resolve(null).then(e).catch(Mx)}:qi;function Mx(e){setTimeout(function(){throw e})}function Yl(e,t){var n=t,r=0;do{var a=n.nextSibling;if(e.removeChild(n),a&&a.nodeType===8)if(n=a.data,n==="/$"){if(r===0){e.removeChild(a),Sr(t);return}r--}else n!=="$"&&n!=="$?"&&n!=="$!"||r++;n=a}while(n);Sr(t)}function Cn(e){for(;e!=null;e=e.nextSibling){var t=e.nodeType;if(t===1||t===3)break;if(t===8){if(t=e.data,t==="$"||t==="$!"||t==="$?")break;if(t==="/$")return null}}return e}function cd(e){e=e.previousSibling;for(var t=0;e;){if(e.nodeType===8){var n=e.data;if(n==="$"||n==="$!"||n==="$?"){if(t===0)return e;t--}else n==="/$"&&t++}e=e.previousSibling}return null}var Hs=Math.random().toString(36).slice(2),Ht="__reactFiber$"+Hs,Tr="__reactProps$"+Hs,cn="__reactContainer$"+Hs,Ki="__reactEvents$"+Hs,Dx="__reactListeners$"+Hs,zx="__reactHandles$"+Hs;function Kn(e){var t=e[Ht];if(t)return t;for(var n=e.parentNode;n;){if(t=n[cn]||n[Ht]){if(n=t.alternate,t.child!==null||n!==null&&n.child!==null)for(e=cd(e);e!==null;){if(n=e[Ht])return n;e=cd(e)}return t}e=n,n=e.parentNode}return null}function Wr(e){return e=e[Ht]||e[cn],!e||e.tag!==5&&e.tag!==6&&e.tag!==13&&e.tag!==3?null:e}function ws(e){if(e.tag===5||e.tag===6)return e.stateNode;throw Error(B(33))}function gl(e){return e[Tr]||null}var Qi=[],js=-1;function zn(e){return{current:e}}function Ce(e){0>js||(e.current=Qi[js],Qi[js]=null,js--)}function Ne(e,t){js++,Qi[js]=e.current,e.current=t}var On={},et=zn(On),ct=zn(!1),ss=On;function $s(e,t){var n=e.type.contextTypes;if(!n)return On;var r=e.stateNode;if(r&&r.__reactInternalMemoizedUnmaskedChildContext===t)return r.__reactInternalMemoizedMaskedChildContext;var a={},l;for(l in n)a[l]=t[l];return r&&(e=e.stateNode,e.__reactInternalMemoizedUnmaskedChildContext=t,e.__reactInternalMemoizedMaskedChildContext=a),a}function dt(e){return e=e.childContextTypes,e!=null}function Wa(){Ce(ct),Ce(et)}function dd(e,t,n){if(et.current!==On)throw Error(B(168));Ne(et,t),Ne(ct,n)}function Wp(e,t,n){var r=e.stateNode;if(t=t.childContextTypes,typeof r.getChildContext!="function")return n;r=r.getChildContext();for(var a in r)if(!(a in t))throw Error(B(108,bf(e)||"Unknown",a));return Ae({},n,r)}function qa(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||On,ss=et.current,Ne(et,e),Ne(ct,ct.current),!0}function ud(e,t,n){var r=e.stateNode;if(!r)throw Error(B(169));n?(e=Wp(e,t,ss),r.__reactInternalMemoizedMergedChildContext=e,Ce(ct),Ce(et),Ne(et,e)):Ce(ct),Ne(ct,n)}var tn=null,yl=!1,Zl=!1;function qp(e){tn===null?tn=[e]:tn.push(e)}function Fx(e){yl=!0,qp(e)}function Fn(){if(!Zl&&tn!==null){Zl=!0;var e=0,t=be;try{var n=tn;for(be=1;e<n.length;e++){var r=n[e];do r=r(!0);while(r!==null)}tn=null,yl=!1}catch(a){throw tn!==null&&(tn=tn.slice(e+1)),gp(Mo,Fn),a}finally{be=t,Zl=!1}}return null}var ks=[],bs=0,Ka=null,Qa=0,Nt=[],St=0,rs=null,sn=1,rn="";function Wn(e,t){ks[bs++]=Qa,ks[bs++]=Ka,Ka=e,Qa=t}function Kp(e,t,n){Nt[St++]=sn,Nt[St++]=rn,Nt[St++]=rs,rs=e;var r=sn;e=rn;var a=32-Dt(r)-1;r&=~(1<<a),n+=1;var l=32-Dt(t)+a;if(30<l){var i=a-a%5;l=(r&(1<<i)-1).toString(32),r>>=i,a-=i,sn=1<<32-Dt(t)+a|n<<a|r,rn=l+e}else sn=1<<l|n<<a|r,rn=e}function qo(e){e.return!==null&&(Wn(e,1),Kp(e,1,0))}function Ko(e){for(;e===Ka;)Ka=ks[--bs],ks[bs]=null,Qa=ks[--bs],ks[bs]=null;for(;e===rs;)rs=Nt[--St],Nt[St]=null,rn=Nt[--St],Nt[St]=null,sn=Nt[--St],Nt[St]=null}var gt=null,ht=null,Re=!1,It=null;function Qp(e,t){var n=_t(5,null,null,0);n.elementType="DELETED",n.stateNode=t,n.return=e,t=e.deletions,t===null?(e.deletions=[n],e.flags|=16):t.push(n)}function pd(e,t){switch(e.tag){case 5:var n=e.type;return t=t.nodeType!==1||n.toLowerCase()!==t.nodeName.toLowerCase()?null:t,t!==null?(e.stateNode=t,gt=e,ht=Cn(t.firstChild),!0):!1;case 6:return t=e.pendingProps===""||t.nodeType!==3?null:t,t!==null?(e.stateNode=t,gt=e,ht=null,!0):!1;case 13:return t=t.nodeType!==8?null:t,t!==null?(n=rs!==null?{id:sn,overflow:rn}:null,e.memoizedState={dehydrated:t,treeContext:n,retryLane:1073741824},n=_t(18,null,null,0),n.stateNode=t,n.return=e,e.child=n,gt=e,ht=null,!0):!1;default:return!1}}function Ji(e){return(e.mode&1)!==0&&(e.flags&128)===0}function Gi(e){if(Re){var t=ht;if(t){var n=t;if(!pd(e,t)){if(Ji(e))throw Error(B(418));t=Cn(n.nextSibling);var r=gt;t&&pd(e,t)?Qp(r,n):(e.flags=e.flags&-4097|2,Re=!1,gt=e)}}else{if(Ji(e))throw Error(B(418));e.flags=e.flags&-4097|2,Re=!1,gt=e}}}function md(e){for(e=e Line truncated
Error generating stack: `+l.message+`
`+l.stack}return{value:e,source:t,stack:a,digest:null}}function ri(e,t,n){return{value:e,source:null,stack:n??null,digest:t??null}}function eo(e,t){try{console.error(t.value)}catch(n){setTimeout(function(){throw n})}}var Jx=typeof WeakMap=="function"?WeakMap:Map;function jm(e,t,n){n=an(-1,n),n.tag=3,n.payload={element:null};var r=t.value;return n.callback=function(){nl||(nl=!0,uo=r),eo(e,t)},n}function km(e,t,n){n=an(-1,n),n.tag=3;var r=e.type.getDerivedStateFromError;if(typeof r=="function"){var a=t.value;n.payload=function(){return r(a)},n.callback=function(){eo(e,t)}}var l=e.stateNode;return l!==null&&typeof l.componentDidCatch=="function"&&(n.callback=function(){eo(e,t),typeof r!="function"&&(Rn===null?Rn=new Set([this]):Rn.add(this));var i=t.stack;this.componentDidCatch(t.value,{componentStack:i!==null?i:""})}),n}function jd(e,t,n){var r=e.pingCache;if(r===null){r=e.pingCache=new Jx;var a=new Set;r.set(t,a)}else a=r.get(t),a===void 0&&(a=new Set,r.set(t,a));a.has(n)||(a.add(n),e=ch.bind(null,e,t,n),t.then(e,e))}function kd(e){do{var t;if((t=e.tag===13)&&(t=e.memoizedState,t=t!==null?t.dehydrated!==null:!0),t)return e;e=e.return}while(e!==null);return null}function bd(e,t,n,r,a){return e.mode&1?(e.flags|=65536,e.lanes=a,e):(e===t?e.flags|=65536:(e.flags|=128,n.flags|=131072,n.flags&=-52805,n.tag===1&&(n.alternate===null?n.tag=17:(t=an(-1,1),t.tag=2,En(n,t,1))),n.lanes|=1),e)}var Gx=mn.ReactCurrentOwner,ot=!1;function nt(e,t,n,r){t.child=e===null?Xp(t,null,n,r):Is(t,e.child,n,r)}function Nd(e,t,n,r,a){n=n.render;var l=t.ref;return Ps(t,a),r=rc(e,t,n,r,l,a),n=ac(),e!==null&&!ot?(t.updateQueue=e.updateQueue,t.flags&=-2053,e.lanes&=~a,un(e,t,a)):(Re&&n&&qo(t),t.flags|=1,nt(e,t,r,a),t.child)}function Sd(e,t,n,r,a){if(e===null){var l=n.type;return typeof l=="function"&&!xc(l)&&l.defaultProps===void 0&&n.compare===null&&n.defaultProps===void 0?(t.tag=15,t.type=l,bm(e,t,l,r,a)):(e=La(n.type,null,r,t,t.mode,a),e.ref=t.ref,e.return=t,t.child=e)}if(l=e.child,!(e.lanes&a)){var i=l.memoizedProps;if(n=n.compare,n=n!==null?n:Cr,n(i,r)&&e.ref===t.ref)return un(e,t,a)}return t.flags|=1,e=Pn(l,r),e.ref=t.ref,e.return=t,t.child=e}function bm(e,t,n,r,a){if(e!==null){var l=e.memoizedProps;if(Cr(l,r)&&e.ref===t.ref)if(ot=!1,t.pendingProps=r=l,(e.lanes&a)!==0)e.flags&131072&&(ot=!0);else return t.lanes=e.lanes,un(e,t,a)}return to(e,t,n,r,a)}function Nm(e,t,n){var r=t.pendingProps,a=r.children,l=e!==null?e.memoizedState:null;if(r.mode==="hidden")if(!(t.mode&1))t.memoizedState={baseLanes:0,cachePool:null,transitions:null},Ne(_s,xt),xt|=n;else{if(!(n&1073741824))return e=l!==null?l.baseLanes|n:n,t.lanes=t.childLanes=1073741824,t.memoizedState={baseLanes:e,cachePool:null,transitions:null},t.updateQueue=null,Ne(_s,xt),xt|=e,null;t.memoizedState={baseLanes:0,cachePool:null,transitions:null},r=l!==null?l.baseLanes:n,Ne(_s,xt),xt|=r}else l!==null?(r=l.baseLanes|n,t.memoizedState=null):r=n,Ne(_s,xt),xt|=r;return nt(e,t,a,n),t.child}function Sm(e,t){var n=t.ref;(e===null&&n!==null||e!==null&&e.ref!==n)&&(t.flags|=512,t.flags|=2097152)}function to(e,t,n,r,a){var l=dt(n)?ss:et.current;return l=$s(t,l),Ps(t,a),n=rc(e,t,n,r,l,a),r=ac(),e!==null&&!ot?(t.updateQueue=e.updateQueue,t.flags&=-2053,e.lanes&=~a,un(e,t,a)):(Re&&r&&qo(t),t.flags|=1,nt(e,t,n,a),t.child)}function _d(e,t,n,r,a){if(dt(n)){var l=!0;qa(t)}else l=!1;if(Ps(t,a),t.stateNode===null)Ra(e,t),wm(t,n,r),Zi(t,n,r,a),r=!0;else if(e===null){var i=t.stateNode,o=t.memoizedProps;i.props=o;var c=i.context,d=n.contextType;typeof d=="object"&&d!==null?d=Rt(d):(d=dt(n)?ss:et.current,d=$s(t,d));var m=n.getDerivedStateFromProps,f=typeof m=="function"||typeof i.getSnapshotBeforeUpdate=="function";f||typeof i.UNSAFE_componentWillReceiveProps!="function"&&typeof i.componentWillReceiveProps!="function"||(o!==r||c!==d)&&wd(t,i,r,d),yn=!1;var p=t.memoizedState;i.state=p,Xa(t,r,i,a),c=t.memoizedState,o!==r||p!==c||ct.current||yn?(typeof m=="function"&&(Yi(t,n,m,r),c=t.memoizedState),(o=yn||vd(t,n,o,r,p,c,d))?(f||typeof i.UNSAFE_componentWillMount!="function"&&typeof i.componentWillMount!="function"||(typeof i.componentWillMount=="function"&&i.componentWillMount(),typeof i.UNSAFE_componentWillMount=="function"&&i.UNSAFE_componentWillMount()),typeof i.componentDidMount=="function"&&(t.flags|=4194308)):(typeof i.componentDidMount=="function"&&(t.flags|=4194308),t.memoizedProps=r,t.memoizedState=c),i.props=r,i.state=c,i.context=d,r=o):(typeof i.componentDidMount=="function"&&(t.flags|=4194308),r=!1)}else{i=t.stateNode,Zp(e,t),o=t.memoizedProps,d=t.type===t.elementType?o:$t(t.type,o),i.props=d,f=t.pendingProps,p=i.context,c=n.contextType,typeof c=="object"&&c!==null?c=Rt(c):(c=dt(n)?ss:et.current,c=$s(t,c));var N=n.getDerivedStateFromProps;(m=typeof N=="function"||typeof i.getSnapshotBeforeUpdate=="function")||typeof i.UNSAFE_componentWillReceiveProps!="function"&&typeof i.componentWillReceiveProps!="function"||(o!==f||p!==c)&&wd(t,i,r,c),yn=!1,p=t.memoizedState,i.state=p,Xa(t,r,i,a);var v=t.memoizedS Line truncated
* @remix-run/router v1.23.2
*
* Copyright (c) Remix Software Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE.md file in the root directory of this source tree.
*
* @license MIT
*/function Mr(){return Mr=Object.assign?Object.assign.bind():function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(e[r]=n[r])}return e},Mr.apply(this,arguments)}var bn;(function(e){e.Pop="POP",e.Push="PUSH",e.Replace="REPLACE"})(bn||(bn={}));const Fd="popstate";function jh(e){e===void 0&&(e={});function t(r,a){let{pathname:l,search:i,hash:o}=r.location;return xo("",{pathname:l,search:i,hash:o},a.state&&a.state.usr||null,a.state&&a.state.key||"default")}function n(r,a){return typeof a=="string"?a:ll(a)}return bh(t,n,null,e)}function ze(e,t){if(e===!1||e===null||typeof e>"u")throw new Error(t)}function wc(e,t){if(!e){typeof console<"u"&&console.warn(t);try{throw new Error(t)}catch{}}}function kh(){return Math.random().toString(36).substr(2,8)}function Ud(e,t){return{usr:e.state,key:e.key,idx:t}}function xo(e,t,n,r){return n===void 0&&(n=null),Mr({pathname:typeof e=="string"?e:e.pathname,search:"",hash:""},typeof t=="string"?Ws(t):t,{state:n,key:t&&t.key||r||kh()})}function ll(e){let{pathname:t="/",search:n="",hash:r=""}=e;return n&&n!=="?"&&(t+=n.charAt(0)==="?"?n:"?"+n),r&&r!=="#"&&(t+=r.charAt(0)==="#"?r:"#"+r),t}function Ws(e){let t={};if(e){let n=e.indexOf("#");n>=0&&(t.hash=e.substr(n),e=e.substr(0,n));let r=e.indexOf("?");r>=0&&(t.search=e.substr(r),e=e.substr(0,r)),e&&(t.pathname=e)}return t}function bh(e,t,n,r){r===void 0&&(r={});let{window:a=document.defaultView,v5Compat:l=!1}=r,i=a.history,o=bn.Pop,c=null,d=m();d==null&&(d=0,i.replaceState(Mr({},i.state,{idx:d}),""));function m(){return(i.state||{idx:null}).idx}function f(){o=bn.Pop;let k=m(),y=k==null?null:k-d;d=k,c&&c({action:o,location:w.location,delta:y})}function p(k,y){o=bn.Push;let u=xo(w.location,k,y);d=m()+1;let g=Ud(u,d),_=w.createHref(u);try{i.pushState(g,"",_)}catch(L){if(L instanceof DOMException&&L.name==="DataCloneError")throw L;a.location.assign(_)}l&&c&&c({action:o,location:w.location,delta:1})}function N(k,y){o=bn.Replace;let u=xo(w.location,k,y);d=m();let g=Ud(u,d),_=w.createHref(u);i.replaceState(g,"",_),l&&c&&c({action:o,location:w.location,delta:0})}function v(k){let y=a.location.origin!=="null"?a.location.origin:a.location.href,u=typeof k=="string"?k:ll(k);return u=u.replace(/ $/,"%20"),ze(y,"No window.location.(origin|href) available to create URL for href: "+u),new URL(u,y)}let w={get action(){return o},get location(){return e(a,i)},listen(k){if(c)throw new Error("A history only accepts one active listener");return a.addEventListener(Fd,f),c=k,()=>{a.removeEventListener(Fd,f),c=null}},createHref(k){return t(a,k)},createURL:v,encodeLocation(k){let y=v(k);return{pathname:y.pathname,search:y.search,hash:y.hash}},push:p,replace:N,go(k){return i.go(k)}};return w}var Bd;(function(e){e.data="data",e.deferred="deferred",e.redirect="redirect",e.error="error"})(Bd||(Bd={}));function Nh(e,t,n){return n===void 0&&(n="/"),Sh(e,t,n)}function Sh(e,t,n,r){let a=typeof t=="string"?Ws(t):t,l=jc(a.pathname||"/",n);if(l==null)return null;let i=Jm(e);_h(i);let o=null;for(let c=0;o==null&&c<i.length;++c){let d=Dh(l);o=Oh(i[c],d)}return o}function Jm(e,t,n,r){t===void 0&&(t=[]),n===void 0&&(n=[]),r===void 0&&(r="");let a=(l,i,o)=>{let c={relativePath:o===void 0?l.path||"":o,caseSensitive:l.caseSensitive===!0,childrenIndex:i,route:l};c.relativePath.startsWith("/")&&(ze(c.relativePath.startsWith(r),'Absolute route path "'+c.relativePath+'" nested under path '+('"'+r+'" is not valid. An absolute child route path ')+"must start with the combined path of all its parent routes."),c.relativePath=c.relativePath.slice(r.length));let d=Ln([r,c.relativePath]),m=n.concat(c);l.children&&l.children.length>0&&(ze(l.index!==!0,"Index routes must not have child routes. Please remove "+('all child routes from route path "'+d+'".')),Jm(l.children,t,m,d)),!(l.path==null&&!l.index)&&t.push({path:d,score:Ah(d,l.index),routesMeta:m})};return e.forEach((l,i)=>{var o;if(l.path===""||!((o=l.path)!=null&&o.includes("?")))a(l,i);else for(let c of Gm(l.path))a(l,i,c)}),t}function Gm(e){let t=e.split("/");if(t.length===0)return[];let[n,...r]=t,a=n.endsWith("?"),l=n.replace(/\?$/,"");if(r.length===0)return a?[l,""]:[l];let i=Gm(r.join("/")),o=[];return o.push(...i.map(c=>c===""?l:[l,c].join("/"))),a&&o.push(...i),o.map(c=>e.startsWith("/")&&c===""?"/":c)}function _h(e){e.sort((t,n)=>t.score!==n.score?n.score-t.score:$h(t.routesMeta.map(r=>r.childrenIndex),n.routesMeta.map(r=>r.childrenIndex)))}const Ch=/^:[\w-]+$/,Eh=3,Rh=2,Th=1,Ph=10,Lh=-2,Vd=e=>e==="*";function Ah(e,t){let n=e.split("/"),r=n.length;return n.some(Vd)&&(r+=Lh),t&&(r+=Rh),n.filter(a=>!Vd(a)).reduce((a,l)=>a+(Ch.test(l)?Eh:l===""?Th:Ph),r)}function $h(e,t){return e.length===t.length&&e.slice(0,-1).every((r,a)=>r===t[a])?e[e.length-1]-t[t.length-1]:0}function Oh(e,t,n){let{routesMeta:r}=e,a={},l="/",i=[];for(let o=0;o<r.length;++o){let c=r[o],d=o===r.length-1,m=l==="/"?t:t.slice(l.length)||"/",f=Ih({path:c.relativePath,caseSensitive:c.caseSeLine truncated
* React Router v6.30.3
*
* Copyright (c) Remix Software Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE.md file in the root directory of this source tree.
*
* @license MIT
*/function Dr(){return Dr=Object.assign?Object.assign.bind():function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(e[r]=n[r])}return e},Dr.apply(this,arguments)}const kc=x.createContext(null),Qh=x.createContext(null),ps=x.createContext(null),El=x.createContext(null),Un=x.createContext({outlet:null,matches:[],isDataRoute:!1}),e0=x.createContext(null);function Jh(e,t){let{relative:n}=t===void 0?{}:t;Kr()||ze(!1);let{basename:r,navigator:a}=x.useContext(ps),{hash:l,pathname:i,search:o}=n0(e,{relative:n}),c=i;return r!=="/"&&(c=i==="/"?r:Ln([r,i])),a.createHref({pathname:c,search:o,hash:l})}function Kr(){return x.useContext(El)!=null}function qs(){return Kr()||ze(!1),x.useContext(El).location}function t0(e){x.useContext(ps).static||x.useLayoutEffect(e)}function Bn(){let{isDataRoute:e}=x.useContext(Un);return e?og():Gh()}function Gh(){Kr()||ze(!1);let e=x.useContext(kc),{basename:t,future:n,navigator:r}=x.useContext(ps),{matches:a}=x.useContext(Un),{pathname:l}=qs(),i=JSON.stringify(Xm(a,n.v7_relativeSplatPath)),o=x.useRef(!1);return t0(()=>{o.current=!0}),x.useCallback(function(d,m){if(m===void 0&&(m={}),!o.current)return;if(typeof d=="number"){r.go(d);return}let f=Ym(d,JSON.parse(i),l,m.relative==="path");e==null&&t!=="/"&&(f.pathname=f.pathname==="/"?t:Ln([t,f.pathname])),(m.replace?r.replace:r.push)(f,m.state,m)},[t,r,i,l,e])}function bc(){let{matches:e}=x.useContext(Un),t=e[e.length-1];return t?t.params:{}}function n0(e,t){let{relative:n}=t===void 0?{}:t,{future:r}=x.useContext(ps),{matches:a}=x.useContext(Un),{pathname:l}=qs(),i=JSON.stringify(Xm(a,r.v7_relativeSplatPath));return x.useMemo(()=>Ym(e,JSON.parse(i),l,n==="path"),[e,i,l,n])}function Xh(e,t){return Yh(e,t)}function Yh(e,t,n,r){Kr()||ze(!1);let{navigator:a}=x.useContext(ps),{matches:l}=x.useContext(Un),i=l[l.length-1],o=i?i.params:{};i&&i.pathname;let c=i?i.pathnameBase:"/";i&&i.route;let d=qs(),m;if(t){var f;let k=typeof t=="string"?Ws(t):t;c==="/"||(f=k.pathname)!=null&&f.startsWith(c)||ze(!1),m=k}else m=d;let p=m.pathname||"/",N=p;if(c!=="/"){let k=c.replace(/^\//,"").split("/");N="/"+p.replace(/^\//,"").split("/").slice(k.length).join("/")}let v=Nh(e,{pathname:N}),w=sg(v&&v.map(k=>Object.assign({},k,{params:Object.assign({},o,k.params),pathname:Ln([c,a.encodeLocation?a.encodeLocation(k.pathname).pathname:k.pathname]),pathnameBase:k.pathnameBase==="/"?c:Ln([c,a.encodeLocation?a.encodeLocation(k.pathnameBase).pathname:k.pathnameBase])})),l,n,r);return t&&w?x.createElement(El.Provider,{value:{location:Dr({pathname:"/",search:"",hash:"",state:null,key:"default"},m),navigationType:bn.Pop}},w):w}function Zh(){let e=ig(),t=qh(e)?e.status+" "+e.statusText:e instanceof Error?e.message:JSON.stringify(e),n=e instanceof Error?e.stack:null,a={padding:"0.5rem",backgroundColor:"rgba(200,200,200, 0.5)"};return x.createElement(x.Fragment,null,x.createElement("h2",null,"Unexpected Application Error!"),x.createElement("h3",{style:{fontStyle:"italic"}},t),n?x.createElement("pre",{style:a},n):null,null)}const eg=x.createElement(Zh,null);class tg extends x.Component{constructor(t){super(t),this.state={location:t.location,revalidation:t.revalidation,error:t.error}}static getDerivedStateFromError(t){return{error:t}}static getDerivedStateFromProps(t,n){return n.location!==t.location||n.revalidation!=="idle"&&t.revalidation==="idle"?{error:t.error,location:t.location,revalidation:t.revalidation}:{error:t.error!==void 0?t.error:n.error,location:n.location,revalidation:t.revalidation||n.revalidation}}componentDidCatch(t,n){console.error("React Router caught the following error during render",t,n)}render(){return this.state.error!==void 0?x.createElement(Un.Provider,{value:this.props.routeContext},x.createElement(e0.Provider,{value:this.state.error,children:this.props.component})):this.props.children}}function ng(e){let{routeContext:t,match:n,children:r}=e,a=x.useContext(kc);return a&&a.static&&a.staticContext&&(n.route.errorElement||n.route.ErrorBoundary)&&(a.staticContext._deepestRenderedBoundaryId=n.route.id),x.createElement(Un.Provider,{value:t},r)}function sg(e,t,n,r){var a;if(t===void 0&&(t=[]),n===void 0&&(n=null),r===void 0&&(r=null),e==null){var l;if(!n)return null;if(n.errors)e=n.matches;else if((l=r)!=null&&l.v7_partialHydration&&t.length===0&&!n.initialized&&n.matches.length>0)e=n.matches;else return null}let i=e,o=(a=n)==null?void 0:a.errors;if(o!=null){let m=i.findIndex(f=>f.route.id&&(o==null?void 0:o[f.route.id])!==void 0);m>=0||ze(!1),i=i.slice(0,Math.min(i.length,m+1))}let c=!1,d=-1;if(n&&r&&r.v7_partialHydration)for(let m=0;m<i.length;m++){let f=i[m];if((f.route.HydrateFallback||f.route.hydrateFallbackElement)&&(d=m),f.route.id){let{loaderData:p,errors:N}=n,v=f.route.loader&&p[f.route.id]===void 0&&(!N||N[f.route.id]===void 0);if(f.route.lazy||v){c=!0,d>=0?i=i.slice(0,d+1):i=[i[0]];break}}}return i.reduceRight((m,f,p)=>{let N,v=!1,w=null,k=null;n&&(N=o&&f.route.id?o[f.route.id]:Line truncated
* React Router DOM v6.30.3
*
* Copyright (c) Remix Software Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE.md file in the root directory of this source tree.
*
* @license MIT
*/function go(){return go=Object.assign?Object.assign.bind():function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(e[r]=n[r])}return e},go.apply(this,arguments)}function mg(e,t){if(e==null)return{};var n={},r=Object.keys(e),a,l;for(l=0;l<r.length;l++)a=r[l],!(t.indexOf(a)>=0)&&(n[a]=e[a]);return n}function fg(e){return!!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)}function xg(e,t){return e.button===0&&(!t||t==="_self")&&!fg(e)}const hg=["onClick","relative","reloadDocument","replace","state","target","to","preventScrollReset","viewTransition"],gg="6";try{window.__reactRouterVersion=gg}catch{}const yg="startTransition",qd=cf[yg];function vg(e){let{basename:t,children:n,future:r,window:a}=e,l=x.useRef();l.current==null&&(l.current=jh({window:a,v5Compat:!0}));let i=l.current,[o,c]=x.useState({action:i.action,location:i.location}),{v7_startTransition:d}=r||{},m=x.useCallback(f=>{d&&qd?qd(()=>c(f)):c(f)},[c,d]);return x.useLayoutEffect(()=>i.listen(m),[i,m]),x.useEffect(()=>dg(r),[r]),x.createElement(ug,{basename:t,children:n,location:o.location,navigationType:o.action,navigator:i,future:r})}const wg=typeof window<"u"&&typeof window.document<"u"&&typeof window.document.createElement<"u",jg=/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i,nn=x.forwardRef(function(t,n){let{onClick:r,relative:a,reloadDocument:l,replace:i,state:o,target:c,to:d,preventScrollReset:m,viewTransition:f}=t,p=mg(t,hg),{basename:N}=x.useContext(ps),v,w=!1;if(typeof d=="string"&&jg.test(d)&&(v=d,wg))try{let g=new URL(window.location.href),_=d.startsWith("//")?new URL(g.protocol+d):new URL(d),L=jc(_.pathname,N);_.origin===g.origin&&L!=null?d=L+_.search+_.hash:w=!0}catch{}let k=Jh(d,{relative:a}),y=kg(d,{replace:i,state:o,target:c,preventScrollReset:m,relative:a,viewTransition:f});function u(g){r&&r(g),g.defaultPrevented||y(g)}return x.createElement("a",go({},p,{href:v||k,onClick:w||l?r:u,ref:n,target:c}))});var Kd;(function(e){e.UseScrollRestoration="useScrollRestoration",e.UseSubmit="useSubmit",e.UseSubmitFetcher="useSubmitFetcher",e.UseFetcher="useFetcher",e.useViewTransitionState="useViewTransitionState"})(Kd||(Kd={}));var Qd;(function(e){e.UseFetcher="useFetcher",e.UseFetchers="useFetchers",e.UseScrollRestoration="useScrollRestoration"})(Qd||(Qd={}));function kg(e,t){let{target:n,replace:r,state:a,preventScrollReset:l,relative:i,viewTransition:o}=t===void 0?{}:t,c=Bn(),d=qs(),m=n0(e,{relative:i});return x.useCallback(f=>{if(xg(f,n)){f.preventDefault();let p=r!==void 0?r:ll(d)===ll(m);c(e,{replace:p,state:a,preventScrollReset:l,relative:i,viewTransition:o})}},[d,c,m,r,a,n,e,l,i,o])}/**
* @license lucide-react v0.303.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/var bg={xmlns:"http://www.w3.org/2000/svg",width:24,height:24,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round"};/**
* @license lucide-react v0.303.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const Ng=e=>e.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase().trim(),Z=(e,t)=>{const n=x.forwardRef(({color:r="currentColor",size:a=24,strokeWidth:l=2,absoluteStrokeWidth:i,className:o="",children:c,...d},m)=>x.createElement("svg",{ref:m,...bg,width:a,height:a,stroke:r,strokeWidth:i?Number(l)*24/Number(a):l,className:["lucide",`lucide-${Ng(e)}`,o].join(" "),...d},[...t.map(([f,p])=>x.createElement(f,p)),...Array.isArray(c)?c:[c]]));return n.displayName=`${e}`,n};/**
* @license lucide-react v0.303.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const l0=Z("Activity",[["path",{d:"M22 12h-4l-3 9L9 3l-3 9H2",key:"d5dnw9"}]]);/**
* @license lucide-react v0.303.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const Jd=Z("AlertCircle",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["line",{x1:"12",x2:"12",y1:"8",y2:"12",key:"1pkeuh"}],["line",{x1:"12",x2:"12.01",y1:"16",y2:"16",key:"4dfq90"}]]);/**
* @license lucide-react v0.303.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const Oe=Z("AlertTriangle",[["path",{d:"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z",key:"c3ski4"}],["path",{d:"M12 9v4",key:"juzpu7"}],["path",{d:"M12 17h.01",key:"p32p05"}]]);/**
* @license lucide-react v0.303.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const Sg=Z("ArrowLeft",[["path",{d:"m12 19-7-7 7-7",key:"1l729n"}],["path",{d:"M19 12H5",key:"x3x0zl"}]]);/**
* @license lucide-react v0.303.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const _g=Z("ArrowRightLeft",[["path",{d:"m16 3 4 4-4 4",key:"1x1c3m"}],["path",{d:"M20 7H4",key:"zbl0bi"}],["path",{d:"m8 21-4-4 4-4",key:"h9nckh"}],["path",{d:"M4 17h16",key:"g4d7ey"}]]);/**
* @license lucide-react v0.303.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const Gd=Z("ArrowRight",[["path",{d:"M5 12h14",key:"1ays0h"}],["path",{d:"m12 5 7 7-7 7",key:"xquz4c"}]]);/**
* @license lucide-react v0.303.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const Cg=Z("BarChart3",[["path",{d:"M3 3v18h18",key:"1s2lah"}],["path",{d:"M18 17V9",key:"2bz60n"}],["path",{d:"M13 17V5",key:"1frdt8"}],["path",{d:"M8 17v-3",key:"17ska0"}]]);/**
* @license lucide-react v0.303.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const Nc=Z("BookOpen",[["path",{d:"M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z",key:"vv98re"}],["path",{d:"M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z",key:"1cyq3y"}]]);/**
* @license lucide-react v0.303.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const In=Z("Bot",[["path",{d:"M12 8V4H8",key:"hb8ula"}],["rect",{width:"16",height:"12",x:"4",y:"8",rx:"2",key:"enze0r"}],["path",{d:"M2 14h2",key:"vft8re"}],["path",{d:"M20 14h2",key:"4cs60a"}],["path",{d:"M15 13v2",key:"1xurst"}],["path",{d:"M9 13v2",key:"rq6x2g"}]]);/**
* @license lucide-react v0.303.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const Xd=Z("Box",[["path",{d:"M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z",key:"hh9hay"}],["path",{d:"m3.3 7 8.7 5 8.7-5",key:"g66t2b"}],["path",{d:"M12 22V12",key:"d0xqtd"}]]);/**
* @license lucide-react v0.303.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const Mt=Z("Brain",[["path",{d:"M9.5 2A2.5 2.5 0 0 1 12 4.5v15a2.5 2.5 0 0 1-4.96.44 2.5 2.5 0 0 1-2.96-3.08 3 3 0 0 1-.34-5.58 2.5 2.5 0 0 1 1.32-4.24 2.5 2.5 0 0 1 1.98-3A2.5 2.5 0 0 1 9.5 2Z",key:"1mhkh5"}],["path",{d:"M14.5 2A2.5 2.5 0 0 0 12 4.5v15a2.5 2.5 0 0 0 4.96.44 2.5 2.5 0 0 0 2.96-3.08 3 3 0 0 0 .34-5.58 2.5 2.5 0 0 0-1.32-4.24 2.5 2.5 0 0 0-1.98-3A2.5 2.5 0 0 0 14.5 2Z",key:"1d6s00"}]]);/**
* @license lucide-react v0.303.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const Eg=Z("Bug",[["path",{d:"m8 2 1.88 1.88",key:"fmnt4t"}],["path",{d:"M14.12 3.88 16 2",key:"qol33r"}],["path",{d:"M9 7.13v-1a3.003 3.003 0 1 1 6 0v1",key:"d7y7pr"}],["path",{d:"M12 20c-3.3 0-6-2.7-6-6v-3a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v3c0 3.3-2.7 6-6 6",key:"xs1cw7"}],["path",{d:"M12 20v-9",key:"1qisl0"}],["path",{d:"M6.53 9C4.6 8.8 3 7.1 3 5",key:"32zzws"}],["path",{d:"M6 13H2",key:"82j7cp"}],["path",{d:"M3 21c0-2.1 1.7-3.9 3.8-4",key:"4p0ekp"}],["path",{d:"M20.97 5c0 2.1-1.6 3.8-3.5 4",key:"18gb23"}],["path",{d:"M22 13h-4",key:"1jl80f"}],["path",{d:"M17.2 17c2.1.1 3.8 1.9 3.8 4",key:"k3fwyw"}]]);/**
* @license lucide-react v0.303.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const yo=Z("Calendar",[["rect",{width:"18",height:"18",x:"3",y:"4",rx:"2",ry:"2",key:"eu3xkr"}],["line",{x1:"16",x2:"16",y1:"2",y2:"6",key:"m3sa8f"}],["line",{x1:"8",x2:"8",y1:"2",y2:"6",key:"18kwsl"}],["line",{x1:"3",x2:"21",y1:"10",y2:"10",key:"xt86sb"}]]);/**
* @license lucide-react v0.303.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const Mn=Z("CheckCircle2",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"m9 12 2 2 4-4",key:"dzmm74"}]]);/**
* @license lucide-react v0.303.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const Wt=Z("CheckCircle",[["path",{d:"M22 11.08V12a10 10 0 1 1-5.93-9.14",key:"g774vq"}],["path",{d:"m9 11 3 3L22 4",key:"1pflzl"}]]);/**
* @license lucide-react v0.303.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const Yd=Z("Check",[["path",{d:"M20 6 9 17l-5-5",key:"1gmf2c"}]]);/**
* @license lucide-react v0.303.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const pt=Z("ChevronDown",[["path",{d:"m6 9 6 6 6-6",key:"qrunsl"}]]);/**
* @license lucide-react v0.303.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const zr=Z("ChevronRight",[["path",{d:"m9 18 6-6-6-6",key:"mthhwq"}]]);/**
* @license lucide-react v0.303.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const Zn=Z("ChevronUp",[["path",{d:"m18 15-6-6-6 6",key:"153udz"}]]);/**
* @license lucide-react v0.303.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const Rg=Z("Circle",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}]]);/**
* @license lucide-react v0.303.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const Xt=Z("Clock",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["polyline",{points:"12 6 12 12 16 14",key:"68esgv"}]]);/**
* @license lucide-react v0.303.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const i0=Z("Code",[["polyline",{points:"16 18 22 12 16 6",key:"z7tu5w"}],["polyline",{points:"8 6 2 12 8 18",key:"1eg1df"}]]);/**
* @license lucide-react v0.303.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const o0=Z("Container",[["path",{d:"M22 7.7c0-.6-.4-1.2-.8-1.5l-6.3-3.9a1.72 1.72 0 0 0-1.7 0l-10.3 6c-.5.2-.9.8-.9 1.4v6.6c0 .5.4 1.2.8 1.5l6.3 3.9a1.72 1.72 0 0 0 1.7 0l10.3-6c.5-.3.9-1 .9-1.5Z",key:"1t2lqe"}],["path",{d:"M10 21.9V14L2.1 9.1",key:"o7czzq"}],["path",{d:"m10 14 11.9-6.9",key:"zm5e20"}],["path",{d:"M14 19.8v-8.1",key:"159ecu"}],["path",{d:"M18 17.5V9.4",key:"11uown"}]]);/**
* @license lucide-react v0.303.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const cr=Z("Copy",[["rect",{width:"14",height:"14",x:"8",y:"8",rx:"2",ry:"2",key:"17jyea"}],["path",{d:"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2",key:"zix9uf"}]]);/**
* @license lucide-react v0.303.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const il=Z("Cpu",[["rect",{x:"4",y:"4",width:"16",height:"16",rx:"2",key:"1vbyd7"}],["rect",{x:"9",y:"9",width:"6",height:"6",key:"o3kz5p"}],["path",{d:"M15 2v2",key:"13l42r"}],["path",{d:"M15 20v2",key:"15mkzm"}],["path",{d:"M2 15h2",key:"1gxd5l"}],["path",{d:"M2 9h2",key:"1bbxkp"}],["path",{d:"M20 15h2",key:"19e6y8"}],["path",{d:"M20 9h2",key:"19tzq7"}],["path",{d:"M9 2v2",key:"165o2o"}],["path",{d:"M9 20v2",key:"i2bqo8"}]]);/**
* @license lucide-react v0.303.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const os=Z("Download",[["path",{d:"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4",key:"ih7n3h"}],["polyline",{points:"7 10 12 15 17 10",key:"2ggqvy"}],["line",{x1:"12",x2:"12",y1:"15",y2:"3",key:"1vk2je"}]]);/**
* @license lucide-react v0.303.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const An=Z("ExternalLink",[["path",{d:"M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6",key:"a6xqqp"}],["polyline",{points:"15 3 21 3 21 9",key:"mznyad"}],["line",{x1:"10",x2:"21",y1:"14",y2:"3",key:"18c3s4"}]]);/**
* @license lucide-react v0.303.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const ol=Z("Eye",[["path",{d:"M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z",key:"rwhkz3"}],["circle",{cx:"12",cy:"12",r:"3",key:"1v7zrd"}]]);/**
* @license lucide-react v0.303.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const yt=Z("FileText",[["path",{d:"M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z",key:"1nnpy2"}],["polyline",{points:"14 2 14 8 20 8",key:"1ew0cm"}],["line",{x1:"16",x2:"8",y1:"13",y2:"13",key:"14keom"}],["line",{x1:"16",x2:"8",y1:"17",y2:"17",key:"17nazh"}],["line",{x1:"10",x2:"8",y1:"9",y2:"9",key:"1a5vjj"}]]);/**
* @license lucide-react v0.303.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const vo=Z("FlaskConical",[["path",{d:"M10 2v7.527a2 2 0 0 1-.211.896L4.72 20.55a1 1 0 0 0 .9 1.45h12.76a1 1 0 0 0 .9-1.45l-5.069-10.127A2 2 0 0 1 14 9.527V2",key:"pzvekw"}],["path",{d:"M8.5 2h7",key:"csnxdl"}],["path",{d:"M7 16h10",key:"wp8him"}]]);/**
* @license lucide-react v0.303.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const pn=Z("Globe",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20",key:"13o1zl"}],["path",{d:"M2 12h20",key:"9i4pu4"}]]);/**
* @license lucide-react v0.303.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const Tg=Z("HardDrive",[["line",{x1:"22",x2:"2",y1:"12",y2:"12",key:"1y58io"}],["path",{d:"M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z",key:"oot6mr"}],["line",{x1:"6",x2:"6.01",y1:"16",y2:"16",key:"sgf278"}],["line",{x1:"10",x2:"10.01",y1:"16",y2:"16",key:"1l4acy"}]]);/**
* @license lucide-react v0.303.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const Pg=Z("Heart",[["path",{d:"M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4.05 3 5.5l7 7Z",key:"c3ymky"}]]);/**
* @license lucide-react v0.303.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const Lg=Z("Home",[["path",{d:"m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z",key:"y5dka4"}],["polyline",{points:"9 22 9 12 15 12 15 22",key:"e2us08"}]]);/**
* @license lucide-react v0.303.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const Ag=Z("Key",[["circle",{cx:"7.5",cy:"15.5",r:"5.5",key:"yqb3hr"}],["path",{d:"m21 2-9.6 9.6",key:"1j0ho8"}],["path",{d:"m15.5 7.5 3 3L22 7l-3-3",key:"1rn1fs"}]]);/**
* @license lucide-react v0.303.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const $g=Z("Layers",[["path",{d:"m12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83Z",key:"8b97xw"}],["path",{d:"m22 17.65-9.17 4.16a2 2 0 0 1-1.66 0L2 17.65",key:"dd6zsq"}],["path",{d:"m22 12.65-9.17 4.16a2 2 0 0 1-1.66 0L2 12.65",key:"ep9fru"}]]);/**
* @license lucide-react v0.303.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const Og=Z("Link",[["path",{d:"M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71",key:"1cjeqo"}],["path",{d:"M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71",key:"19qd67"}]]);/**
* @license lucide-react v0.303.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const qt=Z("Loader2",[["path",{d:"M21 12a9 9 0 1 1-6.219-8.56",key:"13zald"}]]);/**
* @license lucide-react v0.303.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const c0=Z("Lock",[["rect",{width:"18",height:"11",x:"3",y:"11",rx:"2",ry:"2",key:"1w4ew1"}],["path",{d:"M7 11V7a5 5 0 0 1 10 0v4",key:"fwvmzm"}]]);/**
* @license lucide-react v0.303.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const d0=Z("MessageSquare",[["path",{d:"M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z",key:"1lielz"}]]);/**
* @license lucide-react v0.303.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const Ig=Z("MinusCircle",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"M8 12h8",key:"1wcyev"}]]);/**
* @license lucide-react v0.303.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const Mg=Z("Minus",[["path",{d:"M5 12h14",key:"1ays0h"}]]);/**
* @license lucide-react v0.303.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const cl=Z("Pause",[["rect",{width:"4",height:"16",x:"6",y:"4",key:"iffhe4"}],["rect",{width:"4",height:"16",x:"14",y:"4",key:"sjin7j"}]]);/**
* @license lucide-react v0.303.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const Yt=Z("Play",[["polygon",{points:"5 3 19 12 5 21 5 3",key:"191637"}]]);/**
* @license lucide-react v0.303.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const Gt=Z("Plus",[["path",{d:"M5 12h14",key:"1ays0h"}],["path",{d:"M12 5v14",key:"s699le"}]]);/**
* @license lucide-react v0.303.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const ln=Z("RefreshCw",[["path",{d:"M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8",key:"v9h5vc"}],["path",{d:"M21 3v5h-5",key:"1q7to0"}],["path",{d:"M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16",key:"3uifl3"}],["path",{d:"M8 16H3v5",key:"1cv678"}]]);/**
* @license lucide-react v0.303.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const wo=Z("Rocket",[["path",{d:"M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z",key:"m3kijz"}],["path",{d:"m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z",key:"1fmvmk"}],["path",{d:"M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0",key:"1f8sc4"}],["path",{d:"M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5",key:"qeys4"}]]);/**
* @license lucide-react v0.303.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const Dg=Z("Router",[["rect",{width:"20",height:"8",x:"2",y:"14",rx:"2",key:"w68u3i"}],["path",{d:"M6.01 18H6",key:"19vcac"}],["path",{d:"M10.01 18H10",key:"uamcmx"}],["path",{d:"M15 10v4",key:"qjz1xs"}],["path",{d:"M17.84 7.17a4 4 0 0 0-5.66 0",key:"1rif40"}],["path",{d:"M20.66 4.34a8 8 0 0 0-11.31 0",key:"6a5xfq"}]]);/**
* @license lucide-react v0.303.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const u0=Z("Save",[["path",{d:"M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z",key:"1owoqh"}],["polyline",{points:"17 21 17 13 7 13 7 21",key:"1md35c"}],["polyline",{points:"7 3 7 8 15 8",key:"8nz8an"}]]);/**
* @license lucide-react v0.303.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const Zd=Z("ScrollText",[["path",{d:"M8 21h12a2 2 0 0 0 2-2v-2H10v2a2 2 0 1 1-4 0V5a2 2 0 1 0-4 0v3h4",key:"13a6an"}],["path",{d:"M19 17V5a2 2 0 0 0-2-2H4",key:"zz82l3"}],["path",{d:"M15 8h-5",key:"1khuty"}],["path",{d:"M15 12h-5",key:"r7krc0"}]]);/**
* @license lucide-react v0.303.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const Qr=Z("Search",[["circle",{cx:"11",cy:"11",r:"8",key:"4ej97u"}],["path",{d:"m21 21-4.3-4.3",key:"1qie3q"}]]);/**
* @license lucide-react v0.303.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const dl=Z("Send",[["path",{d:"m22 2-7 20-4-9-9-4Z",key:"1q3vgg"}],["path",{d:"M22 2 11 13",key:"nzbqef"}]]);/**
* @license lucide-react v0.303.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const zg=Z("Settings2",[["path",{d:"M20 7h-9",key:"3s1dr2"}],["path",{d:"M14 17H5",key:"gfn3mx"}],["circle",{cx:"17",cy:"17",r:"3",key:"18b49y"}],["circle",{cx:"7",cy:"7",r:"3",key:"dfmy0x"}]]);/**
* @license lucide-react v0.303.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const p0=Z("Settings",[["path",{d:"M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z",key:"1qme2f"}],["circle",{cx:"12",cy:"12",r:"3",key:"1v7zrd"}]]);/**
* @license lucide-react v0.303.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const Fg=Z("ShieldAlert",[["path",{d:"M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10",key:"1irkt0"}],["path",{d:"M12 8v4",key:"1got3b"}],["path",{d:"M12 16h.01",key:"1drbdi"}]]);/**
* @license lucide-react v0.303.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const Ke=Z("Shield",[["path",{d:"M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10",key:"1irkt0"}]]);/**
* @license lucide-react v0.303.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const m0=Z("SkipForward",[["polygon",{points:"5 4 15 12 5 20 5 4",key:"16p6eg"}],["line",{x1:"19",x2:"19",y1:"5",y2:"19",key:"futhcm"}]]);/**
* @license lucide-react v0.303.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const Ug=Z("Sparkles",[["path",{d:"m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275L12 3Z",key:"17u4zn"}],["path",{d:"M5 3v4",key:"bklmnn"}],["path",{d:"M19 17v4",key:"iiml17"}],["path",{d:"M3 5h4",key:"nem4j1"}],["path",{d:"M17 19h4",key:"lbex7p"}]]);/**
* @license lucide-react v0.303.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const eu=Z("Square",[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2",key:"afitv7"}]]);/**
* @license lucide-react v0.303.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const es=Z("StopCircle",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["rect",{width:"6",height:"6",x:"9",y:"9",key:"1wrtvo"}]]);/**
* @license lucide-react v0.303.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const Bg=Z("Tag",[["path",{d:"M12 2H2v10l9.29 9.29c.94.94 2.48.94 3.42 0l6.58-6.58c.94-.94.94-2.48 0-3.42L12 2Z",key:"14b2ls"}],["path",{d:"M7 7h.01",key:"7u93v4"}]]);/**
* @license lucide-react v0.303.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const Kt=Z("Target",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["circle",{cx:"12",cy:"12",r:"6",key:"1vlfrh"}],["circle",{cx:"12",cy:"12",r:"2",key:"1c9p78"}]]);/**
* @license lucide-react v0.303.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const Ye=Z("Terminal",[["polyline",{points:"4 17 10 11 4 5",key:"akl6gq"}],["line",{x1:"12",x2:"20",y1:"19",y2:"19",key:"q2wloq"}]]);/**
* @license lucide-react v0.303.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const Vg=Z("Timer",[["line",{x1:"10",x2:"14",y1:"2",y2:"2",key:"14vaq8"}],["line",{x1:"12",x2:"15",y1:"14",y2:"11",key:"17fdiu"}],["circle",{cx:"12",cy:"14",r:"8",key:"1e1u0o"}]]);/**
* @license lucide-react v0.303.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const Pt=Z("Trash2",[["path",{d:"M3 6h18",key:"d0wm0j"}],["path",{d:"M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6",key:"4alrt4"}],["path",{d:"M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2",key:"v07s0e"}],["line",{x1:"10",x2:"10",y1:"11",y2:"17",key:"1uufr5"}],["line",{x1:"14",x2:"14",y1:"11",y2:"17",key:"xtxkd"}]]);/**
* @license lucide-react v0.303.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const tu=Z("Upload",[["path",{d:"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4",key:"ih7n3h"}],["polyline",{points:"17 8 12 3 7 8",key:"t8dd8p"}],["line",{x1:"12",x2:"12",y1:"3",y2:"15",key:"widbto"}]]);/**
* @license lucide-react v0.303.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const Hg=Z("Wifi",[["path",{d:"M5 13a10 10 0 0 1 14 0",key:"6v8j51"}],["path",{d:"M8.5 16.5a5 5 0 0 1 7 0",key:"sej527"}],["path",{d:"M2 8.82a15 15 0 0 1 20 0",key:"dnpr2z"}],["line",{x1:"12",x2:"12.01",y1:"20",y2:"20",key:"of4bc4"}]]);/**
* @license lucide-react v0.303.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const vr=Z("Wrench",[["path",{d:"M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z",key:"cbrjhi"}]]);/**
* @license lucide-react v0.303.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const Ct=Z("XCircle",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"m15 9-6 6",key:"1uzhvr"}],["path",{d:"m9 9 6 6",key:"z0biqf"}]]);/**
* @license lucide-react v0.303.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const Sc=Z("X",[["path",{d:"M18 6 6 18",key:"1bl5f8"}],["path",{d:"m6 6 12 12",key:"d8bk6v"}]]);/**
* @license lucide-react v0.303.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const Rl=Z("Zap",[["polygon",{points:"13 2 3 14 12 14 11 22 21 10 12 10 13 2",key:"45s27k"}]]),Wg=[{path:"/",icon:Lg,label:"Dashboard"},{path:"/auto",icon:wo,label:"Auto Pentest"},{path:"/vuln-lab",icon:vo,label:"Vuln Lab"},{path:"/terminal",icon:Ye,label:"Terminal Agent"},{path:"/sandboxes",icon:o0,label:"Sandboxes"},{path:"/scan/new",icon:In,label:"AI Agent"},{path:"/realtime",icon:Rl,label:"Real-time Task"},{path:"/tasks",icon:Nc,label:"Task Library"},{path:"/scheduler",icon:Xt,label:"Scheduler"},{path:"/reports",icon:yt,label:"Reports"},{path:"/settings",icon:p0,label:"Settings"}];function qg(){const e=qs();return s.jsxs("aside",{className:"w-64 bg-dark-800 border-r border-dark-900/50 flex flex-col",children:[s.jsx("div",{className:"p-6 border-b border-dark-900/50",children:s.jsxs(nn,{to:"/",className:"flex items-center gap-3",children:[s.jsx("div",{className:"w-10 h-10 bg-primary-500 rounded-lg flex items-center justify-center",children:s.jsx(Ke,{className:"w-6 h-6 text-white"})}),s.jsxs("div",{children:[s.jsx("h1",{className:"text-lg font-bold text-white",children:"NeuroSploit"}),s.jsx("p",{className:"text-xs text-dark-400",children:"v3.0 AI Pentest"})]})]})}),s.jsx("nav",{className:"flex-1 p-4",children:s.jsx("ul",{className:"space-y-2",children:Wg.map(t=>{const n=e.pathname===t.path,r=t.icon;return s.jsx("li",{children:s.jsxs(nn,{to:t.path,className:`flex items-center gap-3 px-4 py-3 rounded-lg transition-colors ${n?"bg-primary-500/20 text-primary-500":"text-dark-300 hover:bg-dark-900/50 hover:text-white"}`,children:[s.jsx(r,{className:"w-5 h-5"}),s.jsx("span",{children:t.label})]})},t.path)})})}),s.jsx("div",{className:"p-4 border-t border-dark-900/50",children:s.jsxs("div",{className:"flex items-center gap-2 text-sm",children:[s.jsx(l0,{className:"w-4 h-4 text-green-500"}),s.jsx("span",{className:"text-dark-400",children:"System Online"})]})})]})}const Kg={"/":"Dashboard","/scan/new":"New Security Scan","/reports":"Reports","/settings":"Settings"};function Qg(){const e=qs(),t=Kg[e.pathname]||"NeuroSploit";return s.jsxs("header",{className:"h-16 bg-dark-800 border-b border-dark-900/50 flex items-center justify-between px-6",children:[s.jsx("h1",{className:"text-xl font-semibold text-white",children:t}),s.jsx("div",{className:"flex items-center gap-4",children:s.jsx("span",{className:"text-sm text-dark-400",children:new Date().toLocaleDateString("en-US",{weekday:"long",year:"numeric",month:"long",day:"numeric"})})})]})}function Jg({children:e}){return s.jsxs("div",{className:"flex min-h-screen bg-dark-700",children:[s.jsx(qg,{}),s.jsxs("div",{className:"flex-1 flex flex-col",children:[s.jsx(Qg,{}),s.jsx("main",{className:"flex-1 p-6 overflow-auto",children:e})]})]})}function f0(e){var t,n,r="";if(typeof e=="string"||typeof e=="number")r+=e;else if(typeof e=="object")if(Array.isArray(e)){var a=e.length;for(t=0;t<a;t++)e[t]&&(n=f0(e[t]))&&(r&&(r+=" "),r+=n)}else for(n in e)e[n]&&(r&&(r+=" "),r+=n);return r}function Jr(){for(var e,t,n=0,r="",a=arguments.length;n<a;n++)(e=arguments[n])&&(t=f0(e))&&(r&&(r+=" "),r+=t);return r}function re({children:e,className:t,title:n,subtitle:r,action:a}){return s.jsxs("div",{className:Jr("bg-dark-800 rounded-xl border border-dark-900/50",t),children:[(n||a)&&s.jsxs("div",{className:"flex items-center justify-between p-4 border-b border-dark-900/50",children:[s.jsxs("div",{children:[n&&s.jsx("h3",{className:"text-lg font-semibold text-white",children:n}),r&&s.jsx("p",{className:"text-sm text-dark-400 mt-1",children:r})]}),a]}),s.jsx("div",{className:"p-4",children:e})]})}function H({variant:e="primary",size:t="md",isLoading:n=!1,children:r,className:a,disabled:l,...i}){const o="inline-flex items-center justify-center font-medium rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-dark-700 disabled:opacity-50 disabled:cursor-not-allowed",c={primary:"bg-primary-500 text-white hover:bg-primary-600 focus:ring-primary-500",secondary:"bg-dark-900 text-white hover:bg-dark-800 focus:ring-dark-500",danger:"bg-red-600 text-white hover:bg-red-700 focus:ring-red-500",ghost:"text-dark-300 hover:text-white hover:bg-dark-900/50 focus:ring-dark-500"},d={sm:"px-3 py-1.5 text-sm",md:"px-4 py-2 text-sm",lg:"px-6 py-3 text-base"};return s.jsx("button",{className:Jr(o,c[e],d[t],a),disabled:l||n,...i,children:n?s.jsxs(s.Fragment,{children:[s.jsxs("svg",{className:"animate-spin -ml-1 mr-2 h-4 w-4",xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",children:[s.jsx("circle",{className:"opacity-25",cx:"12",cy:"12",r:"10",stroke:"currentColor",strokeWidth:"4"}),s.jsx("path",{className:"opacity-75",fill:"currentColor",d:"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"})]}),"Loading..."]}):r})}const Gg={critical:"bg-red-500/20 text-red-400 border-red-500/30",high:"bg-orange-500/20 text-orange-400 border-orange-500/30",medium:"bg-yellow-500/20 text-yellow-400 border-yellow-500/30",low:"bg-blue-500/20 text-blue-400 border-blue-500/30",iLine truncated
`).forEach(function(i){a=i.indexOf(":"),n=i.substring(0,a).trim().toLowerCase(),r=i.substring(a+1).trim(),!(!n||t[n]&&Zy[n])&&(n==="set-cookie"?t[n]?t[n].push(r):t[n]=[r]:t[n]=t[n]?t[n]+", "+r:r)}),t},lu=Symbol("internals");function nr(e){return e&&String(e).trim().toLowerCase()}function $a(e){return e===!1||e==null?e:T.isArray(e)?e.map($a):String(e)}function tv(e){const t=Object.create(null),n=/([^\s,;=]+)\s*(?:=\s*([^,;]+))?/g;let r;for(;r=n.exec(e);)t[r[1]]=r[2];return t}const nv=e=>/^[-_a-zA-Z0-9^`|~,!#$%&'*+.]+$/.test(e.trim());function ci(e,t,n,r,a){if(T.isFunction(r))return r.call(this,t,n);if(a&&(t=n),!!T.isString(t)){if(T.isString(r))return t.indexOf(r)!==-1;if(T.isRegExp(r))return r.test(t)}}function sv(e){return e.trim().toLowerCase().replace(/([a-z\d])(\w*)/g,(t,n,r)=>n.toUpperCase()+r)}function rv(e,t){const n=T.toCamelCase(" "+t);["get","set","has"].forEach(r=>{Object.defineProperty(e,r+n,{value:function(a,l,i){return this[r].call(this,t,a,l,i)},configurable:!0})})}let ft=class{constructor(t){t&&this.set(t)}set(t,n,r){const a=this;function l(o,c,d){const m=nr(c);if(!m)throw new Error("header name must be a non-empty string");const f=T.findKey(a,m);(!f||a[f]===void 0||d===!0||d===void 0&&a[f]!==!1)&&(a[f||c]=$a(o))}const i=(o,c)=>T.forEach(o,(d,m)=>l(d,m,c));if(T.isPlainObject(t)||t instanceof this.constructor)i(t,n);else if(T.isString(t)&&(t=t.trim())&&!nv(t))i(ev(t),n);else if(T.isObject(t)&&T.isIterable(t)){let o={},c,d;for(const m of t){if(!T.isArray(m))throw TypeError("Object iterator must return a key-value pair");o[d=m[0]]=(c=o[d])?T.isArray(c)?[...c,m[1]]:[c,m[1]]:m[1]}i(o,n)}else t!=null&&l(n,t,r);return this}get(t,n){if(t=nr(t),t){const r=T.findKey(this,t);if(r){const a=this[r];if(!n)return a;if(n===!0)return tv(a);if(T.isFunction(n))return n.call(this,a,r);if(T.isRegExp(n))return n.exec(a);throw new TypeError("parser must be boolean|regexp|function")}}}has(t,n){if(t=nr(t),t){const r=T.findKey(this,t);return!!(r&&this[r]!==void 0&&(!n||ci(this,this[r],r,n)))}return!1}delete(t,n){const r=this;let a=!1;function l(i){if(i=nr(i),i){const o=T.findKey(r,i);o&&(!n||ci(r,r[o],o,n))&&(delete r[o],a=!0)}}return T.isArray(t)?t.forEach(l):l(t),a}clear(t){const n=Object.keys(this);let r=n.length,a=!1;for(;r--;){const l=n[r];(!t||ci(this,this[l],l,t,!0))&&(delete this[l],a=!0)}return a}normalize(t){const n=this,r={};return T.forEach(this,(a,l)=>{const i=T.findKey(r,l);if(i){n[i]=$a(a),delete n[l];return}const o=t?sv(l):String(l).trim();o!==l&&delete n[l],n[o]=$a(a),r[o]=!0}),this}concat(...t){return this.constructor.concat(this,...t)}toJSON(t){const n=Object.create(null);return T.forEach(this,(r,a)=>{r!=null&&r!==!1&&(n[a]=t&&T.isArray(r)?r.join(", "):r)}),n}[Symbol.iterator](){return Object.entries(this.toJSON())[Symbol.iterator]()}toString(){return Object.entries(this.toJSON()).map(([t,n])=>t+": "+n).join(`
`)}getSetCookie(){return this.get("set-cookie")||[]}get[Symbol.toStringTag](){return"AxiosHeaders"}static from(t){return t instanceof this?t:new this(t)}static concat(t,...n){const r=new this(t);return n.forEach(a=>r.set(a)),r}static accessor(t){const r=(this[lu]=this[lu]={accessors:{}}).accessors,a=this.prototype;function l(i){const o=nr(i);r[o]||(rv(a,i),r[o]=!0)}return T.isArray(t)?t.forEach(l):l(t),this}};ft.accessor(["Content-Type","Content-Length","Accept","Accept-Encoding","User-Agent","Authorization"]);T.reduceDescriptors(ft.prototype,({value:e},t)=>{let n=t[0].toUpperCase()+t.slice(1);return{get:()=>e,set(r){this[n]=r}}});T.freezeMethods(ft);function di(e,t){const n=this||Zr,r=t||n,a=ft.from(r.headers);let l=r.data;return T.forEach(e,function(o){l=o.call(n,l,a.normalize(),t?t.status:void 0)}),a.normalize(),l}function E0(e){return!!(e&&e.__CANCEL__)}let ea=class extends ue{constructor(t,n,r){super(t??"canceled",ue.ERR_CANCELED,n,r),this.name="CanceledError",this.__CANCEL__=!0}};function R0(e,t,n){const r=n.config.validateStatus;!n.status||!r||r(n.status)?e(n):t(new ue("Request failed with status code "+n.status,[ue.ERR_BAD_REQUEST,ue.ERR_BAD_RESPONSE][Math.floor(n.status/100)-4],n.config,n.request,n))}function av(e){const t=/^([-+\w]{1,25})(:?\/\/|:)/.exec(e);return t&&t[1]||""}function lv(e,t){e=e||10;const n=new Array(e),r=new Array(e);let a=0,l=0,i;return t=t!==void 0?t:1e3,function(c){const d=Date.now(),m=r[l];i||(i=d),n[a]=c,r[a]=d;let f=l,p=0;for(;f!==a;)p+=n[f++],f=f%e;if(a=(a+1)%e,a===l&&(l=(l+1)%e),d-i<t)return;const N=m&&d-m;return N?Math.round(p*1e3/N):void 0}}function iv(e,t){let n=0,r=1e3/t,a,l;const i=(d,m=Date.now())=>{n=m,a=null,l&&(clearTimeout(l),l=null),e(...d)};return[(...d)=>{const m=Date.now(),f=m-n;f>=r?i(d,m):(a=d,l||(l=setTimeout(()=>{l=null,i(a)},r-f)))},()=>a&&i(a)]}const ul=(e,t,n=3)=>{let r=0;const a=lv(50,250);return iv(l=>{const i=l.loaded,o=l.lengthComputable?l.total:void 0,c=i-r,d=a(c),m=i<=o;r=i;const f={loaded:i,total:o,progress:o?i/o:void 0,bytes:c,rate:d||void 0,estimated:d&&o&&m?(o-i)/d:void 0,event:l,lengthComputable:o!=null,[t?"download":"upload"]:!0};e(f)},n)},iu=(e,t)=>{const n=e!=null;return[r=>t[0]({lengthComputable:n,total:e,loaded:r}),t[1]]},ou=e=>(...t)=>T.asap(()=>e(...t)),ov=Ze.hasStandardBrowserEnv?((e,t)=>n=>(n=new URL(n,Ze.origin),e.protocol===n.protocol&&e.host===n.host&&(t||e.port===n.port)))(new URL(Ze.origin),Ze.navigator&&/(msie|trident)/i.test(Ze.navigator.userAgent)):()=>!0,cv=Ze.hasStandardBrowserEnv?{write(e,t,n,r,a,l,i){if(typeof document>"u")return;const o=[`${e}=${encodeURIComponent(t)}`];T.isNumber(n)&&o.push(`expires=${new Date(n).toUTCString()}`),T.isString(r)&&o.push(`path=${r}`),T.isString(a)&&o.push(`domain=${a}`),l===!0&&o.push("secure"),T.isString(i)&&o.push(`SameSite=${i}`),document.cookie=o.join("; ")},read(e){if(typeof document>"u")return null;const t=document.cookie.match(new RegExp("(?:^|; )"+e+"=([^;]*)"));return t?decodeURIComponent(t[1]):null},remove(e){this.write(e,"",Date.now()-864e5,"/")}}:{write(){},read(){return null},remove(){}};function dv(e){return typeof e!="string"?!1:/^([a-z][a-z\d+\-.]*:)?\/\//i.test(e)}function uv(e,t){return t?e.replace(/\/?\/$/,"")+"/"+t.replace(/^\/+/,""):e}function T0(e,t,n){let r=!dv(t);return e&&(r||n==!1)?uv(e,t):t}const cu=e=>e instanceof ft?{...e}:e;function cs(e,t){t=t||{};const n={};function r(d,m,f,p){return T.isPlainObject(d)&&T.isPlainObject(m)?T.merge.call({caseless:p},d,m):T.isPlainObject(m)?T.merge({},m):T.isArray(m)?m.slice():m}function a(d,m,f,p){if(T.isUndefined(m)){if(!T.isUndefined(d))return r(void 0,d,f,p)}else return r(d,m,f,p)}function l(d,m){if(!T.isUndefined(m))return r(void 0,m)}function i(d,m){if(T.isUndefined(m)){if(!T.isUndefined(d))return r(void 0,d)}else return r(void 0,m)}function o(d,m,f){if(f in t)return r(d,m);if(f in e)return r(void 0,d)}const c={url:l,method:l,data:l,baseURL:i,transformRequest:i,transformResponse:i,paramsSerializer:i,timeout:i,timeoutMessage:i,withCredentials:i,withXSRFToken:i,adapter:i,responseType:i,xsrfCookieName:i,xsrfHeaderName:i,onUploadProgress:i,onDownloadProgress:i,decompress:i,maxContentLength:i,maxBodyLength:i,beforeRedirect:i,transport:i,httpAgent:i,httpsAgent:i,cancelToken:i,socketPath:i,responseEncoding:i,validateStatus:o,headers:(d,m,f)=>a(cu(d),cu(m),f,!0)};return T.forEach(Object.keys({...e,...t}),function(m){if(m==="__proto__"||m==="constructor"||m==="prototype")return;const f=T.hasOwnProp(c,m)?c[m]:a,p=f(e[m],t[m],m);T.isUndefined(p)&&f!==o||(n[m]=p)}),n}const P0=e=>{const t=cs({},e);let{data:n,withXSRFToken:r,xsrfHeaderName:a,xsrfCookieName:l,headers:i,auth:o}=t;if(t.headers=i=ft.from(i),t.url=_0(T0(t.baseURL,t.url,t.allowAbsoluteUrls),e.params,e.paramsSerializer),o&&i.set("Authorization","Basic "+btoa((o.username||"")+":"+(o.password?unescape(encodeURIComponent(o.password)):""))),T.isFormData(n)){if(Ze.hasStandardBrowserEnv||Ze.hasStandardBrowserWebWorkerEnv)i.setContentType(void 0);else if(T.isFunction(n.getHeader Line truncated
`+i.map(xu).join(`
`):" "+xu(i[0]):"as no adapter specified";throw new ue("There is no suitable adapter to dispatch the request "+o,"ERR_NOT_SUPPORT")}return a}const A0={getAdapter:kv,adapters:Tc};function ui(e){if(e.cancelToken&&e.cancelToken.throwIfRequested(),e.signal&&e.signal.aborted)throw new ea(null,e)}function hu(e){return ui(e),e.headers=ft.from(e.headers),e.data=di.call(e,e.transformRequest),["post","put","patch"].indexOf(e.method)!==-1&&e.headers.setContentType("application/x-www-form-urlencoded",!1),A0.getAdapter(e.adapter||Zr.adapter,e)(e).then(function(r){return ui(e),r.data=di.call(e,e.transformResponse,r),r.headers=ft.from(r.headers),r},function(r){return E0(r)||(ui(e),r&&r.response&&(r.response.data=di.call(e,e.transformResponse,r.response),r.response.headers=ft.from(r.response.headers))),Promise.reject(r)})}const $0="1.13.5",$l={};["object","boolean","number","function","string","symbol"].forEach((e,t)=>{$l[e]=function(r){return typeof r===e||"a"+(t<1?"n ":" ")+e}});const gu={};$l.transitional=function(t,n,r){function a(l,i){return"[Axios v"+$0+"] Transitional option '"+l+"'"+i+(r?". "+r:"")}return(l,i,o)=>{if(t===!1)throw new ue(a(i," has been removed"+(n?" in "+n:"")),ue.ERR_DEPRECATED);return n&&!gu[i]&&(gu[i]=!0,console.warn(a(i," has been deprecated since v"+n+" and will be removed in the near future"))),t?t(l,i,o):!0}};$l.spelling=function(t){return(n,r)=>(console.warn(`${r} is likely a misspelling of ${t}`),!0)};function bv(e,t,n){if(typeof e!="object")throw new ue("options must be an object",ue.ERR_BAD_OPTION_VALUE);const r=Object.keys(e);let a=r.length;for(;a-- >0;){const l=r[a],i=t[l];if(i){const o=e[l],c=o===void 0||i(o,l,e);if(c!==!0)throw new ue("option "+l+" must be "+c,ue.ERR_BAD_OPTION_VALUE);continue}if(n!==!0)throw new ue("Unknown option "+l,ue.ERR_BAD_OPTION)}}const Oa={assertOptions:bv,validators:$l},bt=Oa.validators;let ns=class{constructor(t){this.defaults=t||{},this.interceptors={request:new au,response:new au}}async request(t,n){try{return await this._request(t,n)}catch(r){if(r instanceof Error){let a={};Error.captureStackTrace?Error.captureStackTrace(a):a=new Error;const l=a.stack?a.stack.replace(/^.+\n/,""):"";try{r.stack?l&&!String(r.stack).endsWith(l.replace(/^.+\n.+\n/,""))&&(r.stack+=`
`+l):r.stack=l}catch{}}throw r}}_request(t,n){typeof t=="string"?(n=n||{},n.url=t):n=t||{},n=cs(this.defaults,n);const{transitional:r,paramsSerializer:a,headers:l}=n;r!==void 0&&Oa.assertOptions(r,{silentJSONParsing:bt.transitional(bt.boolean),forcedJSONParsing:bt.transitional(bt.boolean),clarifyTimeoutError:bt.transitional(bt.boolean),legacyInterceptorReqResOrdering:bt.transitional(bt.boolean)},!1),a!=null&&(T.isFunction(a)?n.paramsSerializer={serialize:a}:Oa.assertOptions(a,{encode:bt.function,serialize:bt.function},!0)),n.allowAbsoluteUrls!==void 0||(this.defaults.allowAbsoluteUrls!==void 0?n.allowAbsoluteUrls=this.defaults.allowAbsoluteUrls:n.allowAbsoluteUrls=!0),Oa.assertOptions(n,{baseUrl:bt.spelling("baseURL"),withXsrfToken:bt.spelling("withXSRFToken")},!0),n.method=(n.method||this.defaults.method||"get").toLowerCase();let i=l&&T.merge(l.common,l[n.method]);l&&T.forEach(["delete","get","head","post","put","patch","common"],v=>{delete l[v]}),n.headers=ft.concat(i,l);const o=[];let c=!0;this.interceptors.request.forEach(function(w){if(typeof w.runWhen=="function"&&w.runWhen(n)===!1)return;c=c&&w.synchronous;const k=n.transitional||Ec;k&&k.legacyInterceptorReqResOrdering?o.unshift(w.fulfilled,w.rejected):o.push(w.fulfilled,w.rejected)});const d=[];this.interceptors.response.forEach(function(w){d.push(w.fulfilled,w.rejected)});let m,f=0,p;if(!c){const v=[hu.bind(this),void 0];for(v.unshift(...o),v.push(...d),p=v.length,m=Promise.resolve(n);f<p;)m=m.then(v[f++],v[f++]);return m}p=o.length;let N=n;for(;f<p;){const v=o[f++],w=o[f++];try{N=v(N)}catch(k){w.call(this,k);break}}try{m=hu.call(this,N)}catch(v){return Promise.reject(v)}for(f=0,p=d.length;f<p;)m=m.then(d[f++],d[f++]);return m}getUri(t){t=cs(this.defaults,t);const n=T0(t.baseURL,t.url,t.allowAbsoluteUrls);return _0(n,t.params,t.paramsSerializer)}};T.forEach(["delete","get","head","options"],function(t){ns.prototype[t]=function(n,r){return this.request(cs(r||{},{method:t,url:n,data:(r||{}).data}))}});T.forEach(["post","put","patch"],function(t){function n(r){return function(l,i,o){return this.request(cs(o||{},{method:t,headers:r?{"Content-Type":"multipart/form-data"}:{},url:l,data:i}))}}ns.prototype[t]=n(),ns.prototype[t+"Form"]=n(!0)});let Nv=class O0{constructor(t){if(typeof t!="function")throw new TypeError("executor must be a function.");let n;this.promise=new Promise(function(l){n=l});const r=this;this.promise.then(a=>{if(!r._listeners)return;let l=r._listeners.length;for(;l-- >0;)r._listeners[l](a);r._listeners=null}),this.promise.then=a=>{let l;const i=new Promise(o=>{r.subscribe(o),l=o}).then(a);return i.cancel=function(){r.unsubscribe(l)},i},t(function(l,i,o){r.reason||(r.reason=new ea(l,i,o),n(r.reason))})}throwIfRequested(){if(this.reason)throw this.reason}subscribe(t){if(this.reason){t(this.reason);return}this._listeners?this._listeners.push(t):this._listeners=[t]}unsubscribe(t){if(!this._listeners)return;const n=this._listeners.indexOf(t);n!==-1&&this._listeners.splice(n,1)}toAbortSignal(){const t=new AbortController,n=r=>{t.abort(r)};return this.subscribe(n),t.signal.unsubscribe=()=>this.unsubscribe(n),t.signal}static source(){let t;return{token:new O0(function(a){t=a}),cancel:t}}};function Sv(e){return function(n){return e.apply(null,n)}}function _v(e){return T.isObject(e)&&e.isAxiosError===!0}const No={Continue:100,SwitchingProtocols:101,Processing:102,EarlyHints:103,Ok:200,Created:201,Accepted:202,NonAuthoritativeInformation:203,NoContent:204,ResetContent:205,PartialContent:206,MultiStatus:207,AlreadyReported:208,ImUsed:226,MultipleChoices:300,MovedPermanently:301,Found:302,SeeOther:303,NotModified:304,UseProxy:305,Unused:306,TemporaryRedirect:307,PermanentRedirect:308,BadRequest:400,Unauthorized:401,PaymentRequired:402,Forbidden:403,NotFound:404,MethodNotAllowed:405,NotAcceptable:406,ProxyAuthenticationRequired:407,RequestTimeout:408,Conflict:409,Gone:410,LengthRequired:411,PreconditionFailed:412,PayloadTooLarge:413,UriTooLong:414,UnsupportedMediaType:415,RangeNotSatisfiable:416,ExpectationFailed:417,ImATeapot:418,MisdirectedRequest:421,UnprocessableEntity:422,Locked:423,FailedDependency:424,TooEarly:425,UpgradeRequired:426,PreconditionRequired:428,TooManyRequests:429,RequestHeaderFieldsTooLarge:431,UnavailableForLegalReasons:451,InternalServerError:500,NotImplemented:501,BadGateway:502,ServiceUnavailable:503,GatewayTimeout:504,HttpVersionNotSupported:505,VariantAlsoNegotiates:506,InsufficientStorage:507,LoopDetected:508,NotExtended:510,NetworkAuthenticationRequired:511,WebServerIsDown:521,ConnectionTimedOut:522,OriginIsUnreachable:523,TimeoutOccurred:524,SslHandshakeFailed:525,InvalidSslCertificate:526};Object.entries(No).forEach(([e,t])=>{No[t]=e});function I0(e){const t=new ns(e),n=x0(ns.prototype.request,t);return T.extend(n,ns.prototype,t,{allOwnKeys:!0}),T.extend(n,t,null,{allOwnKeys:!0}),n.create=function(a){return I0(cs(e,a))},n}const Me=I0(Zr);Me.Axios=ns;Me.CanceledError=ea;Me.CancelToken=Nv;Me.isCancel=E0;Me.VERSION=$0; Line truncated
* @license React
* use-sync-external-store-shim.production.js
*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/var Us=x;function Rv(e,t){return e===t&&(e!==0||1/e===1/t)||e!==e&&t!==t}var Tv=typeof Object.is=="function"?Object.is:Rv,Pv=Us.useState,Lv=Us.useEffect,Av=Us.useLayoutEffect,$v=Us.useDebugValue;function Ov(e,t){var n=t(),r=Pv({inst:{value:n,getSnapshot:t}}),a=r[0].inst,l=r[1];return Av(function(){a.value=n,a.getSnapshot=t,fi(a)&&l({inst:a})},[e,n,t]),Lv(function(){return fi(a)&&l({inst:a}),e(function(){fi(a)&&l({inst:a})})},[e]),$v(n),n}function fi(e){var t=e.getSnapshot;e=e.value;try{var n=t();return!Tv(e,n)}catch{return!0}}function Iv(e,t){return t()}var Mv=typeof window>"u"||typeof window.document>"u"||typeof window.document.createElement>"u"?Iv:Ov;F0.useSyncExternalStore=Us.useSyncExternalStore!==void 0?Us.useSyncExternalStore:Mv;z0.exports=F0;var Dv=z0.exports;/**
* @license React
* use-sync-external-store-shim/with-selector.production.js
*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/var Ol=x,zv=Dv;function Fv(e,t){return e===t&&(e!==0||1/e===1/t)||e!==e&&t!==t}var Uv=typeof Object.is=="function"?Object.is:Fv,Bv=zv.useSyncExternalStore,Vv=Ol.useRef,Hv=Ol.useEffect,Wv=Ol.useMemo,qv=Ol.useDebugValue;D0.useSyncExternalStoreWithSelector=function(e,t,n,r,a){var l=Vv(null);if(l.current===null){var i={hasValue:!1,value:null};l.current=i}else i=l.current;l=Wv(function(){function c(N){if(!d){if(d=!0,m=N,N=r(N),a!==void 0&&i.hasValue){var v=i.value;if(a(v,N))return f=v}return f=N}if(v=f,Uv(m,N))return v;var w=r(N);return a!==void 0&&a(v,w)?(m=N,v):(m=N,f=w)}var d=!1,m,f,p=n===void 0?null:n;return[function(){return c(t())},p===null?void 0:function(){return c(p())}]},[t,n,r,a]);var o=Bv(e,l[0],l[1]);return Hv(function(){i.hasValue=!0,i.value=o},[o]),qv(o),o};M0.exports=D0;var Kv=M0.exports;const Qv=Iu(Kv),U0={},{useDebugValue:Jv}=Ro,{useSyncExternalStoreWithSelector:Gv}=Qv;let ju=!1;const Xv=e=>e;function Yv(e,t=Xv,n){(U0?"production":void 0)!=="production"&&n&&!ju&&(console.warn("[DEPRECATED] Use `createWithEqualityFn` instead of `create` or use `useStoreWithEqualityFn` instead of `useStore`. They can be imported from 'zustand/traditional'. https://github.com/pmndrs/zustand/discussions/1937"),ju=!0);const r=Gv(e.subscribe,e.getState,e.getServerState||e.getInitialState,t,n);return Jv(r),r}const ku=e=>{(U0?"production":void 0)!=="production"&&typeof e!="function"&&console.warn("[DEPRECATED] Passing a vanilla store will be unsupported in a future version. Instead use `import { useStore } from 'zustand'`.");const t=typeof e=="function"?Ev(e):e,n=(r,a)=>Yv(t,r,a);return Object.assign(n,t),n},B0=e=>e?ku(e):ku,Zv={};function ew(e,t){let n;try{n=e()}catch{return}return{getItem:a=>{var l;const i=c=>c===null?null:JSON.parse(c,void 0),o=(l=n.getItem(a))!=null?l:null;return o instanceof Promise?o.then(i):i(o)},setItem:(a,l)=>n.setItem(a,JSON.stringify(l,void 0)),removeItem:a=>n.removeItem(a)}}const Fr=e=>t=>{try{const n=e(t);return n instanceof Promise?n:{then(r){return Fr(r)(n)},catch(r){return this}}}catch(n){return{then(r){return this},catch(r){return Fr(r)(n)}}}},tw=(e,t)=>(n,r,a)=>{let l={getStorage:()=>localStorage,serialize:JSON.stringify,deserialize:JSON.parse,partialize:k=>k,version:0,merge:(k,y)=>({...y,...k}),...t},i=!1;const o=new Set,c=new Set;let d;try{d=l.getStorage()}catch{}if(!d)return e((...k)=>{console.warn(`[zustand persist middleware] Unable to update item '${l.name}', the given storage is currently unavailable.`),n(...k)},r,a);const m=Fr(l.serialize),f=()=>{const k=l.partialize({...r()});let y;const u=m({state:k,version:l.version}).then(g=>d.setItem(l.name,g)).catch(g=>{y=g});if(y)throw y;return u},p=a.setState;a.setState=(k,y)=>{p(k,y),f()};const N=e((...k)=>{n(...k),f()},r,a);let v;const w=()=>{var k;if(!d)return;i=!1,o.forEach(u=>u(r()));const y=((k=l.onRehydrateStorage)==null?void 0:k.call(l,r()))||void 0;return Fr(d.getItem.bind(d))(l.name).then(u=>{if(u)return l.deserialize(u)}).then(u=>{if(u)if(typeof u.version=="number"&&u.version!==l.version){if(l.migrate)return l.migrate(u.state,u.version);console.error("State loaded from storage couldn't be migrated since no migrate function was provided")}else return u.state}).then(u=>{var g;return v=l.merge(u,(g=r())!=null?g:N),n(v,!0),f()}).then(()=>{y==null||y(v,void 0),i=!0,c.forEach(u=>u(v))}).catch(u=>{y==null||y(void 0,u)})};return a.persist={setOptions:k=>{l={...l,...k},k.getStorage&&(d=k.getStorage())},clearStorage:()=>{d==null||d.removeItem(l.name)},getOptions:()=>l,rehydrate:()=>w(),hasHydrated:()=>i,onHydrate:k=>(o.add(k),()=>{o.delete(k)}),onFinishHydration:k=>(c.add(k),()=>{c.delete(k)})},w(),v||N},nw=(e,t)=>(n,r,a)=>{let l={storage:ew(()=>localStorage),partialize:w=>w,version:0,merge:(w,k)=>({...k,...w}),...t},i=!1;const o=new Set,c=new Set;let d=l.storage;if(!d)return e((...w)=>{console.warn(`[zustand persist middleware] Unable to update item '${l.name}', the given storage is currently unavailable.`),n(...w)},r,a);const m=()=>{const w=l.partialize({...r()});return d.setItem(l.name,{state:w,version:l.version})},f=a.setState;a.setState=(w,k)=>{f(w,k),m()};const p=e((...w)=>{n(...w),m()},r,a);a.getInitialState=()=>p;let N;const v=()=>{var w,k;if(!d)return;i=!1,o.forEach(u=>{var g;return u((g=r())!=null?g:p)});const y=((k=l.onRehydrateStorage)==null?void 0:k.call(l,(w=r())!=null?w:p))||void 0;return Fr(d.getItem.bind(d))(l.name).then(u=>{if(u)if(typeof u.version=="number"&&u.version!==l.version){if(l.migrate)return[!0,l.migrate(u.state,u.version)];console.error("State loaded from storage couldn't be migrated since no migrate function was provided")}else return[!1,u.state];return[!1,void 0]}).then(u=>{var g;const[_,L]=u;if(N=l.merge(L,(g=r())!=null?g:p),n(N,!0),_)return m()}).then(()=>{y==null||y(N,void 0),N=r(),i=!0,c.forEach(u=>u(N))}).catch(u=>{y==null||y(void 0,u)})};return a.persist={setOptions:w=>{l={...l,...w},w.storage&&(d=w.storage)},clearStorage:()=>{d==null||d.removeItem(l.name)},getOptions:()=>l,rehydrate:()=>v(),haLine truncated
https://example1.com
https://example2.com`,rows:5,value:i,onChange:M=>{o(M.target.value),f("")}}),s.jsx("p",{className:"text-xs text-dark-500 mt-1",children:"Note: Agent will test the first URL. Multiple URL support coming soon."}),m&&s.jsx("p",{className:"mt-1 text-sm text-red-400",children:m})]}),n==="file"&&s.jsxs("div",{children:[s.jsx("input",{type:"file",ref:t,onChange:he,accept:".txt,.csv,.lst",className:"hidden"}),s.jsxs("div",{onClick:()=>{var M;return(M=t.current)==null?void 0:M.click()},className:"border-2 border-dashed border-dark-700 rounded-lg p-8 text-center cursor-pointer hover:border-primary-500 transition-colors",children:[s.jsx(tu,{className:"w-10 h-10 mx-auto text-dark-400 mb-3"}),s.jsx("p",{className:"text-dark-300",children:"Click to upload a file with URLs"}),s.jsx("p",{className:"text-sm text-dark-500 mt-1",children:"Supports .txt, .csv, .lst files"})]}),c.length>0&&s.jsxs("p",{className:"mt-2 text-sm text-green-400",children:[c.length," valid URLs loaded - using first URL"]}),m&&s.jsx("p",{className:"mt-2 text-sm text-red-400",children:m})]})]})}),s.jsxs(re,{title:s.jsxs("div",{className:"flex items-center justify-between w-full",children:[s.jsxs("div",{className:"flex items-center gap-2",children:[s.jsx(Nc,{className:"w-5 h-5 text-primary-500"}),s.jsx("span",{children:"Task Library"})]}),s.jsx(H,{variant:"ghost",size:"sm",onClick:()=>L(!_),children:s.jsx(pt,{className:`w-4 h-4 transition-transform ${_?"rotate-180":""}`})})]}),subtitle:"Select a preset task or create a custom prompt",children:[s.jsx("div",{className:"flex items-center justify-between mb-4 pb-4 border-b border-dark-700",children:s.jsxs("div",{className:"flex items-center gap-2",children:[s.jsx("input",{type:"checkbox",id:"customPrompt",checked:E,onChange:M=>U(M.target.checked),className:"w-4 h-4 rounded border-dark-600 bg-dark-800 text-primary-500 focus:ring-primary-500"}),s.jsx("label",{htmlFor:"customPrompt",className:"text-white",children:"Use custom prompt instead of task"})]})}),E?s.jsx(Ur,{placeholder:`Enter your custom prompt for the AI agent...
Example: Test for SQL injection on all form inputs, check for authentication bypass on the login endpoint, and look for IDOR vulnerabilities in user profile APIs.`,rows:6,value:q,onChange:M=>A(M.target.value)}):s.jsxs(s.Fragment,{children:[_&&s.jsx(s.Fragment,{children:s.jsx("div",{className:"flex gap-2 mb-4 flex-wrap",children:ow.map(M=>s.jsx(H,{variant:u===M.id?"primary":"secondary",size:"sm",onClick:()=>ge(M.id),children:M.name},M.id))})}),s.jsx("div",{className:`grid grid-cols-1 md:grid-cols-2 gap-3 ${_?"max-h-80 overflow-auto":""}`,children:D?s.jsx("p",{className:"text-dark-400 col-span-2 text-center py-4",children:"Loading tasks..."}):(_?v:v.slice(0,4)).map(M=>{var me;return s.jsxs("div",{onClick:()=>y(M),className:`p-4 rounded-lg border cursor-pointer transition-all ${(k==null?void 0:k.id)===M.id?"border-primary-500 bg-primary-500/10":"border-dark-700 hover:border-dark-500 bg-dark-900/50"}`,children:[s.jsxs("div",{className:"flex items-center justify-between mb-1",children:[s.jsx("span",{className:"font-medium text-white",children:M.name}),M.is_preset&&s.jsx("span",{className:"text-xs bg-primary-500/20 text-primary-400 px-2 py-0.5 rounded",children:"Preset"})]}),s.jsx("p",{className:"text-sm text-dark-400 line-clamp-2",children:M.description}),s.jsxs("div",{className:"flex items-center gap-2 mt-2",children:[s.jsx("span",{className:"text-xs text-dark-500",children:M.category}),M.estimated_tokens>0&&s.jsxs("span",{className:"text-xs text-dark-500",children:["~",M.estimated_tokens," tokens"]})]}),((me=M.tags)==null?void 0:me.length)>0&&s.jsx("div",{className:"flex gap-1 mt-2 flex-wrap",children:M.tags.slice(0,3).map(de=>s.jsx("span",{className:"text-xs bg-dark-700 text-dark-300 px-2 py-0.5 rounded",children:de},de))})]},M.id)})}),!_&&v.length>4&&s.jsxs(H,{variant:"ghost",className:"w-full mt-3",onClick:()=>L(!0),children:["Show all ",v.length," tasks"]})]}),k&&!E&&s.jsxs("div",{className:"mt-4 p-4 bg-dark-800 rounded-lg border border-dark-700",children:[s.jsxs("div",{className:"flex items-center justify-between mb-2",children:[s.jsxs("span",{className:"font-medium text-white",children:["Selected: ",k.name]}),s.jsx(H,{variant:"ghost",size:"sm",onClick:()=>y(null),children:"Clear"})]}),s.jsx("p",{className:"text-sm text-dark-400 whitespace-pre-wrap line-clamp-4",children:k.prompt})]})]}),s.jsx(re,{title:s.jsxs("div",{className:"flex items-center gap-2",children:[s.jsx(Ag,{className:"w-5 h-5 text-primary-500"}),s.jsx("span",{children:"Authentication"}),s.jsx("span",{className:"text-xs text-dark-500",children:"(Optional)"})]}),children:s.jsxs("div",{className:"space-y-4",children:[s.jsx("div",{className:"flex gap-2 flex-wrap",children:[{id:"none",label:"None"},{id:"cookie",label:"Cookie"},{id:"bearer",label:"Bearer Token"},{id:"basic",label:"Basic Auth"},{id:"header",label:"Custom Header"}].map(M=>s.jsx(H,{variant:ne===M.id?"primary":"secondary",size:"sm",onClick:()=>ae(M.id),children:M.label},M.id))}),ne!=="none"&&s.jsx(st,{placeholder:ne==="cookie"?"session=abc123; token=xyz789":ne==="bearer"?"eyJhbGciOiJIUzI1NiIs...":ne==="basic"?"username:password":"X-API-Key: your-api-key",value:ce,onChange:M=>xe(M.target.value),label:ne==="cookie"?"Cookie String":ne==="bearer"?"Bearer Token":ne==="basic"?"Username:Password":"Header:Value"})]})}),s.jsxs(re,{title:s.jsxs("div",{className:"flex items-center gap-2 cursor-pointer",onClick:()=>Q(!te),children:[s.jsx(p0,{className:"w-5 h-5 text-primary-500"}),s.jsx("span",{children:"Advanced Options"}),s.jsx(pt,{className:`w-4 h-4 transition-transform ${te?"rotate-180":""}`})]}),children:[te&&s.jsx("div",{className:"space-y-4",children:s.jsxs("div",{children:[s.jsx("label",{className:"text-sm text-dark-300 mb-1 block",children:"Max Crawl Depth"}),s.jsxs("div",{className:"flex items-center gap-4",children:[s.jsx("input",{type:"range",min:"1",max:"10",value:z,onChange:M=>Y(parseInt(M.target.value)),className:"flex-1"}),s.jsx("span",{className:"text-white font-medium w-8",children:z})]})]})}),!te&&s.jsx("p",{className:"text-dark-500 text-sm",children:"Click to expand advanced options"})]}),p==="prompt_only"&&s.jsxs("div",{className:"bg-yellow-500/10 border border-yellow-500/30 rounded-lg p-4 flex items-start gap-3",children:[s.jsx(Oe,{className:"w-6 h-6 text-yellow-500 flex-shrink-0"}),s.jsxs("div",{children:[s.jsx("p",{className:"font-medium text-yellow-400",children:"High Token Usage Warning"}),s.jsx("p",{className:"text-sm text-yellow-300/80 mt-1",children:"In AI Prompt Mode, the agent has full autonomy to decide what tools to use and what tests to run. This results in significantly higher API token consumption. Consider using Full Auto mode for most use cases."})]})]}),s.jsxs("div",{className:"flex justify-end gap-3 sticky bottom-4 bg-dark-950/90 backdrop-blur p-4 -mx-4 rounded-lg",children:[s.jsx(H,{variant:"secondary",onClick:()=>e("/"),children:"Cancel"}),s.jsxs(H,{onClick:P,isLoading:se,size:"lg",children:[s.jsx(Yt,{className:"w-5 h-5 mr-2"}),"Deploy Agent (",X.name,") Line truncated
<div style="background: #1e293b; border: 1px solid #334155; border-left: 4px solid ${S[F.severity]}; border-radius: 8px; margin-bottom: 24px; overflow: hidden; page-break-inside: avoid;">
<div style="padding: 20px; display: flex; justify-content: space-between; align-items: flex-start; background: linear-gradient(135deg, ${S[F.severity]}10 0%, transparent 100%);">
<div style="flex: 1;">
<div style="display: flex; align-items: center; gap: 12px; margin-bottom: 8px;">
<span style="background: ${S[F.severity]}; color: white; padding: 4px 12px; border-radius: 4px; font-size: 11px; font-weight: 700; text-transform: uppercase;">
${F.severity}
</span>
<span style="color: #64748b; font-size: 12px;">Finding #${ie+1}</span>
</div>
<h3 style="margin: 0 0 8px 0; color: white; font-size: 18px; font-weight: 600;">${F.title}</h3>
<p style="margin: 0; color: #94a3b8; font-size: 13px; font-family: monospace;">${F.affected_endpoint}</p>
</div>
</div>
<div style="padding: 20px; border-top: 1px solid #334155;">
<!-- Technical Metrics -->
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 16px; padding: 16px; background: #0f172a; border-radius: 8px; margin-bottom: 20px;">
${F.cvss_score?`
<div>
<div style="color: #64748b; font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px;">CVSS 3.1 Score</div>
<div style="display: flex; align-items: baseline; gap: 8px;">
<span style="font-size: 28px; font-weight: 700; color: ${S[F.severity]};">${F.cvss_score}</span>
<span style="font-size: 12px; color: #94a3b8;">${F.cvss_score>=9?"Critical":F.cvss_score>=7?"High":F.cvss_score>=4?"Medium":"Low"}</span>
</div>
${F.cvss_vector?`<div style="font-size: 10px; color: #475569; font-family: monospace; margin-top: 4px;">${F.cvss_vector}</div>`:""}
</div>
`:""}
${F.cwe_id?`
<div>
<div style="color: #64748b; font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px;">CWE Reference</div>
<a href="${Te}" target="_blank" style="color: #60a5fa; text-decoration: none; font-size: 14px; font-weight: 500;">${F.cwe_id}</a>
</div>
`:""}
${pe?`
<div>
<div style="color: #64748b; font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px;">OWASP Top 10</div>
<div style="color: #fbbf24; font-size: 13px; font-weight: 500;">${pe}</div>
</div>
`:""}
<div>
<div style="color: #64748b; font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px;">Vulnerability Type</div>
<div style="color: white; font-size: 14px;">${F.vulnerability_type}</div>
</div>
</div>
<!-- Description -->
${F.description?`
<div style="margin-bottom: 20px;">
<h4 style="color: #e2e8f0; font-size: 13px; font-weight: 600; margin: 0 0 8px; text-transform: uppercase; letter-spacing: 0.5px;">📋 Description</h4>
<p style="color: #cbd5e1; margin: 0; line-height: 1.7; font-size: 14px;">${F.description}</p>
</div>
`:""}
<!-- Affected Endpoint -->
<div style="margin-bottom: 20px;">
<h4 style="color: #e2e8f0; font-size: 13px; font-weight: 600; margin: 0 0 8px; text-transform: uppercase; letter-spacing: 0.5px;">🎯 Affected Endpoint</h4>
<div style="background: #0f172a; padding: 12px 16px; border-radius: 6px; font-family: monospace; font-size: 13px; color: #38bdf8; overflow-x: auto;">${F.affected_endpoint}</div>
</div>
<!-- Evidence -->
${F.evidence?`
<div style="margin-bottom: 20px;">
<h4 style="color: #e2e8f0; font-size: 13px; font-weight: 600; margin: 0 0 8px; text-transform: uppercase; letter-spacing: 0.5px;">🔍 Evidence / Proof of Concept</h4>
<pre style="background: #0f172a; padding: 16px; border-radius: 6px; color: #fbbf24; margin: 0; overflow-x: auto; font-size: 12px; line-height: 1.5; white-space: pre-wrap; word-break: break-all;">${F.evidence}</pre>
</div>
`:""}
<!-- Impact -->
${F.impact?`
<div style="margin-bottom: 20px;">
<h4 style="color: #e2e8f0; font-size: 13px; font-weight: 600; margin: 0 0 8px; text-transform: uppercase; letter-spacing: 0.5px;">⚠️ Impact</h4>
<p style="color: #cbd5e1; margin: 0; line-height: 1.7; font-size: 14px;">${F.impact}</p>
</div>
`:""}
<!-- Remediation -->
${F.remediation?`
<div style="background: linear-gradient(135deg, #16a34a15 0%, #16a34a05 100%); border: 1px solid #16a34a40; border-radius: 8px; padding: 16px;">
<h4 style="color: #4ade80; font-size: 13px; font-weight: 600; margin: 0 0 8px; text-transform: uppercase; letter-spacing: 0.5px;">✅ Remediation</h4>
<p style="color: #cbd5e1; margin: 0; line-height: 1.7; font-size: 14px;">${F.remediation}</p>
</div>
`:""}
<!-- References -->
${F.references&&F.references.length>0?`
<div style="margin-top: 20px;">
<h4 style="color: #e2e8f0; font-size: 13px; font-weight: 600; margin: 0 0 8px; text-transform: uppercase; letter-spacing: 0.5px;">📚 References</h4>
<ul style="margin: 0; padding-left: 20px; color: #94a3b8; font-size: 13px;">
${F.references.map(ve=>`<li style="margin-bottom: 4px;"><a href="${ve}" target="_blank" style="color: #60a5fa; text-decoration: none;">${ve}</a></li>`).join("")}
</ul>
</div>
`:""}
</div>
</div>
`}).join(""),K=`
<div style="background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%); border: 1px solid #334155; border-radius: 12px; padding: 24px; margin-bottom: 40px;">
<h2 style="color: white; margin: 0 0 16px; font-size: 20px; border: none; padding: 0;">📊 Executive Summary</h2>
<p style="color: #cbd5e1; line-height: 1.8; margin: 0 0 20px;">
This security assessment of <strong style="color: white;">${a.target}</strong> was conducted using NeuroSploit AI-powered penetration testing platform.
The assessment identified <strong style="color: white;">${a.findings.length} security findings</strong> across various severity levels.
${I.critical>0?`<span style="color: #dc2626; font-weight: 600;">${I.critical} critical vulnerabilities require immediate attention.</span>`:""}
${I.high>0?`<span style="color: #ea580c;">${I.high} high-severity issues should be addressed promptly.</span>`:""}
</p>
<div style="display: flex; align-items: center; gap: 16px; padding: 16px; background: #0f172a; border-radius: 8px;">
<div>
<div style="color: #64748b; font-size: 12px; text-transform: uppercase; margin-bottom: 4px;">Overall Risk Score</div>
<div style="font-size: 32px; font-weight: 700; color: ${C};">${O}/100</div>
</div>
<div style="flex: 1;">
<div style="height: 12px; background: #1e293b; border-radius: 6px; overflow: hidden;">
<div style="height: 100%; width: ${O}%; background: ${C}; border-radius: 6px;"></div>
</div>
<div style="color: ${C}; font-size: 14px; font-weight: 600; margin-top: 8px;">${J} Risk</div>
</div>
</div>
</div>
`;return`<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>NeuroSploit Security Report - ${e}</title>
<style>
* { box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; margin: 0; padding: 40px; line-height: 1.6; }
.container { max-width: 1000px; margin: 0 auto; }
.header { text-align: center; margin-bottom: 40px; padding-bottom: 40px; border-bottom: 1px solid #334155; }
.header h1 { color: white; margin: 0 0 8px; font-size: 28px; }
.header p { color: #94a3b8; margin: 0; font-size: 14px; }
.stats { display: grid; grid-template-columns: repeat(6, 1fr); gap: 12px; margin-bottom: 40px; }
.stat-card { background: #1e293b; border: 1px solid #334155; border-radius: 8px; padding: 16px; text-align: center; }
.stat-value { font-size: 28px; font-weight: bold; margin-bottom: 4px; }
.stat-label { color: #94a3b8; font-size: 12px; text-transform: uppercase; letter-spacing: 0.5px; }
h2 { color: white; border-bottom: 1px solid #334155; padding-bottom: 12px; font-size: 18px; }
.footer { text-align: center; margin-top: 40px; padding-top: 40px; border-top: 1px solid #334155; color: #64748b; font-size: 12px; }
@media print {
body { background: white; color: black; padding: 20px; }
.stat-card, .findings > div { border-color: #ddd; background: #f9f9f9; }
.header, .footer { border-color: #ddd; }
}
@media (max-width: 768px) {
.stats { grid-template-columns: repeat(3, 1fr); }
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🛡️ NeuroSploit Security Assessment Report</h1>
<p>Target: ${a.target} | Agent ID: ${e} | Mode: ${vi[a.mode]||a.mode}</p>
<p>Date: ${new Date().toLocaleDateString("en-US",{weekday:"long",year:"numeric",month:"long",day:"numeric"})}</p>
</div>
${K}
<div class="stats">
<div class="stat-card"><div class="stat-value" style="color: white;">${a.findings.length}</div><div class="stat-label">Total</div></div>
<div class="stat-card"><div class="stat-value" style="color: #dc2626;">${I.critical}</div><div class="stat-label">Critical</div></div>
<div class="stat-card"><div class="stat-value" style="color: #ea580c;">${I.high}</div><div class="stat-label">High</div></div>
<div class="stat-card"><div class="stat-value" style="color: #ca8a04;">${I.medium}</div><div class="stat-label">Medium</div></div>
<div class="stat-card"><div class="stat-value" style="color: #2563eb;">${I.low}</div><div class="stat-label">Low</div></div>
<div class="stat-card"><div class="stat-value" style="color: #6b7280;">${I.info}</div><div class="stat-label">Info</div></div>
</div>
<h2>🔍 Detailed Findings</h2>
<div class="findings">
${j||'<p style="text-align: center; color: #94a3b8; padding: 40px;">No vulnerabilities identified during this assessment.</p>'}
</div>
<div class="footer">
<p><strong>Generated by NeuroSploit v3.0 AI Security Scanner</strong></p>
<p>Report generated: ${new Date().toISOString()}</p>
<p style="margin-top: 16px; font-size: 11px;">This report is confidential and intended for authorized personnel only.</p>
</div>
</div>
</body>
</html>`},G=async()=>{if(e){y(!0);try{await we.stop(e);const S=await we.getStatus(e);l(S)}catch(S){console.error("Failed to stop agent:",S)}finally{y(!1)}}},ge=async()=>{if(e)try{await we.pause(e);const S=await we.getStatus(e);l(S)}catch(S){console.error("Failed to pause agent:",S)}},he=async()=>{if(e)try{await we.resume(e);const S=await we.getStatus(e);l(S)}catch(S){console.error("Failed to resume agent:",S)}},ke=async()=>{if(!_.trim()||!e)return;R(!0),U(null);const S=_;try{await we.sendPrompt(e,_),L(""),U(`Prompt sent: "${S.slice(0,50)}${S.length>50?"...":""}"`),setTimeout(()=>U(null),5e3);const[h,b]=await Promise.all([we.getStatus(e),we.getLogs(e,200)]);l(h),o(b.logs||[])}catch(h){console.error("Failed to send prompt:",h),U("Failed to send prompt"),setTimeout(()=>U(null),3e3)}finally{R(!1)}},P=async S=>{if(e){Q(!0);try{await we.skipToPhase(e,S);const h=a?yi(a.phase):0,b=gi.findIndex(O=>O.key===S),I=new Set(ne);for(let O=h;O<b;O++)I.add(gi[O].key);ae(I),A(null)}catch(h){console.error("Failed to skip phase:",h)}finally{Q(!1)}}};x.useEffect(()=>{if(!a)return;const S=a.phase.toLowerCase();if(S.includes("_skipped")){const h=S.replace("_skipped","");ae(b=>new Set(b).add(h))}},[a==null?void 0:a.phase]);const X=S=>{navigator.clipboard.writeText(S)};if(c)return s.jsx("div",{className:"flex items-center justify-center h-64",children:s.jsx(ln,{className:"w-8 h-8 animate-spin text-primary-500"})});if(m)return s.jsxs("div",{className:"flex flex-col items-center justify-center h-64",children:[s.jsx(Ct,{className:"w-12 h-12 text-red-500 mb-4"}),s.jsx("p",{className:"text-xl text-white mb-2",children:m}),s.jsx(H,{onClick:()=>t("/scan/new"),children:"Start New Agent"})]});if(!a)return null;const M={critical:a.findings.filter(S=>S.severity==="critical").length,high:a.findings.filter(S=>S.severity==="high").length,medium:a.findings.filter(S=>S.severity==="medium").length,low:a.findings.filter(S=>S.severity==="low").length,info:a.findings.filter(S=>S.severity==="info").length},me=S=>{var h;return s.jsxs("div",{className:"p-4 pt-0 space-y-4 border-t border-dark-700",children:[s.jsxs("div",{className:"flex flex-wrap items-center gap-4",children:[s.jsxs("div",{className:"flex items-center gap-2",children:[s.jsx("span",{className:"text-sm text-dark-400",children:"CVSS:"}),s.jsx("span",{className:`font-bold ${S.cvss_score>=9?"text-red-500":S.cvss_score>=7?"text-orange-500":S.cvss_score>=4?"text-yellow-500":"text-blue-500"}`,children:((h=S.cvss_score)==null?void 0:h.toFixed(1))||"N/A"})]}),S.cwe_id&&s.jsxs("div",{className:"flex items-center gap-2",children:[s.jsx("span",{className:"text-sm text-dark-400",children:"CWE:"}),s.jsxs("a",{href:`https://cwe.mitre.org/data/definitions/${S.cwe_id.replace("CWE-","")}.html`,target:"_blank",rel:"noopener noreferrer",className:"text-primary-400 hover:underline flex items-center gap-1",children:[S.cwe_id,s.jsx(An,{className:"w-3 h-3"})]})]}),s.jsx("span",{className:"text-xs bg-dark-700 px-2 py-1 rounded text-dark-300",children:S.vulnerability_type}),S.confidence&&s.jsxs("span",{className:`text-xs px-2 py-1 rounded ${S.confidence==="high"?"bg-green-500/20 text-green-400":S.confidence==="medium"?"bg-yellow-500/20 text-yellow-400":"bg-red-500/20 text-red-400"}`,children:[S.confidence," confidence"]})]}),S.cvss_vector&&s.jsx("div",{className:"text-xs bg-dark-800 p-2 rounded font-mono text-dark-300",children:S.cvss_vector}),s.jsxs("div",{className:"bg-dark-800/50 rounded-lg p-4 space-y-3",children:[s.jsxs("h4",{className:"text-sm font-medium text-primary-400 flex items-center gap-2",children:[s.jsx(i0,{className:"w-4 h-4"}),"Technical Details"]}),s.jsxs("div",{children:[s.jsx("span",{className:"text-xs text-dark-500",children:"Endpoint:"}),s.jsxs("div",{className:"flex items-center gap-2 mt-1",children:[s.jsx(pn,{className:"w-4 h-4 text-dark-400"}),s.jsx("code",{className:"text-sm text-blue-400 bg-dark-900 px-2 py-1 rounded break-all",children:S.affected_endpoint})]})]}),S.parameter&&s.jsxs("div",{children:[s.jsx("span",{className:"text-xs text-dark-500",children:"Vulnerable Parameter:"}),s.jsx("code",{className:"block mt-1 text-sm text-yellow-400 bg-dark-900 px-2 py-1 rounded",children:S.parameter})]}),S.payload&&s.jsxs("div",{children:[s.jsxs("div",{className:"flex items-center justify-between",children:[s.jsx("span",{className:"text-xs text-dark-500",children:"Payload Used:"}),s.jsx(H,{variant:"ghost",size:"sm",onClick:()=>X(S.payload),children:s.jsx(cr,{className:"w-3 h-3"})})]}),s.jsx("code",{className:"block mt-1 text-sm text-red-400 bg-dark-900 px-2 py-1 rounded break-all",children:S.payload})]}),S.request&&s.jsxs("div",{children:[s.jsxs("div",{className:"flex items-center justify-between",children:[s.jsx("span",{className:"text-xs text-dark-500",children:"HTTP Request:"}),s.jsx(H,{variant:"ghost",size:"sm",onClick:()=>X(S.request),children:s.jsx(cr,{className:"w-3 h-3"})})]}),s.jsx("pre",{className:"mt-1 text-xs text-green-400 bg-dark-900 p-2 rounded overflow-x-auto max-h-32",children:S.Line truncated
`).map(pe=>pe.trim()).filter(Boolean):void 0,ie=await we.autoPentest(C,{subdomain_discovery:o,targets:F,auth_type:f||void 0,auth_value:N||void 0,prompt:y.trim()||void 0});D(ie.agent_id)}catch(F){q(((K=(j=F==null?void 0:F.response)==null?void 0:j.data)==null?void 0:K.detail)||(F==null?void 0:F.message)||"Failed to start pentest"),_(!1)}}},P=async()=>{if(L)try{await we.stop(L),_(!1)}catch{}},X=()=>{localStorage.removeItem(wi),D(null),E(null),ce([]),_(!1),q(null),G(null),n("")},M=x.useCallback(async()=>{var C,j;if(R!=null&&R.scan_id){se(!0);try{const K=await We.generateAiReport({scan_id:R.scan_id,title:`AI Pentest Report - ${t}`});G(K.id)}catch(K){q(((j=(C=K==null?void 0:K.response)==null?void 0:C.data)==null?void 0:j.detail)||"Failed to generate AI report")}finally{se(!1)}}},[R==null?void 0:R.scan_id,t]),me=R?Sw(R.progress):-1,de=(R==null?void 0:R.findings)||[],S=(R==null?void 0:R.rejected_findings)||[],h=[...de,...S],[b,I]=x.useState("all"),O=b==="confirmed"?de:b==="rejected"?S:h,J=de.reduce((C,j)=>(C[j.severity]=(C[j.severity]||0)+1,C),{});return s.jsxs("div",{className:"min-h-screen flex flex-col items-center py-12 px-4",children:[s.jsxs("div",{className:"text-center mb-10",children:[s.jsx("div",{className:"inline-flex items-center justify-center w-16 h-16 bg-green-500/20 rounded-2xl mb-4",children:s.jsx(wo,{className:"w-8 h-8 text-green-400"})}),s.jsx("h1",{className:"text-3xl font-bold text-white mb-2",children:"Auto Pentest"}),s.jsx("p",{className:"text-dark-400 max-w-md",children:"One-click comprehensive penetration test. 100 vulnerability types, AI-powered analysis, full report."})]}),!L&&s.jsxs("div",{className:"w-full max-w-2xl bg-dark-800 border border-dark-700 rounded-2xl p-8",children:[s.jsxs("div",{className:"mb-6",children:[s.jsx("label",{className:"block text-sm font-medium text-dark-300 mb-2",children:"Target URL"}),s.jsx("input",{type:"url",value:t,onChange:C=>n(C.target.value),placeholder:"https://example.com",disabled:g,className:"w-full px-4 py-4 bg-dark-900 border border-dark-600 rounded-xl text-white text-lg placeholder-dark-500 focus:outline-none focus:border-green-500 focus:ring-1 focus:ring-green-500 disabled:opacity-50"})]}),s.jsxs("div",{className:"flex flex-wrap gap-4 mb-6",children:[s.jsxs("label",{className:"flex items-center gap-2 cursor-pointer",children:[s.jsx("input",{type:"checkbox",checked:o,onChange:C=>c(C.target.checked),disabled:g,className:"w-4 h-4 rounded bg-dark-900 border-dark-600 text-green-500 focus:ring-green-500"}),s.jsx("span",{className:"text-sm text-dark-300",children:"Subdomain Discovery"})]}),s.jsxs("label",{className:"flex items-center gap-2 cursor-pointer",children:[s.jsx("input",{type:"checkbox",checked:r,onChange:C=>a(C.target.checked),disabled:g,className:"w-4 h-4 rounded bg-dark-900 border-dark-600 text-green-500 focus:ring-green-500"}),s.jsx("span",{className:"text-sm text-dark-300",children:"Multiple Targets"})]})]}),r&&s.jsxs("div",{className:"mb-6",children:[s.jsx("label",{className:"block text-sm font-medium text-dark-300 mb-2",children:"Additional Targets (one per line)"}),s.jsx("textarea",{value:l,onChange:C=>i(C.target.value),rows:4,disabled:g,placeholder:`https://api.example.com
https://admin.example.com`,className:"w-full px-4 py-3 bg-dark-900 border border-dark-600 rounded-xl text-white placeholder-dark-500 focus:outline-none focus:border-green-500 disabled:opacity-50"})]}),s.jsxs("div",{className:"mb-6",children:[s.jsxs("button",{onClick:()=>m(!d),disabled:g,className:"flex items-center gap-2 text-sm text-dark-400 hover:text-white transition-colors disabled:opacity-50",children:[s.jsx(c0,{className:"w-4 h-4"}),s.jsx("span",{children:"Authentication (Optional)"}),d?s.jsx(Zn,{className:"w-4 h-4"}):s.jsx(pt,{className:"w-4 h-4"})]}),d&&s.jsxs("div",{className:"mt-3 space-y-3 pl-6",children:[s.jsxs("select",{value:f,onChange:C=>p(C.target.value),disabled:g,className:"w-full px-3 py-2 bg-dark-900 border border-dark-600 rounded-lg text-white text-sm focus:outline-none focus:border-green-500",children:[s.jsx("option",{value:"",children:"No Authentication"}),s.jsx("option",{value:"bearer",children:"Bearer Token"}),s.jsx("option",{value:"cookie",children:"Cookie"}),s.jsx("option",{value:"basic",children:"Basic Auth (user:pass)"}),s.jsx("option",{value:"header",children:"Custom Header (Name:Value)"})]}),f&&s.jsx("input",{type:"text",value:N,onChange:C=>v(C.target.value),disabled:g,placeholder:f==="bearer"?"eyJhbGciOiJIUzI1NiIs...":f==="cookie"?"session=abc123; token=xyz":f==="basic"?"admin:password123":"X-API-Key:your-api-key",className:"w-full px-3 py-2 bg-dark-900 border border-dark-600 rounded-lg text-white text-sm placeholder-dark-500 focus:outline-none focus:border-green-500"})]})]}),s.jsxs("div",{className:"mb-6",children:[s.jsxs("button",{onClick:()=>k(!w),disabled:g,className:"flex items-center gap-2 text-sm text-dark-400 hover:text-white transition-colors disabled:opacity-50",children:[s.jsx(d0,{className:"w-4 h-4"}),s.jsx("span",{children:"Custom AI Prompt (Optional)"}),w?s.jsx(Zn,{className:"w-4 h-4"}):s.jsx(pt,{className:"w-4 h-4"})]}),w&&s.jsxs("div",{className:"mt-3 pl-6",children:[s.jsx("textarea",{value:y,onChange:C=>u(C.target.value),rows:4,disabled:g,placeholder:`Focus on authentication bypass and IDOR vulnerabilities.
The app uses JWT tokens in localStorage.
Admin panel at /admin requires role=admin cookie.`,className:"w-full px-4 py-3 bg-dark-900 border border-dark-600 rounded-xl text-white text-sm placeholder-dark-500 focus:outline-none focus:border-green-500 disabled:opacity-50"}),s.jsx("p",{className:"mt-1 text-xs text-dark-500",children:"Guide the AI agent with additional context, focus areas, or specific instructions."})]})]}),U&&s.jsxs("div",{className:"mb-6 p-3 bg-red-500/10 border border-red-500/20 rounded-lg flex items-center gap-2",children:[s.jsx(Oe,{className:"w-5 h-5 text-red-400"}),s.jsx("span",{className:"text-red-400 text-sm",children:U})]}),s.jsxs("button",{onClick:ke,disabled:!t.trim(),className:"w-full py-4 bg-green-500 hover:bg-green-600 disabled:bg-dark-600 disabled:text-dark-400 text-white font-bold text-lg rounded-xl transition-colors flex items-center justify-center gap-3",children:[s.jsx(wo,{className:"w-6 h-6"}),"START PENTEST"]})]}),L&&s.jsxs("div",{className:"w-full max-w-4xl",children:[s.jsxs("div",{className:"bg-dark-800 border border-dark-700 rounded-2xl p-6 mb-6",children:[s.jsxs("div",{className:"flex items-center justify-between mb-4",children:[s.jsxs("div",{className:"flex items-center gap-3",children:[s.jsx("div",{className:`w-3 h-3 rounded-full ${g?"bg-green-500 animate-pulse":(R==null?void 0:R.status)==="completed"?"bg-green-500":(R==null?void 0:R.status)==="error"?"bg-red-500":"bg-gray-500"}`}),s.jsx("h3",{className:"text-white font-semibold",children:g?"Pentest Running":(R==null?void 0:R.status)==="completed"?"Pentest Complete":(R==null?void 0:R.status)==="error"?"Pentest Failed":"Pentest Stopped"}),s.jsx("span",{className:"text-dark-400 text-sm",children:t})]}),s.jsxs("div",{className:"flex items-center gap-2",children:[g&&s.jsxs("button",{onClick:P,className:"px-4 py-1.5 bg-red-500/20 hover:bg-red-500/30 text-red-400 rounded-lg text-sm transition-colors flex items-center gap-1.5",children:[s.jsx(Sc,{className:"w-4 h-4"})," Stop"]}),!g&&s.jsxs("button",{onClick:X,className:"px-4 py-1.5 bg-dark-700 hover:bg-dark-600 text-dark-300 rounded-lg text-sm transition-colors flex items-center gap-1.5",children:[s.jsx(Pt,{className:"w-4 h-4"})," Clear Session"]})]})]}),R&&s.jsxs(s.Fragment,{children:[s.jsxs("div",{className:"flex items-center justify-between text-sm mb-2",children:[s.jsx("span",{className:"text-dark-400",children:R.phase||"Initializing..."}),s.jsxs("span",{className:"text-dark-400",children:[R.progress,"%"]})]}),s.jsx("div",{className:"w-full bg-dark-900 rounded-full h-2 mb-4",children:s.jsx("div",{className:"bg-green-500 h-2 rounded-full transition-all duration-500",style:{width:`${R.progress}%`}})}),s.jsx("div",{className:"grid grid-cols-3 gap-3",children:kw.map((C,j)=>{const K=C.icon,F=j===me&&g,ie=j<me||R.status==="completed";return s.jsxs("div",{className:`rounded-xl p-3 border transition-all duration-300 ${F?"bg-green-500/10 border-green-500/30":ie?"bg-dark-700/50 border-dark-600":"bg-dark-900 border-dark-700"}`,children:[s.jsxs("div",{className:"flex items-center gap-2 mb-1",children:[F?s.jsx(qt,{className:"w-4 h-4 animate-spin text-green-400 flex-shrink-0"}):ie?s.jsx(Mn,{className:"w-4 h-4 text-green-500 flex-shrink-0"}):s.jsx(K,{className:`w-4 h-4 flex-shrink-0 ${F?"text-green-400":"text-dark-500"}`}),s.jsx("span",{className:`text-xs font-medium ${F?"text-green-400":ie?"text-dark-300":"text-dark-500"}`,children:C.label}),s.jsxs("span",{className:`ml-auto text-[10px] ${F?"text-green-500/60":ie?"text-dark-500":"text-dark-600"}`,children:[C.range[0],"-",C.range[1],"%"]})]}),j===0&&s.jsx("div",{className:"flex flex-wrap gap-1.5 mt-2",children:bw.map(pe=>s.jsx(_w,{stream:pe,progress:R.progress,isRunning:g},pe.key))})]},C.key)})})]})]}),de.length>0&&s.jsxs("div",{className:"bg-dark-800 border border-dark-700 rounded-2xl p-6 mb-6",children:[s.jsxs("div",{className:"flex items-center justify-between mb-4",children:[s.jsxs("div",{className:"flex items-center gap-3",children:[s.jsxs("h3",{className:"text-white font-semibold",children:["Findings (",de.length,")"]}),s.jsx("div",{className:"flex gap-2",children:["critical","high","medium","low","info"].map(C=>{const j=J[C]||0;return j===0?null:s.jsx("span",{className:`${Tu[C]} text-white px-2 py-0.5 rounded-full text-xs font-bold`,children:j},C)})})]}),s.jsxs("div",{className:"flex gap-2",children:[s.jsxs("button",{onClick:()=>{te(!0),ne(!1)},className:`px-3 py-1 rounded-lg text-xs transition-colors ${A?"bg-primary-500 text-white":"bg-dark-700 text-dark-400 hover:text-white"}`,children:[s.jsx(Eg,{className:"w-3 h-3 inline mr-1"}),"Findings (",de.length,")"]}),S.length>0&&s.jsxs("button",{onClick:()=>{te(!0),ne(!1),I(b==="rejected"?"all":"rejected")},className:`px-3 py-1 rounded-lg text-xs transition-colors ${b==="rejected"&&A?"bg-orange-500/30 text-orange-400":"bg-dark-700 text-orange-400/60 hover:text-orange-400"}`,children:[s.jsx(Oe,{className:"w-3 h-3 inline mr-1"}),"Rejected (",S.length,")"]}),s.jsxs("button",{onClick:()=>{ne(!0),te(!1)},className:`px-3 py-1 rounLine truncated
[stderr]
${h.stderr}`:""].filter(Boolean).join("")||"(no output)",timestamp:new Date().toISOString(),exit_code:h.exit_code,command:h.command,duration:h.duration};c(O=>O&&{...O,messages:[...O.messages,I]}),(me=n.current)==null||me.focus()}catch(h){const b={role:"tool",content:((S=(de=h==null?void 0:h.response)==null?void 0:de.data)==null?void 0:S.detail)||(h==null?void 0:h.message)||"Command execution failed",timestamp:new Date().toISOString(),exit_code:-1};c(I=>I&&{...I,messages:[...I.messages,b]})}finally{_(!1)}},[l,f,g,N]),he=x.useCallback(async()=>{if(l)try{const P=await Zt.getExploitationPath(l);c(X=>X&&{...X,exploitation_path:P.steps||P||[]})}catch{}},[l]);x.useEffect(()=>{l&&o&&o.messages.length>0&&he()},[l,o==null?void 0:o.messages.length,he]);const ke=(P,X)=>{switch(P.role){case"user":return s.jsx("div",{className:"flex justify-end animate-fadeIn",children:s.jsxs("div",{className:"max-w-[80%] rounded-xl px-4 py-3 border-l-4 border-primary-500 bg-dark-800 text-white shadow-lg",children:[s.jsx("div",{className:"whitespace-pre-wrap text-sm leading-relaxed break-words",children:P.content}),s.jsx("div",{className:"text-[10px] mt-2 text-dark-500",children:new Date(P.timestamp).toLocaleTimeString()})]})},X);case"assistant":return s.jsx("div",{className:"flex justify-start animate-fadeIn",children:s.jsxs("div",{className:"max-w-[85%] rounded-xl px-4 py-3 bg-dark-900 text-dark-200 shadow-lg",children:[s.jsxs("div",{className:"flex items-center gap-2 mb-2 text-xs text-dark-400",children:[s.jsx(In,{className:"w-3.5 h-3.5 text-primary-500"}),s.jsx("span",{className:"font-medium",children:"Terminal Agent"})]}),s.jsx("div",{className:"whitespace-pre-wrap text-sm leading-relaxed break-words",children:P.content}),P.suggested_commands&&P.suggested_commands.length>0&&s.jsx("div",{className:"flex flex-wrap gap-2 mt-3 pt-3 border-t border-dark-700",children:P.suggested_commands.map((M,me)=>s.jsxs("button",{onClick:()=>ge(M),disabled:g,className:"px-3 py-1.5 text-xs font-mono bg-green-500/20 text-green-400 rounded-lg hover:bg-green-500/30 transition-colors disabled:opacity-50 flex items-center gap-1.5 border border-green-500/20 hover:border-green-500/40",children:[s.jsx(Yt,{className:"w-3 h-3"}),M]},me))}),s.jsx("div",{className:"text-[10px] mt-2 text-dark-500",children:new Date(P.timestamp).toLocaleTimeString()})]})},X);case"tool":return s.jsx("div",{className:"flex justify-start animate-fadeIn",children:s.jsxs("div",{className:`max-w-[90%] rounded-xl px-4 py-3 bg-dark-950 shadow-lg border-l-4 ${P.exit_code!==void 0&&P.exit_code!==0?"border-red-500":"border-green-500/50"}`,children:[P.command&&s.jsxs("div",{className:"flex items-center gap-2 mb-2 text-xs",children:[s.jsx(Ye,{className:"w-3.5 h-3.5 text-green-400"}),s.jsx("span",{className:"font-mono text-green-400",children:P.command}),P.exit_code!==void 0&&s.jsxs("span",{className:`px-1.5 py-0.5 rounded text-[10px] font-medium ${P.exit_code===0?"bg-green-500/20 text-green-400":"bg-red-500/10 text-red-400"}`,children:["exit ",P.exit_code]}),P.duration!==void 0&&s.jsx("span",{className:"text-dark-500",children:P.duration<1?"<1s":`${P.duration.toFixed(1)}s`})]}),s.jsx("pre",{className:"font-mono text-xs text-green-400 whitespace-pre-wrap break-all leading-relaxed max-h-80 overflow-y-auto",children:P.content}),s.jsx("div",{className:"text-[10px] mt-2 text-dark-500",children:new Date(P.timestamp).toLocaleTimeString()})]})},X);case"system":return s.jsx("div",{className:"flex justify-center animate-fadeIn",children:s.jsx("p",{className:"text-dark-400 text-xs italic text-center px-4 py-2 max-w-[70%]",children:P.content})},X);default:return null}};return s.jsxs("div",{className:"h-[calc(100vh-80px)] flex gap-0 animate-fadeIn",children:[s.jsxs("div",{className:"w-72 flex-shrink-0 bg-dark-800 border-r border-dark-700 flex flex-col",children:[s.jsxs("div",{className:"p-4 border-b border-dark-700",children:[s.jsx("div",{className:"flex items-center justify-between mb-3",children:s.jsxs("h2",{className:"text-white font-semibold flex items-center gap-2",children:[s.jsx(Ye,{className:"w-5 h-5 text-primary-500"}),"Terminal Agent"]})}),s.jsxs("button",{onClick:()=>D(!0),className:"w-full flex items-center justify-center gap-2 px-4 py-2.5 bg-primary-500 hover:bg-primary-600 text-white text-sm font-medium rounded-lg transition-colors",children:[s.jsx(Gt,{className:"w-4 h-4"}),"New Session"]})]}),s.jsx("div",{className:"flex-1 overflow-y-auto p-3 space-y-1.5",children:r.length===0?s.jsx("p",{className:"text-dark-500 text-sm text-center py-8",children:"No sessions yet."}):r.map(P=>s.jsxs("div",{onClick:()=>Y(P.session_id),className:`p-3 rounded-lg cursor-pointer transition-all group ${l===P.session_id?"bg-primary-500/20 text-primary-500 border border-primary-500/30":"bg-dark-900/50 hover:bg-dark-900 border border-transparent hover:border-dark-700 text-dark-300"}`,children:[s.jsxs("div",{className:"flex items-center justify-between",children:[s.jsx("p",{className:"font-medium text-sm truncate flex- Line truncated
//# sourceMappingURL=index-DScaoRL2.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"index-DScaoRL2.js","sources":["../../node_modules/react/cjs/react.production.min.js","../../node_modules/react/index.js","../../node_modules/react/cjs/react-jsx-runtime.production.min.js","../../node_modules/react/jsx-runtime.js","../../node_modules/scheduler/cjs/scheduler.production.min.js","../../node_modules/scheduler/index.js","../../node_modules/react-dom/cjs/react-dom.production.min.js","../../node_modules/react-dom/index.js","../../node_modules/react-dom/client.js","../../node_modules/@remix-run/router/dist/router.js","../../node_modules/react-router/dist/index.js","../../node_modules/react-router-dom/dist/index.js","../../node_modules/lucide-react/dist/esm/defaultAttributes.js","../../node_modules/lucide-react/dist/esm/createLucideIcon.js","../../node_modules/lucide-react/dist/esm/icons/activity.js","../../node_modules/lucide-react/dist/esm/icons/alert-circle.js","../../node_modules/lucide-react/dist/esm/icons/alert-triangle.js","../../node_modules/lucide-react/dist/esm/icons/arrow-left.js","../../node_modules/lucide-react/dist/esm/icons/arrow-right-left.js","../../node_modules/lucide-react/dist/esm/icons/arrow-right.js","../../node_modules/lucide-react/dist/esm/icons/bar-chart-3.js","../../node_modules/lucide-react/dist/esm/icons/book-open.js","../../node_modules/lucide-react/dist/esm/icons/bot.js","../../node_modules/lucide-react/dist/esm/icons/box.js","../../node_modules/lucide-react/dist/esm/icons/brain.js","../../node_modules/lucide-react/dist/esm/icons/bug.js","../../node_modules/lucide-react/dist/esm/icons/calendar.js","../../node_modules/lucide-react/dist/esm/icons/check-circle-2.js","../../node_modules/lucide-react/dist/esm/icons/check-circle.js","../../node_modules/lucide-react/dist/esm/icons/check.js","../../node_modules/lucide-react/dist/esm/icons/chevron-down.js","../../node_modules/lucide-react/dist/esm/icons/chevron-right.js","../../node_modules/lucide-react/dist/esm/icons/chevron-up.js","../../node_modules/lucide-react/dist/esm/icons/circle.js","../../node_modules/lucide-react/dist/esm/icons/clock.js","../../node_modules/lucide-react/dist/esm/icons/code.js","../../node_modules/lucide-react/dist/esm/icons/container.js","../../node_modules/lucide-react/dist/esm/icons/copy.js","../../node_modules/lucide-react/dist/esm/icons/cpu.js","../../node_modules/lucide-react/dist/esm/icons/download.js","../../node_modules/lucide-react/dist/esm/icons/external-link.js","../../node_modules/lucide-react/dist/esm/icons/eye.js","../../node_modules/lucide-react/dist/esm/icons/file-text.js","../../node_modules/lucide-react/dist/esm/icons/flask-conical.js","../../node_modules/lucide-react/dist/esm/icons/globe.js","../../node_modules/lucide-react/dist/esm/icons/hard-drive.js","../../node_modules/lucide-react/dist/esm/icons/heart.js","../../node_modules/lucide-react/dist/esm/icons/home.js","../../node_modules/lucide-react/dist/esm/icons/key.js","../../node_modules/lucide-react/dist/esm/icons/layers.js","../../node_modules/lucide-react/dist/esm/icons/link.js","../../node_modules/lucide-react/dist/esm/icons/loader-2.js","../../node_modules/lucide-react/dist/esm/icons/lock.js","../../node_modules/lucide-react/dist/esm/icons/message-square.js","../../node_modules/lucide-react/dist/esm/icons/minus-circle.js","../../node_modules/lucide-react/dist/esm/icons/minus.js","../../node_modules/lucide-react/dist/esm/icons/pause.js","../../node_modules/lucide-react/dist/esm/icons/play.js","../../node_modules/lucide-react/dist/esm/icons/plus.js","../../node_modules/lucide-react/dist/esm/icons/refresh-cw.js","../../node_modules/lucide-react/dist/esm/icons/rocket.js","../../node_modules/lucide-react/dist/esm/icons/router.js","../../node_modules/lucide-react/dist/esm/icons/save.js","../../node_modules/lucide-react/dist/esm/icons/scroll-text.js","../../node_modules/lucide-react/dist/esm/icons/search.js","../../node_modules/lucide-react/dist/esm/icons/send.js","../../node_modules/lucide-react/dist/esm/icons/settings-2.js","../../node_modules/lucide-react/dist/esm/icons/settings.js","../../node_modules/lucide-react/dist/esm/icons/shield-alert.js","../../node_modules/lucide-react/dist/esm/icons/shield.js","../../node_modules/lucide-react/dist/esm/icons/skip-forward.js","../../node_modules/lucide-react/dist/esm/icons/sparkles.js","../../node_modules/lucide-react/dist/esm/icons/square.js","../../node_modules/lucide-react/dist/esm/icons/stop-circle.js","../../node_modules/lucide-react/dist/esm/icons/tag.js","../../node_modules/lucide-react/dist/esm/icons/target.js","../../node_modules/lucide-react/dist/esm/icons/terminal.js","../../node_modules/lucide-react/dist/esm/icons/timer.js","../../node_modules/lucide-react/dist/esm/icons/trash-2.js","../../node_modules/lucide-react/dist/esm/icons/upload.js","../../node_modules/lucide-react/dist/esm/icons/wifi.js","../../node_modules/lucide-react/dist/esm/icons/wrench.js","../../node_modules/lucide-react/dist/esm/icons/x-circle.js","../../node_modules/lucide-react/dist/esmLine truncated
+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<circle cx="50" cy="50" r="45" fill="#1a1a2e" stroke="#e94560" stroke-width="3"/>
<path d="M30 50 L45 35 L45 45 L70 45 L70 55 L45 55 L45 65 Z" fill="#e94560"/>
<circle cx="50" cy="50" r="8" fill="none" stroke="#e94560" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 314 B

+14
View File
@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>NeuroSploit v3 - AI-Powered Penetration Testing</title>
<script type="module" crossorigin src="/assets/index-DScaoRL2.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CjxVs3nK.css">
</head>
<body>
<div id="root"></div>
</body>
</html>
+3467
View File
File diff suppressed because it is too large. Load diff
+10
View File
@@ -9,17 +9,27 @@ import RealtimeTaskPage from './pages/RealtimeTaskPage'
import ReportsPage from './pages/ReportsPage'
import ReportViewPage from './pages/ReportViewPage'
import SettingsPage from './pages/SettingsPage'
import SchedulerPage from './pages/SchedulerPage'
import AutoPentestPage from './pages/AutoPentestPage'
import VulnLabPage from './pages/VulnLabPage'
import TerminalAgentPage from './pages/TerminalAgentPage'
import SandboxDashboardPage from './pages/SandboxDashboardPage'
function App() {
return (
<Layout>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/auto" element={<AutoPentestPage />} />
<Route path="/vuln-lab" element={<VulnLabPage />} />
<Route path="/terminal" element={<TerminalAgentPage />} />
<Route path="/scan/new" element={<NewScanPage />} />
<Route path="/scan/:scanId" element={<ScanDetailsPage />} />
<Route path="/agent/:agentId" element={<AgentStatusPage />} />
<Route path="/tasks" element={<TaskLibraryPage />} />
<Route path="/realtime" element={<RealtimeTaskPage />} />
<Route path="/scheduler" element={<SchedulerPage />} />
<Route path="/sandboxes" element={<SandboxDashboardPage />} />
<Route path="/reports" element={<ReportsPage />} />
<Route path="/reports/:reportId" element={<ReportViewPage />} />
<Route path="/settings" element={<SettingsPage />} />
+11 -1
View File
@@ -7,14 +7,24 @@ import {
Settings,
Activity,
Shield,
Zap
Zap,
Clock,
Rocket,
FlaskConical,
Terminal,
Container
} from 'lucide-react'
const navItems = [
{ path: '/', icon: Home, label: 'Dashboard' },
{ path: '/auto', icon: Rocket, label: 'Auto Pentest' },
{ path: '/vuln-lab', icon: FlaskConical, label: 'Vuln Lab' },
{ path: '/terminal', icon: Terminal, label: 'Terminal Agent' },
{ path: '/sandboxes', icon: Container, label: 'Sandboxes' },
{ path: '/scan/new', icon: Bot, label: 'AI Agent' },
{ path: '/realtime', icon: Zap, label: 'Real-time Task' },
{ path: '/tasks', icon: BookOpen, label: 'Task Library' },
{ path: '/scheduler', icon: Clock, label: 'Scheduler' },
{ path: '/reports', icon: FileText, label: 'Reports' },
{ path: '/settings', icon: Settings, label: 'Settings' },
]
+151 -20
View File
@@ -3,7 +3,8 @@ import { useParams, useNavigate } from 'react-router-dom'
import {
Bot, RefreshCw, FileText, CheckCircle,
XCircle, Clock, Target, Shield, ChevronDown, ChevronRight, ExternalLink,
Copy, Download, StopCircle, Terminal, Brain, Send, Code, Globe, AlertTriangle
Copy, Download, StopCircle, Terminal, Brain, Send, Code, Globe, AlertTriangle,
SkipForward, MinusCircle, Pause, Play
} from 'lucide-react'
import Card from '../components/common/Card'
import Button from '../components/common/Button'
@@ -77,6 +78,11 @@ export default function AgentStatusPage() {
const [isSubmittingPrompt, setIsSubmittingPrompt] = useState(false)
const [promptSentMessage, setPromptSentMessage] = useState<string | null>(null)
// Phase skip state
const [skipConfirm, setSkipConfirm] = useState<string | null>(null)
const [isSkipping, setIsSkipping] = useState(false)
const [skippedPhases, setSkippedPhases] = useState<Set<string>>(new Set())
// Separate logs by source
const scriptLogs = logs.filter(l => l.source === 'script' || (!l.source && !l.message.includes('[LLM]') && !l.message.includes('[AI]')))
const llmLogs = logs.filter(l => l.source === 'llm' || l.message.includes('[LLM]') || l.message.includes('[AI]'))
@@ -107,12 +113,12 @@ export default function AgentStatusPage() {
fetchStatus()
// Poll every 2 seconds while running
// Poll every 5 seconds while running or paused
const interval = setInterval(() => {
if (status?.status === 'running') {
if (status?.status === 'running' || status?.status === 'paused') {
fetchStatus()
}
}, 2000)
}, 5000)
return () => clearInterval(interval)
}, [agentId, status?.status])
@@ -466,6 +472,28 @@ export default function AgentStatusPage() {
}
}
const handlePauseScan = async () => {
if (!agentId) return
try {
await agentApi.pause(agentId)
const statusData = await agentApi.getStatus(agentId)
setStatus(statusData)
} catch (err: any) {
console.error('Failed to pause agent:', err)
}
}
const handleResumeScan = async () => {
if (!agentId) return
try {
await agentApi.resume(agentId)
const statusData = await agentApi.getStatus(agentId)
setStatus(statusData)
} catch (err: any) {
console.error('Failed to resume agent:', err)
}
}
const handleSubmitPrompt = async () => {
if (!customPrompt.trim() || !agentId) return
setIsSubmittingPrompt(true)
@@ -496,6 +524,37 @@ export default function AgentStatusPage() {
}
}
const handleSkipToPhase = async (targetPhase: string) => {
if (!agentId) return
setIsSkipping(true)
try {
await agentApi.skipToPhase(agentId, targetPhase)
// Mark intermediate phases as skipped
const currentIndex = status ? getPhaseIndex(status.phase) : 0
const targetIndex = SCAN_PHASES.findIndex(p => p.key === targetPhase)
const newSkipped = new Set(skippedPhases)
for (let i = currentIndex; i < targetIndex; i++) {
newSkipped.add(SCAN_PHASES[i].key)
}
setSkippedPhases(newSkipped)
setSkipConfirm(null)
} catch (err: any) {
console.error('Failed to skip phase:', err)
} finally {
setIsSkipping(false)
}
}
// Track skipped phases from status updates
useEffect(() => {
if (!status) return
const phase = status.phase.toLowerCase()
if (phase.includes('_skipped')) {
const skippedKey = phase.replace('_skipped', '')
setSkippedPhases(prev => new Set(prev).add(skippedKey))
}
}, [status?.phase])
const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text)
}
@@ -790,7 +849,8 @@ export default function AgentStatusPage() {
<span className={`px-3 py-1 rounded-full text-sm font-medium flex items-center gap-1 ${
status.status === 'running' ? 'bg-blue-500/20 text-blue-400' :
status.status === 'completed' ? 'bg-green-500/20 text-green-400' :
status.status === 'stopped' ? 'bg-yellow-500/20 text-yellow-400' :
status.status === 'paused' ? 'bg-yellow-500/20 text-yellow-400' :
status.status === 'stopped' ? 'bg-orange-500/20 text-orange-400' :
'bg-red-500/20 text-red-400'
}`}>
{PHASE_ICONS[status.status]}
@@ -802,10 +862,28 @@ export default function AgentStatusPage() {
</div>
<div className="flex gap-2">
{status.status === 'running' && (
<Button variant="danger" onClick={handleStopScan} isLoading={isStopping}>
<StopCircle className="w-4 h-4 mr-2" />
Stop Scan
</Button>
<>
<Button variant="secondary" onClick={handlePauseScan}>
<Pause className="w-4 h-4 mr-2" />
Pause
</Button>
<Button variant="danger" onClick={handleStopScan} isLoading={isStopping}>
<StopCircle className="w-4 h-4 mr-2" />
Stop
</Button>
</>
)}
{status.status === 'paused' && (
<>
<Button variant="primary" onClick={handleResumeScan}>
<Play className="w-4 h-4 mr-2" />
Resume
</Button>
<Button variant="danger" onClick={handleStopScan} isLoading={isStopping}>
<StopCircle className="w-4 h-4 mr-2" />
Stop
</Button>
</>
)}
{status.scan_id && (
<Button variant="secondary" onClick={() => navigate(`/scan/${status.scan_id}`)}>
@@ -833,31 +911,84 @@ export default function AgentStatusPage() {
{(status.status === 'running' || status.status === 'completed' || status.status === 'stopped') && (
<Card>
<div className="space-y-4">
{/* Phase Steps */}
{/* Phase Steps with Skip */}
<div className="flex items-center justify-between px-2">
{SCAN_PHASES.map((phase, index) => {
const currentIndex = status.status === 'completed' ? 4 : status.status === 'stopped' ? getPhaseIndex(status.phase) : getPhaseIndex(status.phase)
const isActive = index === currentIndex
const isCompleted = index < currentIndex || status.status === 'completed'
const isStopped = status.status === 'stopped' && index > currentIndex
const isSkipped = skippedPhases.has(phase.key)
const canSkipTo = (status.status === 'running' || status.status === 'paused') && index > currentIndex && phase.key !== 'completed'
return (
<div key={phase.key} className="flex flex-col items-center flex-1">
<div className={`w-8 h-8 rounded-full flex items-center justify-center mb-1 transition-all ${
isCompleted ? 'bg-green-500 text-white' :
isActive ? 'bg-primary-500 text-white animate-pulse' :
isStopped ? 'bg-yellow-500/20 text-yellow-500' :
'bg-dark-700 text-dark-400'
}`}>
{isCompleted ? <CheckCircle className="w-4 h-4" /> :
<div key={phase.key} className="flex flex-col items-center flex-1 relative group">
{/* Connector line */}
{index > 0 && (
<div className={`absolute top-4 right-1/2 w-full h-0.5 -translate-y-1/2 z-0 ${
isCompleted || isActive ? 'bg-green-500/50' :
isSkipped ? 'bg-yellow-500/30' :
'bg-dark-700'
}`} />
)}
{/* Phase node */}
<div
className={`relative z-10 w-8 h-8 rounded-full flex items-center justify-center mb-1 transition-all ${
isSkipped ? 'bg-yellow-500/20 text-yellow-500 ring-2 ring-yellow-500/30' :
isCompleted ? 'bg-green-500 text-white' :
isActive ? 'bg-primary-500 text-white animate-pulse ring-2 ring-primary-500/30' :
isStopped ? 'bg-yellow-500/20 text-yellow-500' :
canSkipTo ? 'bg-dark-700 text-dark-400 cursor-pointer hover:bg-primary-500/20 hover:text-primary-400 hover:ring-2 hover:ring-primary-500/30' :
'bg-dark-700 text-dark-400'
}`}
onClick={() => canSkipTo && setSkipConfirm(phase.key)}
>
{isSkipped ? <MinusCircle className="w-4 h-4" /> :
isCompleted ? <CheckCircle className="w-4 h-4" /> :
isActive ? (PHASE_ICONS[phase.key === 'recon' ? 'reconnaissance' : phase.key] || <span className="text-xs font-bold">{index + 1}</span>) :
isStopped ? <StopCircle className="w-4 h-4" /> :
canSkipTo ? <SkipForward className="w-3.5 h-3.5" /> :
<span className="text-xs font-bold">{index + 1}</span>}
</div>
<span className={`text-xs text-center ${
isCompleted || isActive ? 'text-white' : 'text-dark-500'
isSkipped ? 'text-yellow-500' :
isCompleted || isActive ? 'text-white' :
canSkipTo ? 'text-dark-400 group-hover:text-primary-400' :
'text-dark-500'
}`}>
{phase.label}
{isSkipped ? `${phase.label} (skipped)` : phase.label}
</span>
{/* Skip tooltip on hover */}
{canSkipTo && (
<div className="absolute -top-8 left-1/2 -translate-x-1/2 bg-dark-800 text-primary-400 text-[10px] px-2 py-0.5 rounded whitespace-nowrap opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none border border-dark-600">
Skip to {phase.label}
</div>
)}
{/* Inline skip confirmation */}
{skipConfirm === phase.key && (
<div className="absolute top-10 left-1/2 -translate-x-1/2 z-20 bg-dark-800 border border-dark-600 rounded-lg p-3 shadow-xl whitespace-nowrap">
<p className="text-xs text-dark-300 mb-2">Skip to <span className="text-white font-medium">{phase.label}</span>?</p>
<div className="flex gap-2">
<button
onClick={() => handleSkipToPhase(phase.key)}
disabled={isSkipping}
className="px-3 py-1 bg-primary-500 text-white text-xs rounded hover:bg-primary-600 disabled:opacity-50"
>
{isSkipping ? 'Skipping...' : 'Confirm'}
</button>
<button
onClick={() => setSkipConfirm(null)}
className="px-3 py-1 bg-dark-700 text-dark-300 text-xs rounded hover:bg-dark-600"
>
Cancel
</button>
</div>
</div>
)}
</div>
)
})}
+929
View File
@@ -0,0 +1,929 @@
import { useState, useEffect, useRef, useCallback } from 'react'
import { useNavigate } from 'react-router-dom'
import {
Rocket, Shield, ChevronDown, ChevronUp, Loader2,
AlertTriangle, CheckCircle2, Globe, Lock, Bug, MessageSquare,
FileText, ScrollText, X, ExternalLink, Download, Sparkles, Trash2,
Brain, Wrench, Layers
} from 'lucide-react'
import { agentApi, reportsApi } from '../services/api'
import type { AgentStatus, AgentFinding, AgentLog } from '../types'
const PHASES = [
{ key: 'parallel', label: 'Parallel Streams', icon: Layers, range: [0, 50] as const },
{ key: 'deep', label: 'Deep Analysis', icon: Brain, range: [50, 75] as const },
{ key: 'final', label: 'Finalization', icon: Shield, range: [75, 100] as const },
]
const STREAMS = [
{ key: 'recon', label: 'Recon', icon: Globe, color: 'blue', activeUntil: 25 },
{ key: 'junior', label: 'Junior AI', icon: Brain, color: 'purple', activeUntil: 35 },
{ key: 'tools', label: 'Tools', icon: Wrench, color: 'orange', activeUntil: 50 },
] as const
const STREAM_COLORS: Record<string, { bg: string; text: string; border: string; pulse: string }> = {
blue: { bg: 'bg-blue-500/20', text: 'text-blue-400', border: 'border-blue-500/40', pulse: 'bg-blue-400' },
purple: { bg: 'bg-purple-500/20', text: 'text-purple-400', border: 'border-purple-500/40', pulse: 'bg-purple-400' },
orange: { bg: 'bg-orange-500/20', text: 'text-orange-400', border: 'border-orange-500/40', pulse: 'bg-orange-400' },
}
function phaseFromProgress(progress: number): number {
if (progress < 50) return 0
if (progress < 75) return 1
return 2
}
function StreamBadge({ stream, progress, isRunning }: {
stream: typeof STREAMS[number]
progress: number
isRunning: boolean
}) {
const active = isRunning && progress < stream.activeUntil
const done = progress >= stream.activeUntil
const colors = STREAM_COLORS[stream.color]
const Icon = stream.icon
return (
<div
className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full border text-xs font-medium transition-all duration-300 ${
active
? `${colors.bg} ${colors.text} ${colors.border}`
: done
? 'bg-dark-700/50 text-dark-400 border-dark-600'
: 'bg-dark-900 text-dark-500 border-dark-700'
}`}
>
{active && (
<span className="relative flex h-2 w-2">
<span className={`animate-ping absolute inline-flex h-full w-full rounded-full ${colors.pulse} opacity-75`} />
<span className={`relative inline-flex rounded-full h-2 w-2 ${colors.pulse}`} />
</span>
)}
{done && <CheckCircle2 className="w-3 h-3 text-green-500" />}
{!active && !done && <Icon className="w-3 h-3" />}
<span>{stream.label}</span>
</div>
)
}
function logMessageColor(message: string): string {
if (message.startsWith('[STREAM 1]')) return 'text-blue-400'
if (message.startsWith('[STREAM 2]')) return 'text-purple-400'
if (message.startsWith('[STREAM 3]')) return 'text-orange-400'
if (message.startsWith('[TOOL]')) return 'text-orange-300'
if (message.startsWith('[DEEP]')) return 'text-cyan-400'
if (message.startsWith('[FINAL]')) return 'text-green-400'
return ''
}
const SEVERITY_COLORS: Record<string, string> = {
critical: 'bg-red-500',
high: 'bg-orange-500',
medium: 'bg-yellow-500',
low: 'bg-blue-500',
info: 'bg-gray-500',
}
const SEVERITY_BORDER: Record<string, string> = {
critical: 'border-red-500/40',
high: 'border-orange-500/40',
medium: 'border-yellow-500/40',
low: 'border-blue-500/40',
info: 'border-gray-500/40',
}
// Resolve a confidence score from the various possible sources on a finding.
function getConfidenceDisplay(finding: { confidence_score?: number; confidence?: string }): { score: number; color: string; label: string } | null {
let score: number | null = null
if (typeof finding.confidence_score === 'number') {
score = finding.confidence_score
} else if (finding.confidence) {
const parsed = Number(finding.confidence)
if (!isNaN(parsed)) {
score = parsed
} else {
const map: Record<string, number> = { high: 90, medium: 60, low: 30 }
score = map[finding.confidence.toLowerCase()] ?? null
}
}
if (score === null) return null
const color = score >= 90 ? 'green' : score >= 60 ? 'yellow' : 'red'
const label = score >= 90 ? 'Confirmed' : score >= 60 ? 'Likely' : 'Low'
return { score, color, label }
}
const CONFIDENCE_STYLES: Record<string, string> = {
green: 'bg-green-500/15 text-green-400 border-green-500/30',
yellow: 'bg-yellow-500/15 text-yellow-400 border-yellow-500/30',
red: 'bg-red-500/15 text-red-400 border-red-500/30',
}
const SESSION_KEY = 'neurosploit_autopentest_session'
interface SavedSession {
agentId: string
target: string
startedAt: string
}
export default function AutoPentestPage() {
const navigate = useNavigate()
const [target, setTarget] = useState('')
const [multiTarget, setMultiTarget] = useState(false)
const [targets, setTargets] = useState('')
const [subdomainDiscovery, setSubdomainDiscovery] = useState(false)
const [showAuth, setShowAuth] = useState(false)
const [authType, setAuthType] = useState<string>('')
const [authValue, setAuthValue] = useState('')
const [showPrompt, setShowPrompt] = useState(false)
const [customPrompt, setCustomPrompt] = useState('')
// Running state
const [isRunning, setIsRunning] = useState(false)
const [agentId, setAgentId] = useState<string | null>(null)
const [status, setStatus] = useState<AgentStatus | null>(null)
const [error, setError] = useState<string | null>(null)
// Live findings & logs
const [showFindings, setShowFindings] = useState(true)
const [showLogs, setShowLogs] = useState(false)
const [logs, setLogs] = useState<AgentLog[]>([])
const [expandedFinding, setExpandedFinding] = useState<string | null>(null)
// Report generation
const [generatingReport, setGeneratingReport] = useState(false)
const [reportId, setReportId] = useState<string | null>(null)
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null)
const logsEndRef = useRef<HTMLDivElement>(null)
// Restore session on mount
useEffect(() => {
try {
const saved = localStorage.getItem(SESSION_KEY)
if (saved) {
const session: SavedSession = JSON.parse(saved)
setAgentId(session.agentId)
setTarget(session.target)
setIsRunning(true)
}
} catch {
// ignore parse errors
}
}, [])
// Save session when agentId changes
useEffect(() => {
if (agentId && isRunning) {
const session: SavedSession = {
agentId,
target: target || '',
startedAt: new Date().toISOString(),
}
localStorage.setItem(SESSION_KEY, JSON.stringify(session))
}
}, [agentId, isRunning, target])
// Poll agent status + logs
useEffect(() => {
if (!agentId) return
const poll = async () => {
try {
const s = await agentApi.getStatus(agentId)
setStatus(s)
if (s.status === 'completed' || s.status === 'error' || s.status === 'stopped') {
setIsRunning(false)
if (pollRef.current) clearInterval(pollRef.current)
}
} catch {
// ignore polling errors
}
// Fetch recent logs
try {
const logData = await agentApi.getLogs(agentId, 200)
setLogs(logData.logs || [])
} catch {
// ignore
}
}
poll()
pollRef.current = setInterval(poll, 3000)
return () => { if (pollRef.current) clearInterval(pollRef.current) }
}, [agentId])
// Auto-scroll logs
useEffect(() => {
if (showLogs && logsEndRef.current) {
logsEndRef.current.scrollIntoView({ behavior: 'smooth' })
}
}, [logs, showLogs])
const handleStart = async () => {
const primaryTarget = target.trim()
if (!primaryTarget) return
setError(null)
setIsRunning(true)
setStatus(null)
setLogs([])
setReportId(null)
try {
const targetList = multiTarget
? targets.split('\n').map(t => t.trim()).filter(Boolean)
: undefined
const resp = await agentApi.autoPentest(primaryTarget, {
subdomain_discovery: subdomainDiscovery,
targets: targetList,
auth_type: authType || undefined,
auth_value: authValue || undefined,
prompt: customPrompt.trim() || undefined,
})
setAgentId(resp.agent_id)
} catch (err: any) {
setError(err?.response?.data?.detail || err?.message || 'Failed to start pentest')
setIsRunning(false)
}
}
const handleStop = async () => {
if (!agentId) return
try {
await agentApi.stop(agentId)
setIsRunning(false)
} catch {
// ignore
}
}
const handleClearSession = () => {
localStorage.removeItem(SESSION_KEY)
setAgentId(null)
setStatus(null)
setLogs([])
setIsRunning(false)
setError(null)
setReportId(null)
setTarget('')
}
const handleGenerateAiReport = useCallback(async () => {
if (!status?.scan_id) return
setGeneratingReport(true)
try {
const report = await reportsApi.generateAiReport({
scan_id: status.scan_id,
title: `AI Pentest Report - ${target}`,
})
setReportId(report.id)
} catch (err: any) {
setError(err?.response?.data?.detail || 'Failed to generate AI report')
} finally {
setGeneratingReport(false)
}
}, [status?.scan_id, target])
const currentPhaseIdx = status ? phaseFromProgress(status.progress) : -1
const findings = status?.findings || []
const rejectedFindings = status?.rejected_findings || []
const allFindings = [...findings, ...rejectedFindings]
const [findingsFilter, setFindingsFilter] = useState<'confirmed' | 'rejected' | 'all'>('all')
const displayFindings = findingsFilter === 'confirmed' ? findings
: findingsFilter === 'rejected' ? rejectedFindings
: allFindings
const sevCounts = findings.reduce(
(acc, f) => {
acc[f.severity] = (acc[f.severity] || 0) + 1
return acc
},
{} as Record<string, number>
)
return (
<div className="min-h-screen flex flex-col items-center py-12 px-4">
{/* Header */}
<div className="text-center mb-10">
<div className="inline-flex items-center justify-center w-16 h-16 bg-green-500/20 rounded-2xl mb-4">
<Rocket className="w-8 h-8 text-green-400" />
</div>
<h1 className="text-3xl font-bold text-white mb-2">Auto Pentest</h1>
<p className="text-dark-400 max-w-md">
One-click comprehensive penetration test. 100 vulnerability types, AI-powered analysis, full report.
</p>
</div>
{/* Main Card - only show config when no session is active */}
{!agentId && (
<div className="w-full max-w-2xl bg-dark-800 border border-dark-700 rounded-2xl p-8">
{/* URL Input */}
<div className="mb-6">
<label className="block text-sm font-medium text-dark-300 mb-2">Target URL</label>
<input
type="url"
value={target}
onChange={e => setTarget(e.target.value)}
placeholder="https://example.com"
disabled={isRunning}
className="w-full px-4 py-4 bg-dark-900 border border-dark-600 rounded-xl text-white text-lg placeholder-dark-500 focus:outline-none focus:border-green-500 focus:ring-1 focus:ring-green-500 disabled:opacity-50"
/>
</div>
{/* Toggles */}
<div className="flex flex-wrap gap-4 mb-6">
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={subdomainDiscovery}
onChange={e => setSubdomainDiscovery(e.target.checked)}
disabled={isRunning}
className="w-4 h-4 rounded bg-dark-900 border-dark-600 text-green-500 focus:ring-green-500"
/>
<span className="text-sm text-dark-300">Subdomain Discovery</span>
</label>
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={multiTarget}
onChange={e => setMultiTarget(e.target.checked)}
disabled={isRunning}
className="w-4 h-4 rounded bg-dark-900 border-dark-600 text-green-500 focus:ring-green-500"
/>
<span className="text-sm text-dark-300">Multiple Targets</span>
</label>
</div>
{/* Multi-target textarea */}
{multiTarget && (
<div className="mb-6">
<label className="block text-sm font-medium text-dark-300 mb-2">Additional Targets (one per line)</label>
<textarea
value={targets}
onChange={e => setTargets(e.target.value)}
rows={4}
disabled={isRunning}
placeholder={"https://api.example.com\nhttps://admin.example.com"}
className="w-full px-4 py-3 bg-dark-900 border border-dark-600 rounded-xl text-white placeholder-dark-500 focus:outline-none focus:border-green-500 disabled:opacity-50"
/>
</div>
)}
{/* Auth Section (collapsible) */}
<div className="mb-6">
<button
onClick={() => setShowAuth(!showAuth)}
disabled={isRunning}
className="flex items-center gap-2 text-sm text-dark-400 hover:text-white transition-colors disabled:opacity-50"
>
<Lock className="w-4 h-4" />
<span>Authentication (Optional)</span>
{showAuth ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />}
</button>
{showAuth && (
<div className="mt-3 space-y-3 pl-6">
<select
value={authType}
onChange={e => setAuthType(e.target.value)}
disabled={isRunning}
className="w-full px-3 py-2 bg-dark-900 border border-dark-600 rounded-lg text-white text-sm focus:outline-none focus:border-green-500"
>
<option value="">No Authentication</option>
<option value="bearer">Bearer Token</option>
<option value="cookie">Cookie</option>
<option value="basic">Basic Auth (user:pass)</option>
<option value="header">Custom Header (Name:Value)</option>
</select>
{authType && (
<input
type="text"
value={authValue}
onChange={e => setAuthValue(e.target.value)}
disabled={isRunning}
placeholder={
authType === 'bearer' ? 'eyJhbGciOiJIUzI1NiIs...' :
authType === 'cookie' ? 'session=abc123; token=xyz' :
authType === 'basic' ? 'admin:password123' :
'X-API-Key:your-api-key'
}
className="w-full px-3 py-2 bg-dark-900 border border-dark-600 rounded-lg text-white text-sm placeholder-dark-500 focus:outline-none focus:border-green-500"
/>
)}
</div>
)}
</div>
{/* Custom AI Prompt (collapsible) */}
<div className="mb-6">
<button
onClick={() => setShowPrompt(!showPrompt)}
disabled={isRunning}
className="flex items-center gap-2 text-sm text-dark-400 hover:text-white transition-colors disabled:opacity-50"
>
<MessageSquare className="w-4 h-4" />
<span>Custom AI Prompt (Optional)</span>
{showPrompt ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />}
</button>
{showPrompt && (
<div className="mt-3 pl-6">
<textarea
value={customPrompt}
onChange={e => setCustomPrompt(e.target.value)}
rows={4}
disabled={isRunning}
placeholder={"Focus on authentication bypass and IDOR vulnerabilities.\nThe app uses JWT tokens in localStorage.\nAdmin panel at /admin requires role=admin cookie."}
className="w-full px-4 py-3 bg-dark-900 border border-dark-600 rounded-xl text-white text-sm placeholder-dark-500 focus:outline-none focus:border-green-500 disabled:opacity-50"
/>
<p className="mt-1 text-xs text-dark-500">
Guide the AI agent with additional context, focus areas, or specific instructions.
</p>
</div>
)}
</div>
{/* Error */}
{error && (
<div className="mb-6 p-3 bg-red-500/10 border border-red-500/20 rounded-lg flex items-center gap-2">
<AlertTriangle className="w-5 h-5 text-red-400" />
<span className="text-red-400 text-sm">{error}</span>
</div>
)}
{/* Start Button */}
<button
onClick={handleStart}
disabled={!target.trim()}
className="w-full py-4 bg-green-500 hover:bg-green-600 disabled:bg-dark-600 disabled:text-dark-400 text-white font-bold text-lg rounded-xl transition-colors flex items-center justify-center gap-3"
>
<Rocket className="w-6 h-6" />
START PENTEST
</button>
</div>
)}
{/* Active Session View */}
{agentId && (
<div className="w-full max-w-4xl">
{/* Session Header */}
<div className="bg-dark-800 border border-dark-700 rounded-2xl p-6 mb-6">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-3">
<div className={`w-3 h-3 rounded-full ${isRunning ? 'bg-green-500 animate-pulse' : status?.status === 'completed' ? 'bg-green-500' : status?.status === 'error' ? 'bg-red-500' : 'bg-gray-500'}`} />
<h3 className="text-white font-semibold">
{isRunning ? 'Pentest Running' : status?.status === 'completed' ? 'Pentest Complete' : status?.status === 'error' ? 'Pentest Failed' : 'Pentest Stopped'}
</h3>
<span className="text-dark-400 text-sm">{target}</span>
</div>
<div className="flex items-center gap-2">
{isRunning && (
<button
onClick={handleStop}
className="px-4 py-1.5 bg-red-500/20 hover:bg-red-500/30 text-red-400 rounded-lg text-sm transition-colors flex items-center gap-1.5"
>
<X className="w-4 h-4" /> Stop
</button>
)}
{!isRunning && (
<button
onClick={handleClearSession}
className="px-4 py-1.5 bg-dark-700 hover:bg-dark-600 text-dark-300 rounded-lg text-sm transition-colors flex items-center gap-1.5"
>
<Trash2 className="w-4 h-4" /> Clear Session
</button>
)}
</div>
</div>
{/* Progress Bar */}
{status && (
<>
<div className="flex items-center justify-between text-sm mb-2">
<span className="text-dark-400">{status.phase || 'Initializing...'}</span>
<span className="text-dark-400">{status.progress}%</span>
</div>
<div className="w-full bg-dark-900 rounded-full h-2 mb-4">
<div
className="bg-green-500 h-2 rounded-full transition-all duration-500"
style={{ width: `${status.progress}%` }}
/>
</div>
{/* Phase Indicators */}
<div className="grid grid-cols-3 gap-3">
{PHASES.map((phase, idx) => {
const Icon = phase.icon
const isActive = idx === currentPhaseIdx && isRunning
const isDone = idx < currentPhaseIdx || status.status === 'completed'
return (
<div
key={phase.key}
className={`rounded-xl p-3 border transition-all duration-300 ${
isActive ? 'bg-green-500/10 border-green-500/30' :
isDone ? 'bg-dark-700/50 border-dark-600' :
'bg-dark-900 border-dark-700'
}`}
>
<div className="flex items-center gap-2 mb-1">
{isActive ? (
<Loader2 className="w-4 h-4 animate-spin text-green-400 flex-shrink-0" />
) : isDone ? (
<CheckCircle2 className="w-4 h-4 text-green-500 flex-shrink-0" />
) : (
<Icon className={`w-4 h-4 flex-shrink-0 ${isActive ? 'text-green-400' : 'text-dark-500'}`} />
)}
<span className={`text-xs font-medium ${
isActive ? 'text-green-400' :
isDone ? 'text-dark-300' :
'text-dark-500'
}`}>
{phase.label}
</span>
<span className={`ml-auto text-[10px] ${
isActive ? 'text-green-500/60' :
isDone ? 'text-dark-500' :
'text-dark-600'
}`}>
{phase.range[0]}-{phase.range[1]}%
</span>
</div>
{/* Stream badges for the parallel phase */}
{idx === 0 && (
<div className="flex flex-wrap gap-1.5 mt-2">
{STREAMS.map(stream => (
<StreamBadge
key={stream.key}
stream={stream}
progress={status.progress}
isRunning={isRunning}
/>
))}
</div>
)}
</div>
)
})}
</div>
</>
)}
</div>
{/* Severity Summary + Tabs */}
{findings.length > 0 && (
<div className="bg-dark-800 border border-dark-700 rounded-2xl p-6 mb-6">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-3">
<h3 className="text-white font-semibold">Findings ({findings.length})</h3>
<div className="flex gap-2">
{['critical', 'high', 'medium', 'low', 'info'].map(sev => {
const count = sevCounts[sev] || 0
if (count === 0) return null
return (
<span key={sev} className={`${SEVERITY_COLORS[sev]} text-white px-2 py-0.5 rounded-full text-xs font-bold`}>
{count}
</span>
)
})}
</div>
</div>
<div className="flex gap-2">
<button
onClick={() => { setShowFindings(true); setShowLogs(false) }}
className={`px-3 py-1 rounded-lg text-xs transition-colors ${showFindings ? 'bg-primary-500 text-white' : 'bg-dark-700 text-dark-400 hover:text-white'}`}
>
<Bug className="w-3 h-3 inline mr-1" />Findings ({findings.length})
</button>
{rejectedFindings.length > 0 && (
<button
onClick={() => { setShowFindings(true); setShowLogs(false); setFindingsFilter(findingsFilter === 'rejected' ? 'all' : 'rejected') }}
className={`px-3 py-1 rounded-lg text-xs transition-colors ${findingsFilter === 'rejected' && showFindings ? 'bg-orange-500/30 text-orange-400' : 'bg-dark-700 text-orange-400/60 hover:text-orange-400'}`}
>
<AlertTriangle className="w-3 h-3 inline mr-1" />Rejected ({rejectedFindings.length})
</button>
)}
<button
onClick={() => { setShowLogs(true); setShowFindings(false) }}
className={`px-3 py-1 rounded-lg text-xs transition-colors ${showLogs ? 'bg-primary-500 text-white' : 'bg-dark-700 text-dark-400 hover:text-white'}`}
>
<ScrollText className="w-3 h-3 inline mr-1" />Activity Log
</button>
</div>
</div>
{/* Findings Filter Tabs */}
{showFindings && allFindings.length > 0 && rejectedFindings.length > 0 && (
<div className="flex gap-1 mb-2">
{(['all', 'confirmed', 'rejected'] as const).map(f => (
<button
key={f}
onClick={() => setFindingsFilter(f)}
className={`px-2 py-0.5 rounded-full text-xs transition-colors ${
findingsFilter === f ? 'bg-primary-500/20 text-primary-400' : 'text-dark-500 hover:text-dark-300'
}`}
>
{f === 'all' ? `All (${allFindings.length})` : f === 'confirmed' ? `Confirmed (${findings.length})` : `Rejected (${rejectedFindings.length})`}
</button>
))}
</div>
)}
{/* Findings List */}
{showFindings && (
<div className="space-y-2 max-h-[500px] overflow-y-auto pr-1">
{displayFindings.map((f: AgentFinding) => (
<div
key={f.id}
className={`border ${f.ai_status === 'rejected' ? 'border-orange-500/30 opacity-70' : (SEVERITY_BORDER[f.severity] || 'border-dark-600')} rounded-xl bg-dark-900 overflow-hidden`}
>
<button
onClick={() => setExpandedFinding(expandedFinding === f.id ? null : f.id)}
className="w-full flex items-center gap-3 p-3 text-left hover:bg-dark-800/50 transition-colors"
>
<span className={`${SEVERITY_COLORS[f.severity]} text-white px-2 py-0.5 rounded text-xs font-bold uppercase flex-shrink-0`}>
{f.severity}
</span>
<span className={`text-sm flex-1 truncate ${f.ai_status === 'rejected' ? 'text-dark-400' : 'text-white'}`}>{f.title}</span>
{f.ai_status === 'rejected' && (
<span className="text-xs px-1.5 py-0.5 rounded-full bg-orange-500/20 text-orange-400 flex-shrink-0">Rejected</span>
)}
{(() => {
const conf = getConfidenceDisplay(f)
if (!conf) return null
return (
<span className={`text-xs font-semibold px-1.5 py-0.5 rounded-full border flex-shrink-0 ${CONFIDENCE_STYLES[conf.color]}`}>
{conf.score}/100
</span>
)
})()}
<span className="text-dark-500 text-xs flex-shrink-0">{f.vulnerability_type}</span>
{expandedFinding === f.id ? <ChevronUp className="w-4 h-4 text-dark-400" /> : <ChevronDown className="w-4 h-4 text-dark-400" />}
</button>
{expandedFinding === f.id && (
<div className="px-3 pb-3 space-y-2 border-t border-dark-700 pt-2">
<div className="grid grid-cols-2 gap-2 text-xs">
{f.affected_endpoint && (
<div>
<span className="text-dark-500">Endpoint: </span>
<span className="text-dark-300 break-all">{f.affected_endpoint}</span>
</div>
)}
{f.parameter && (
<div>
<span className="text-dark-500">Parameter: </span>
<span className="text-dark-300">{f.parameter}</span>
</div>
)}
{f.cwe_id && (
<div>
<span className="text-dark-500">CWE: </span>
<span className="text-dark-300">{f.cwe_id}</span>
</div>
)}
{f.cvss_score > 0 && (
<div>
<span className="text-dark-500">CVSS: </span>
<span className="text-dark-300">{f.cvss_score}</span>
</div>
)}
</div>
{f.description && (
<p className="text-dark-400 text-xs">{f.description.substring(0, 300)}{f.description.length > 300 ? '...' : ''}</p>
)}
{f.payload && (
<div className="bg-dark-800 rounded-lg p-2">
<span className="text-dark-500 text-xs">Payload: </span>
<code className="text-green-400 text-xs break-all">{f.payload.substring(0, 200)}</code>
</div>
)}
{f.evidence && (
<div className="bg-dark-800 rounded-lg p-2">
<span className="text-dark-500 text-xs">Evidence: </span>
<span className="text-dark-300 text-xs">{f.evidence.substring(0, 300)}</span>
</div>
)}
{f.poc_code && (
<div className="mt-2">
<p className="text-xs font-medium text-dark-400 mb-1">PoC Code</p>
<pre className="p-2 bg-dark-950 rounded text-xs text-green-400 overflow-x-auto max-h-[300px] overflow-y-auto whitespace-pre-wrap">{f.poc_code}</pre>
</div>
)}
{f.ai_status === 'rejected' && f.rejection_reason && (
<div className="bg-orange-500/10 border border-orange-500/20 rounded-lg p-2">
<span className="text-orange-400 text-xs font-medium">Rejection: </span>
<span className="text-orange-300/80 text-xs">{f.rejection_reason}</span>
</div>
)}
<div className="flex items-center gap-2 flex-wrap">
<span className={`text-xs ${f.ai_status === 'rejected' ? 'text-orange-400' : f.ai_verified ? 'text-green-400' : 'text-dark-500'}`}>
{f.ai_status === 'rejected' ? 'AI Rejected' : f.ai_verified ? 'AI Verified' : 'Tool Detected'}
</span>
{(() => {
const conf = getConfidenceDisplay(f)
if (!conf) return null
return (
<span className={`text-xs font-semibold px-1.5 py-0.5 rounded-full border ${CONFIDENCE_STYLES[conf.color]}`}>
Confidence: {conf.score}/100 ({conf.label})
</span>
)
})()}
</div>
{(() => {
const hasBreakdown = f.confidence_breakdown && Object.keys(f.confidence_breakdown).length > 0
const hasProof = !!f.proof_of_execution
const hasControls = !!f.negative_controls
if (!hasBreakdown && !hasProof && !hasControls) return null
return (
<div className="bg-dark-800 rounded-lg p-2 space-y-1">
{hasBreakdown && (
<div className="grid grid-cols-2 gap-x-4 gap-y-0.5 text-xs text-dark-400">
{Object.entries(f.confidence_breakdown!).map(([key, val]) => (
<div key={key} className="flex justify-between">
<span className="capitalize">{key.replace(/_/g, ' ')}</span>
<span className={`font-mono font-medium ${
Number(val) > 0 ? 'text-green-400' : Number(val) < 0 ? 'text-red-400' : 'text-dark-500'
}`}>
{Number(val) > 0 ? '+' : ''}{val}
</span>
</div>
))}
</div>
)}
{hasProof && (
<p className="text-xs text-dark-400 flex items-start gap-1">
<span className="text-green-400">&#10003;</span>
<span>Proof: <span className="text-dark-300">{f.proof_of_execution}</span></span>
</p>
)}
{hasControls && (
<p className="text-xs text-dark-400 flex items-start gap-1">
<span className="text-blue-400">&#9632;</span>
<span>Controls: <span className="text-dark-300">{f.negative_controls}</span></span>
</p>
)}
</div>
)
})()}
</div>
)}
</div>
))}
</div>
)}
{/* Activity Log */}
{showLogs && (
<div className="bg-dark-900 rounded-xl p-3 max-h-[500px] overflow-y-auto font-mono text-xs">
{logs.length === 0 ? (
<p className="text-dark-500 text-center py-4">No logs yet...</p>
) : (
logs.map((log, i) => (
<div key={i} className="flex gap-2 py-0.5">
<span className="text-dark-600 flex-shrink-0">{log.time || ''}</span>
<span className={`flex-shrink-0 uppercase w-12 ${
log.level === 'error' ? 'text-red-400' :
log.level === 'warning' ? 'text-yellow-400' :
log.level === 'info' ? 'text-blue-400' :
'text-dark-500'
}`}>{log.level}</span>
<span className={logMessageColor(log.message) || (log.source === 'llm' ? 'text-purple-400' : 'text-dark-300')}>
{log.message}
</span>
</div>
))
)}
<div ref={logsEndRef} />
</div>
)}
</div>
)}
{/* No findings yet but running */}
{findings.length === 0 && isRunning && (
<div className="bg-dark-800 border border-dark-700 rounded-2xl p-6 mb-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-white font-semibold">Activity</h3>
<button
onClick={() => setShowLogs(!showLogs)}
className={`px-3 py-1 rounded-lg text-xs transition-colors ${showLogs ? 'bg-primary-500 text-white' : 'bg-dark-700 text-dark-400 hover:text-white'}`}
>
<ScrollText className="w-3 h-3 inline mr-1" />{showLogs ? 'Hide' : 'Show'} Logs
</button>
</div>
{showLogs ? (
<div className="bg-dark-900 rounded-xl p-3 max-h-[400px] overflow-y-auto font-mono text-xs">
{logs.length === 0 ? (
<div className="flex items-center justify-center py-4 text-dark-500">
<Loader2 className="w-4 h-4 animate-spin mr-2" /> Waiting for logs...
</div>
) : (
logs.map((log, i) => (
<div key={i} className="flex gap-2 py-0.5">
<span className="text-dark-600 flex-shrink-0">{log.time || ''}</span>
<span className={`flex-shrink-0 uppercase w-12 ${
log.level === 'error' ? 'text-red-400' :
log.level === 'warning' ? 'text-yellow-400' :
log.level === 'info' ? 'text-blue-400' :
'text-dark-500'
}`}>{log.level}</span>
<span className={logMessageColor(log.message) || (log.source === 'llm' ? 'text-purple-400' : 'text-dark-300')}>
{log.message}
</span>
</div>
))
)}
<div ref={logsEndRef} />
</div>
) : (
<div className="flex items-center justify-center py-6 text-dark-500">
<Loader2 className="w-5 h-5 animate-spin mr-2" />
Scanning in progress... Findings will appear here as they are discovered.
</div>
)}
</div>
)}
{/* Completion Actions */}
{status?.status === 'completed' && (
<div className="bg-dark-800 border border-green-500/30 rounded-2xl p-6 mb-6">
<div className="flex items-center gap-3 mb-4">
<CheckCircle2 className="w-6 h-6 text-green-500" />
<h3 className="text-green-400 font-semibold text-lg">Pentest Complete</h3>
</div>
<p className="text-dark-400 mb-4">
Found {findings.length} vulnerabilities across {target}.
</p>
{/* Report Generation */}
{generatingReport && (
<div className="mb-4 p-4 bg-purple-500/10 border border-purple-500/20 rounded-xl flex items-center gap-3">
<Loader2 className="w-5 h-5 text-purple-400 animate-spin" />
<div>
<p className="text-purple-400 font-medium text-sm">Generating AI Report...</p>
<p className="text-dark-400 text-xs">The AI is analyzing findings and writing the executive summary.</p>
</div>
</div>
)}
<div className="flex flex-wrap gap-3">
<button
onClick={() => navigate(`/agent/${agentId}`)}
className="px-5 py-2 bg-primary-500 hover:bg-primary-600 text-white rounded-lg transition-colors flex items-center gap-2 text-sm"
>
<ExternalLink className="w-4 h-4" /> View Full Results
</button>
{!reportId ? (
<button
onClick={handleGenerateAiReport}
disabled={generatingReport || !status.scan_id}
className="px-5 py-2 bg-purple-500 hover:bg-purple-600 disabled:opacity-50 text-white rounded-lg transition-colors flex items-center gap-2 text-sm"
>
<Sparkles className="w-4 h-4" /> Generate AI Report
</button>
) : (
<>
<a
href={reportsApi.getViewUrl(reportId)}
target="_blank"
rel="noopener noreferrer"
className="px-5 py-2 bg-green-500 hover:bg-green-600 text-white rounded-lg transition-colors flex items-center gap-2 text-sm"
>
<FileText className="w-4 h-4" /> View Report
</a>
<a
href={reportsApi.getDownloadZipUrl(reportId)}
className="px-5 py-2 bg-dark-700 hover:bg-dark-600 text-white rounded-lg transition-colors flex items-center gap-2 text-sm"
>
<Download className="w-4 h-4" /> Download ZIP
</a>
</>
)}
</div>
</div>
)}
{/* Error state */}
{status?.status === 'error' && (
<div className="bg-dark-800 border border-red-500/30 rounded-2xl p-6">
<div className="flex items-center gap-3 mb-2">
<AlertTriangle className="w-6 h-6 text-red-400" />
<h3 className="text-red-400 font-semibold">Pentest Failed</h3>
</div>
<p className="text-dark-400">{status.error || 'An unexpected error occurred.'}</p>
</div>
)}
</div>
)}
</div>
)
}
+16 -2
View File
@@ -249,13 +249,27 @@ export default function HomePage() {
recentVulnerabilities.slice(0, 5).map((vuln) => (
<div
key={vuln.id}
className="flex items-center justify-between p-3 bg-dark-900/50 rounded-lg"
className={`flex items-center justify-between p-3 bg-dark-900/50 rounded-lg ${
(vuln as any).validation_status === 'ai_rejected' ? 'opacity-60 border-l-2 border-orange-500/40' :
(vuln as any).validation_status === 'false_positive' ? 'opacity-40' : ''
}`}
>
<div className="flex-1 min-w-0">
<p className="font-medium text-white truncate">{vuln.title}</p>
<p className="text-xs text-dark-400 truncate">{vuln.affected_endpoint}</p>
</div>
<SeverityBadge severity={vuln.severity} />
<div className="flex items-center gap-1.5">
{(vuln as any).validation_status === 'ai_rejected' && (
<span className="text-xs px-1.5 py-0.5 rounded-full bg-orange-500/20 text-orange-400">Rejected</span>
)}
{(vuln as any).validation_status === 'validated' && (
<span className="text-xs px-1.5 py-0.5 rounded-full bg-green-500/20 text-green-400">Validated</span>
)}
{(vuln as any).validation_status === 'false_positive' && (
<span className="text-xs px-1.5 py-0.5 rounded-full bg-dark-600 text-dark-400">FP</span>
)}
<SeverityBadge severity={vuln.severity} />
</div>
</div>
))
)}
+474
View File
@@ -0,0 +1,474 @@
import { useState, useEffect, useCallback } from 'react'
import { Link } from 'react-router-dom'
import {
Box, RefreshCw, Trash2, Heart, Clock, Cpu,
HardDrive, Timer, AlertTriangle, CheckCircle2,
XCircle, Wrench, Container
} from 'lucide-react'
import Card from '../components/common/Card'
import Button from '../components/common/Button'
import { sandboxApi } from '../services/api'
import type { SandboxPoolStatus, SandboxContainer } from '../types'
function formatUptime(seconds: number): string {
if (seconds < 60) return `${Math.floor(seconds)}s`
if (seconds < 3600) {
const m = Math.floor(seconds / 60)
const s = Math.floor(seconds % 60)
return `${m}m ${s}s`
}
const h = Math.floor(seconds / 3600)
const m = Math.floor((seconds % 3600) / 60)
return `${h}h ${m}m`
}
function timeAgo(isoDate: string | null): string {
if (!isoDate) return 'Unknown'
const diff = (Date.now() - new Date(isoDate).getTime()) / 1000
if (diff < 60) return 'Just now'
if (diff < 3600) return `${Math.floor(diff / 60)}m ago`
if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`
return `${Math.floor(diff / 86400)}d ago`
}
export default function SandboxDashboardPage() {
const [data, setData] = useState<SandboxPoolStatus | null>(null)
const [loading, setLoading] = useState(true)
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null)
const [destroyConfirm, setDestroyConfirm] = useState<string | null>(null)
const [healthResults, setHealthResults] = useState<Record<string, { status: string; tools: string[] } | null>>({})
const [healthLoading, setHealthLoading] = useState<Record<string, boolean>>({})
const [actionLoading, setActionLoading] = useState(false)
const fetchData = useCallback(async (showSpinner = false) => {
if (showSpinner) setLoading(true)
try {
const result = await sandboxApi.list()
setData(result)
} catch (error) {
console.error('Failed to fetch sandbox data:', error)
if (!data) {
setData({
pool: { active: 0, max_concurrent: 0, image: 'N/A', container_ttl_minutes: 0, docker_available: false },
containers: [],
error: 'Failed to connect to backend',
})
}
} finally {
setLoading(false)
}
}, [])
// Initial fetch + 5-second polling
useEffect(() => {
fetchData(true)
const interval = setInterval(() => fetchData(false), 5000)
return () => clearInterval(interval)
}, [fetchData])
// Auto-dismiss messages
useEffect(() => {
if (message) {
const timer = setTimeout(() => setMessage(null), 4000)
return () => clearTimeout(timer)
}
}, [message])
const handleDestroy = async (scanId: string) => {
if (destroyConfirm !== scanId) {
setDestroyConfirm(scanId)
setTimeout(() => setDestroyConfirm(null), 5000)
return
}
setDestroyConfirm(null)
setActionLoading(true)
try {
await sandboxApi.destroy(scanId)
setMessage({ type: 'success', text: `Container for scan ${scanId.slice(0, 8)}... destroyed` })
fetchData(false)
} catch (error: any) {
setMessage({ type: 'error', text: error?.response?.data?.detail || 'Failed to destroy container' })
} finally {
setActionLoading(false)
}
}
const handleHealthCheck = async (scanId: string) => {
setHealthLoading(prev => ({ ...prev, [scanId]: true }))
try {
const result = await sandboxApi.healthCheck(scanId)
setHealthResults(prev => ({ ...prev, [scanId]: result }))
setTimeout(() => {
setHealthResults(prev => ({ ...prev, [scanId]: null }))
}, 8000)
} catch {
setHealthResults(prev => ({ ...prev, [scanId]: { status: 'error', tools: [] } }))
} finally {
setHealthLoading(prev => ({ ...prev, [scanId]: false }))
}
}
const handleCleanup = async (type: 'expired' | 'orphans') => {
setActionLoading(true)
try {
if (type === 'expired') {
await sandboxApi.cleanup()
} else {
await sandboxApi.cleanupOrphans()
}
setMessage({ type: 'success', text: `${type === 'expired' ? 'Expired' : 'Orphan'} containers cleaned up` })
fetchData(false)
} catch (error: any) {
setMessage({ type: 'error', text: error?.response?.data?.detail || `Cleanup failed` })
} finally {
setActionLoading(false)
}
}
if (loading && !data) {
return (
<div className="animate-pulse space-y-6">
<div className="h-8 bg-dark-800 rounded w-64" />
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{[1, 2, 3, 4].map(i => (
<div key={i} className="h-24 bg-dark-800 rounded-lg" />
))}
</div>
<div className="space-y-4">
{[1, 2].map(i => (
<div key={i} className="h-40 bg-dark-800 rounded-lg" />
))}
</div>
</div>
)
}
const pool = data?.pool
const containers = data?.containers || []
const utilizationPct = pool ? (pool.active / pool.max_concurrent) * 100 : 0
return (
<div className="space-y-6 animate-fadeIn">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-white flex items-center gap-3">
<div className="w-10 h-10 bg-blue-500/20 rounded-lg flex items-center justify-center">
<Container className="w-6 h-6 text-blue-400" />
</div>
Sandbox Containers
</h1>
<p className="text-dark-400 mt-1">Real-time monitoring of per-scan Kali Linux containers</p>
</div>
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="sm"
onClick={() => handleCleanup('expired')}
isLoading={actionLoading}
>
<Timer className="w-4 h-4 mr-1" />
Cleanup Expired
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleCleanup('orphans')}
isLoading={actionLoading}
>
<Trash2 className="w-4 h-4 mr-1" />
Cleanup Orphans
</Button>
<Button
variant="secondary"
size="sm"
onClick={() => fetchData(false)}
>
<RefreshCw className="w-4 h-4 mr-1" />
Refresh
</Button>
</div>
</div>
{/* Status message */}
{message && (
<div className={`flex items-center gap-2 px-4 py-3 rounded-lg text-sm animate-fadeIn ${
message.type === 'success'
? 'bg-green-500/10 border border-green-500/30 text-green-400'
: 'bg-red-500/10 border border-red-500/30 text-red-400'
}`}>
{message.type === 'success' ? (
<CheckCircle2 className="w-4 h-4 flex-shrink-0" />
) : (
<AlertTriangle className="w-4 h-4 flex-shrink-0" />
)}
{message.text}
</div>
)}
{/* Pool Stats Cards */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{/* Active Containers */}
<Card>
<div className="flex items-center gap-3">
<div className={`w-10 h-10 rounded-lg flex items-center justify-center ${
utilizationPct >= 100 ? 'bg-red-500/20' :
utilizationPct >= 80 ? 'bg-yellow-500/20' :
'bg-green-500/20'
}`}>
<Box className={`w-5 h-5 ${
utilizationPct >= 100 ? 'text-red-400' :
utilizationPct >= 80 ? 'text-yellow-400' :
'text-green-400'
}`} />
</div>
<div>
<p className="text-2xl font-bold text-white">
{pool?.active || 0}<span className="text-dark-400 text-lg">/{pool?.max_concurrent || 0}</span>
</p>
<p className="text-xs text-dark-400">Active Containers</p>
</div>
</div>
</Card>
{/* Docker Status */}
<Card>
<div className="flex items-center gap-3">
<div className={`w-10 h-10 rounded-lg flex items-center justify-center ${
pool?.docker_available ? 'bg-green-500/20' : 'bg-red-500/20'
}`}>
<HardDrive className={`w-5 h-5 ${
pool?.docker_available ? 'text-green-400' : 'text-red-400'
}`} />
</div>
<div>
<p className="text-lg font-bold text-white">
{pool?.docker_available ? 'Online' : 'Offline'}
</p>
<p className="text-xs text-dark-400">Docker Engine</p>
</div>
</div>
</Card>
{/* Container Image */}
<Card>
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-purple-500/20 rounded-lg flex items-center justify-center">
<Cpu className="w-5 h-5 text-purple-400" />
</div>
<div>
<p className="text-sm font-bold text-white truncate max-w-[140px]" title={pool?.image}>
{pool?.image?.split(':')[0]?.split('/').pop() || 'N/A'}
</p>
<p className="text-xs text-dark-400">
{pool?.image?.includes(':') ? pool.image.split(':')[1] : 'latest'}
</p>
</div>
</div>
</Card>
{/* TTL */}
<Card>
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-orange-500/20 rounded-lg flex items-center justify-center">
<Clock className="w-5 h-5 text-orange-400" />
</div>
<div>
<p className="text-2xl font-bold text-white">
{pool?.container_ttl_minutes || 0}<span className="text-dark-400 text-lg"> min</span>
</p>
<p className="text-xs text-dark-400">Container TTL</p>
</div>
</div>
</Card>
</div>
{/* Capacity Bar */}
{pool && pool.max_concurrent > 0 && (
<div className="bg-dark-800 rounded-lg p-4 border border-dark-700">
<div className="flex items-center justify-between mb-2">
<span className="text-sm text-dark-300">Pool Capacity</span>
<span className={`text-sm font-medium ${
utilizationPct >= 100 ? 'text-red-400' :
utilizationPct >= 80 ? 'text-yellow-400' :
'text-green-400'
}`}>
{Math.round(utilizationPct)}%
</span>
</div>
<div className="w-full bg-dark-900 rounded-full h-2.5">
<div
className={`h-2.5 rounded-full transition-all duration-500 ${
utilizationPct >= 100 ? 'bg-red-500' :
utilizationPct >= 80 ? 'bg-yellow-500' :
'bg-green-500'
}`}
style={{ width: `${Math.min(utilizationPct, 100)}%` }}
/>
</div>
</div>
)}
{/* Container List */}
{containers.length === 0 ? (
<div className="bg-dark-800 rounded-lg border border-dark-700 p-12 text-center">
<Box className="w-16 h-16 text-dark-600 mx-auto mb-4" />
<h3 className="text-lg font-medium text-dark-300 mb-2">No Sandbox Containers Running</h3>
<p className="text-dark-400 text-sm max-w-md mx-auto">
Containers are automatically created when scans start and destroyed when they complete.
Start a scan to see containers here.
</p>
</div>
) : (
<div className="space-y-3">
<h2 className="text-lg font-semibold text-white">
Running Containers ({containers.length})
</h2>
{containers.map((container: SandboxContainer) => {
const health = healthResults[container.scan_id]
const isHealthLoading = healthLoading[container.scan_id]
const isConfirming = destroyConfirm === container.scan_id
return (
<div
key={container.scan_id}
className="bg-dark-800 rounded-lg border border-dark-700 p-5 hover:border-dark-600 transition-colors"
>
{/* Container Header */}
<div className="flex items-start justify-between mb-4">
<div className="flex items-center gap-3">
<div className={`w-3 h-3 rounded-full ${
container.available ? 'bg-green-500 animate-pulse' : 'bg-red-500'
}`} />
<div>
<h3 className="text-white font-medium font-mono text-sm">
{container.container_name}
</h3>
<div className="flex items-center gap-2 mt-1">
<span className="text-xs text-dark-400">Scan:</span>
<Link
to={`/scan/${container.scan_id}`}
className="text-xs text-primary-400 hover:text-primary-300 font-mono"
>
{container.scan_id.slice(0, 12)}...
</Link>
</div>
</div>
</div>
<div className="flex items-center gap-2">
{/* Status badge */}
<span className={`inline-flex items-center gap-1 px-2.5 py-1 rounded-full text-xs font-medium ${
container.available
? 'bg-green-500/10 text-green-400 border border-green-500/30'
: 'bg-red-500/10 text-red-400 border border-red-500/30'
}`}>
{container.available ? (
<><CheckCircle2 className="w-3 h-3" /> Running</>
) : (
<><XCircle className="w-3 h-3" /> Stopped</>
)}
</span>
</div>
</div>
{/* Container Info Grid */}
<div className="grid grid-cols-2 md:grid-cols-3 gap-4 mb-4">
{/* Uptime */}
<div>
<p className="text-xs text-dark-400 mb-1">Uptime</p>
<p className="text-sm text-white font-medium">
{formatUptime(container.uptime_seconds)}
</p>
</div>
{/* Created */}
<div>
<p className="text-xs text-dark-400 mb-1">Created</p>
<p className="text-sm text-dark-300">
{timeAgo(container.created_at)}
</p>
</div>
{/* Tools count */}
<div>
<p className="text-xs text-dark-400 mb-1">Installed Tools</p>
<p className="text-sm text-white font-medium">
{container.installed_tools.length}
</p>
</div>
</div>
{/* Installed Tools */}
{container.installed_tools.length > 0 && (
<div className="mb-4">
<p className="text-xs text-dark-400 mb-2">Tools</p>
<div className="flex flex-wrap gap-1.5">
{container.installed_tools.map(tool => (
<span
key={tool}
className="inline-flex items-center gap-1 px-2 py-0.5 bg-dark-900 border border-dark-600 rounded text-xs text-dark-300"
>
<Wrench className="w-3 h-3 text-dark-500" />
{tool}
</span>
))}
</div>
</div>
)}
{/* Health Check Result */}
{health && (
<div className={`mb-4 px-3 py-2 rounded-lg text-xs animate-fadeIn ${
health.status === 'healthy'
? 'bg-green-500/10 border border-green-500/20 text-green-400'
: health.status === 'degraded'
? 'bg-yellow-500/10 border border-yellow-500/20 text-yellow-400'
: 'bg-red-500/10 border border-red-500/20 text-red-400'
}`}>
<span className="font-medium">Health: {health.status}</span>
{health.tools.length > 0 && (
<span className="ml-2">
— Verified: {health.tools.join(', ')}
</span>
)}
</div>
)}
{/* Actions */}
<div className="flex items-center gap-2 pt-3 border-t border-dark-700">
<Button
variant="ghost"
size="sm"
onClick={() => handleHealthCheck(container.scan_id)}
isLoading={isHealthLoading}
>
<Heart className="w-4 h-4 mr-1" />
Health Check
</Button>
<Button
variant={isConfirming ? 'danger' : 'ghost'}
size="sm"
onClick={() => handleDestroy(container.scan_id)}
isLoading={actionLoading}
>
<Trash2 className="w-4 h-4 mr-1" />
{isConfirming ? 'Confirm Destroy' : 'Destroy'}
</Button>
</div>
</div>
)
})}
</div>
)}
{/* Auto-refresh indicator */}
<div className="text-center text-xs text-dark-500">
Auto-refreshing every 5 seconds
</div>
</div>
)
}
+565 -38
View File
@@ -2,15 +2,47 @@ import { useEffect, useMemo, useState } from 'react'
import { useParams, useNavigate } from 'react-router-dom'
import {
Globe, FileText, StopCircle, RefreshCw, ChevronDown, ChevronRight,
ExternalLink, Copy, Shield, AlertTriangle, Cpu, CheckCircle, XCircle, Clock
ExternalLink, Copy, Shield, AlertTriangle, Cpu, CheckCircle, XCircle, Clock,
SkipForward, Check, Minus, Pause, Play, Download
} from 'lucide-react'
import Card from '../components/common/Card'
import Button from '../components/common/Button'
import { SeverityBadge } from '../components/common/Badge'
import { scansApi, reportsApi, agentTasksApi } from '../services/api'
import { scansApi, reportsApi, agentTasksApi, agentApi, vulnerabilitiesApi } from '../services/api'
import { wsService } from '../services/websocket'
import { useScanStore } from '../store'
import type { Endpoint, Vulnerability, WSMessage, ScanAgentTask, Report } from '../types'
import type { Endpoint, Vulnerability, WSMessage, ScanAgentTask, Report, AgentStatus, AgentFinding } from '../types'
// Resolve a confidence score from the various possible sources on a finding.
// Handles: numeric confidence_score field, legacy string "high"/"medium"/"low", or numeric strings.
function getConfidenceDisplay(finding: { confidence_score?: number; confidence?: string }): { score: number; color: string; label: string } | null {
let score: number | null = null
if (typeof finding.confidence_score === 'number') {
score = finding.confidence_score
} else if (finding.confidence) {
const parsed = Number(finding.confidence)
if (!isNaN(parsed)) {
score = parsed
} else {
// Legacy text values
const map: Record<string, number> = { high: 90, medium: 60, low: 30 }
score = map[finding.confidence.toLowerCase()] ?? null
}
}
if (score === null) return null
const color = score >= 90 ? 'green' : score >= 60 ? 'yellow' : 'red'
const label = score >= 90 ? 'Confirmed' : score >= 60 ? 'Likely' : 'Low'
return { score, color, label }
}
const CONFIDENCE_STYLES: Record<string, string> = {
green: 'bg-green-500/15 text-green-400 border-green-500/30',
yellow: 'bg-yellow-500/15 text-yellow-400 border-yellow-500/30',
red: 'bg-red-500/15 text-red-400 border-red-500/30',
}
export default function ScanDetailsPage() {
const { scanId } = useParams<{ scanId: string }>()
@@ -29,6 +61,10 @@ export default function ScanDetailsPage() {
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [autoGeneratedReport, setAutoGeneratedReport] = useState<Report | null>(null)
const [agentData, setAgentData] = useState<AgentStatus | null>(null)
const [skipConfirm, setSkipConfirm] = useState<string | null>(null)
const [skippedPhases, setSkippedPhases] = useState<Set<string>>(new Set())
const [validationFilter, setValidationFilter] = useState<'all' | 'confirmed' | 'rejected' | 'validated'>('all')
// Calculate vulnerability counts from actual data
const vulnCounts = useMemo(() => getVulnCounts(), [vulnerabilities])
@@ -68,6 +104,57 @@ export default function ScanDetailsPage() {
if (reportsData.reports?.length > 0) {
setAutoGeneratedReport(reportsData.reports[0])
}
// If scan has no vulns from DB, try to get agent data (in-memory findings)
if ((!vulnsData.vulnerabilities || vulnsData.vulnerabilities.length === 0)) {
const agentStatus = await agentApi.getByScan(scanId)
if (agentStatus) {
setAgentData(agentStatus)
// Convert agent findings to vulnerability format for display
if (agentStatus.findings && agentStatus.findings.length > 0) {
const mapFinding = (f: AgentFinding): Vulnerability => ({
id: f.id,
scan_id: scanId,
title: f.title,
vulnerability_type: f.vulnerability_type,
severity: f.severity,
cvss_score: f.cvss_score || null,
cvss_vector: f.cvss_vector || null,
cwe_id: f.cwe_id || null,
description: f.description || null,
affected_endpoint: f.affected_endpoint || null,
poc_request: f.request || null,
poc_response: f.response || null,
poc_payload: f.payload || null,
poc_parameter: f.parameter || null,
poc_evidence: f.evidence || null,
poc_code: f.poc_code || null,
impact: f.impact || null,
remediation: f.remediation || null,
references: f.references || [],
ai_analysis: f.evidence || null,
validation_status: f.ai_status === 'rejected' ? 'ai_rejected' : 'ai_confirmed',
ai_rejection_reason: f.rejection_reason || null,
confidence_score: f.confidence_score,
confidence_breakdown: f.confidence_breakdown,
proof_of_execution: f.proof_of_execution,
negative_controls: f.negative_controls,
created_at: new Date().toISOString()
})
const confirmed = agentStatus.findings.map(mapFinding)
const rejected = (agentStatus.rejected_findings || []).map(mapFinding)
const mappedVulns: Vulnerability[] = [...confirmed, ...rejected]
setVulnerabilities(mappedVulns)
}
// Update scan progress from agent
if (agentStatus.progress !== undefined) {
updateScan(scanId, {
progress: agentStatus.progress,
current_phase: agentStatus.phase
})
}
}
}
} catch (err: any) {
console.error('Failed to fetch scan:', err)
setError(err?.response?.data?.detail || 'Failed to load scan')
@@ -77,9 +164,9 @@ export default function ScanDetailsPage() {
}
fetchData()
// Poll for updates while scan is running
// Poll for updates while scan is running or paused (8s interval, WebSocket handles real-time)
const pollInterval = setInterval(async () => {
if (currentScan?.status === 'running' || !currentScan) {
if (currentScan?.status === 'running' || currentScan?.status === 'paused' || !currentScan) {
try {
const scan = await scansApi.get(scanId)
setCurrentScan(scan)
@@ -99,11 +186,60 @@ export default function ScanDetailsPage() {
if (tasksData.tasks?.length > 0) {
setAgentTasks(tasksData.tasks)
}
// Also poll agent data for real-time findings
if (!vulnsData.vulnerabilities || vulnsData.vulnerabilities.length === 0) {
const agentStatus = await agentApi.getByScan(scanId)
if (agentStatus) {
setAgentData(agentStatus)
if (agentStatus.findings && agentStatus.findings.length > 0) {
const mapFinding = (f: AgentFinding): Vulnerability => ({
id: f.id,
scan_id: scanId,
title: f.title,
vulnerability_type: f.vulnerability_type,
severity: f.severity,
cvss_score: f.cvss_score || null,
cvss_vector: f.cvss_vector || null,
cwe_id: f.cwe_id || null,
description: f.description || null,
affected_endpoint: f.affected_endpoint || null,
poc_request: f.request || null,
poc_response: f.response || null,
poc_payload: f.payload || null,
poc_parameter: f.parameter || null,
poc_evidence: f.evidence || null,
poc_code: f.poc_code || null,
impact: f.impact || null,
remediation: f.remediation || null,
references: f.references || [],
ai_analysis: f.evidence || null,
validation_status: f.ai_status === 'rejected' ? 'ai_rejected' : 'ai_confirmed',
ai_rejection_reason: f.rejection_reason || null,
confidence_score: f.confidence_score,
confidence_breakdown: f.confidence_breakdown,
proof_of_execution: f.proof_of_execution,
negative_controls: f.negative_controls,
created_at: new Date().toISOString()
})
const confirmed = agentStatus.findings.map(mapFinding)
const rejected = (agentStatus.rejected_findings || []).map(mapFinding)
const mappedVulns: Vulnerability[] = [...confirmed, ...rejected]
setVulnerabilities(mappedVulns)
}
if (agentStatus.progress !== undefined) {
updateScan(scanId, {
progress: agentStatus.progress,
current_phase: agentStatus.phase
})
}
}
}
} catch (err) {
console.error('Poll error:', err)
}
}
}, 3000)
}, 8000)
// Connect WebSocket for running scans
wsService.connect(scanId)
@@ -117,10 +253,16 @@ export default function ScanDetailsPage() {
current_phase: message.message as string
})
break
case 'phase_change':
updateScan(scanId, { current_phase: message.phase as string })
addLog('info', `Phase: ${message.phase}`)
case 'phase_change': {
const phase = message.phase as string
updateScan(scanId, { current_phase: phase })
addLog('info', `Phase: ${phase}`)
// Track skipped phases
if (phase.endsWith('_skipped')) {
setSkippedPhases(prev => new Set([...prev, phase.replace('_skipped', '')]))
}
break
}
case 'endpoint_found':
addEndpoint(message.endpoint as Endpoint)
break
@@ -245,6 +387,36 @@ export default function ScanDetailsPage() {
}
}
const handlePauseScan = async () => {
if (!scanId) return
try {
await scansApi.pause(scanId)
updateScan(scanId, { status: 'paused' })
} catch (error) {
console.error('Failed to pause scan:', error)
}
}
const handleResumeScan = async () => {
if (!scanId) return
try {
await scansApi.resume(scanId)
updateScan(scanId, { status: 'running' })
} catch (error) {
console.error('Failed to resume scan:', error)
}
}
const handleSkipToPhase = async (phase: string) => {
if (!scanId) return
try {
await scansApi.skipToPhase(scanId, phase)
setSkipConfirm(null)
} catch (error: any) {
console.error('Failed to skip phase:', error)
}
}
const handleGenerateReport = async () => {
if (!scanId) return
setIsGeneratingReport(true)
@@ -327,20 +499,53 @@ export default function ScanDetailsPage() {
</div>
</div>
<div className="flex gap-2">
{currentScan.status === 'running' && (
<Button variant="danger" onClick={handleStopScan}>
<StopCircle className="w-4 h-4 mr-2" />
Stop Scan
{agentData?.agent_id && (
<Button variant="secondary" onClick={() => navigate(`/agent/${agentData.agent_id}`)}>
<Cpu className="w-4 h-4 mr-2" />
Agent View
</Button>
)}
{currentScan.status === 'running' && (
<>
<Button variant="secondary" onClick={handlePauseScan}>
<Pause className="w-4 h-4 mr-2" />
Pause
</Button>
<Button variant="danger" onClick={handleStopScan}>
<StopCircle className="w-4 h-4 mr-2" />
Stop
</Button>
</>
)}
{currentScan.status === 'paused' && (
<>
<Button variant="primary" onClick={handleResumeScan}>
<Play className="w-4 h-4 mr-2" />
Resume
</Button>
<Button variant="danger" onClick={handleStopScan}>
<StopCircle className="w-4 h-4 mr-2" />
Stop
</Button>
</>
)}
{autoGeneratedReport && (
<Button
variant="secondary"
onClick={() => window.open(reportsApi.getViewUrl(autoGeneratedReport.id), '_blank')}
>
<FileText className="w-4 h-4 mr-2" />
View Report
</Button>
<>
<Button
variant="secondary"
onClick={() => window.open(reportsApi.getViewUrl(autoGeneratedReport.id), '_blank')}
>
<FileText className="w-4 h-4 mr-2" />
View Report
</Button>
<Button
variant="secondary"
onClick={() => window.open(reportsApi.getDownloadZipUrl(autoGeneratedReport.id), '_blank')}
>
<Download className="w-4 h-4 mr-2" />
Download ZIP
</Button>
</>
)}
{(currentScan.status === 'completed' || currentScan.status === 'stopped') && (
<Button onClick={handleGenerateReport} isLoading={isGeneratingReport}>
@@ -351,23 +556,131 @@ export default function ScanDetailsPage() {
</div>
</div>
{/* Progress */}
{currentScan.status === 'running' && (
<Card>
<div className="space-y-3">
<div className="flex items-center justify-between">
<span className="text-dark-300">{currentScan.current_phase || 'Initializing...'}</span>
<span className="text-white font-medium">{currentScan.progress}%</span>
{/* Phase Stepper */}
{(currentScan.status === 'running' || currentScan.status === 'paused' || currentScan.status === 'completed' || currentScan.status === 'stopped') && (() => {
const PHASES = [
{ id: 'initializing', label: 'Init', fullLabel: 'Initialization' },
{ id: 'recon', label: 'Recon', fullLabel: 'Reconnaissance' },
{ id: 'analyzing', label: 'Analysis', fullLabel: 'AI Analysis' },
{ id: 'testing', label: 'Testing', fullLabel: 'Vulnerability Testing' },
{ id: 'completed', label: 'Done', fullLabel: 'Completed' },
]
const phaseOrder = PHASES.map(p => p.id)
const rawPhase = currentScan.current_phase || 'initializing'
// Normalize: "skipping_to_testing" -> current is between phases
const currentPhase = rawPhase.startsWith('skipping_to_') ? rawPhase.replace('skipping_to_', '') : rawPhase.replace('_skipped', '')
const currentIdx = phaseOrder.indexOf(currentPhase)
const isRunning = currentScan.status === 'running' || currentScan.status === 'paused'
return (
<Card>
<div className="space-y-4">
{/* Phase nodes */}
<div className="flex items-center justify-between relative">
{PHASES.map((phase, idx) => {
const isCompleted = idx < currentIdx || currentScan.status === 'completed'
const isActive = idx === currentIdx && isRunning
const isSkipped = skippedPhases.has(phase.id)
const isFuture = idx > currentIdx && isRunning
const canSkipTo = isFuture && phase.id !== 'initializing'
return (
<div key={phase.id} className="flex items-center flex-1 last:flex-none">
{/* Node */}
<div className="flex flex-col items-center relative z-10">
{/* Circle */}
{canSkipTo ? (
skipConfirm === phase.id ? (
<div className="flex items-center gap-1">
<button
onClick={() => handleSkipToPhase(phase.id)}
className="w-9 h-9 rounded-full bg-brand-500 text-white flex items-center justify-center hover:bg-brand-400 transition-colors"
title={`Skip to ${phase.fullLabel}`}
>
<Check className="w-4 h-4" />
</button>
<button
onClick={() => setSkipConfirm(null)}
className="w-7 h-7 rounded-full bg-dark-600 text-dark-300 flex items-center justify-center hover:bg-dark-500 transition-colors"
>
<XCircle className="w-3.5 h-3.5" />
</button>
</div>
) : (
<button
onClick={() => setSkipConfirm(phase.id)}
className="w-9 h-9 rounded-full border-2 border-dark-500 bg-dark-800 text-dark-400 flex items-center justify-center hover:border-brand-400 hover:text-brand-400 hover:bg-brand-500/10 transition-all group"
title={`Skip to ${phase.fullLabel}`}
>
<SkipForward className="w-4 h-4 opacity-0 group-hover:opacity-100 transition-opacity" />
<span className="absolute text-[10px] group-hover:hidden">{idx + 1}</span>
</button>
)
) : isCompleted ? (
<div className={`w-9 h-9 rounded-full flex items-center justify-center ${
isSkipped
? 'bg-yellow-500/20 border-2 border-yellow-500/50'
: 'bg-green-500/20 border-2 border-green-500/50'
}`}>
{isSkipped
? <Minus className="w-4 h-4 text-yellow-400" />
: <Check className="w-4 h-4 text-green-400" />
}
</div>
) : isActive ? (
<div className="w-9 h-9 rounded-full bg-brand-500/20 border-2 border-brand-500 flex items-center justify-center animate-pulse">
<div className="w-3 h-3 rounded-full bg-brand-400" />
</div>
) : (
<div className="w-9 h-9 rounded-full border-2 border-dark-600 bg-dark-800 flex items-center justify-center">
<span className="text-xs text-dark-500">{idx + 1}</span>
</div>
)}
{/* Label */}
<span className={`text-xs mt-2 font-medium ${
isActive ? 'text-brand-400' : isCompleted ? (isSkipped ? 'text-yellow-400' : 'text-green-400') : 'text-dark-500'
}`}>
{isSkipped ? `${phase.label} (skipped)` : phase.label}
</span>
{/* Skip hint */}
{canSkipTo && skipConfirm === phase.id && (
<span className="text-[10px] text-brand-400 mt-0.5">Skip here?</span>
)}
</div>
{/* Connector line */}
{idx < PHASES.length - 1 && (
<div className={`flex-1 h-0.5 mx-2 mt-[-20px] ${
idx < currentIdx ? 'bg-green-500/50' : 'bg-dark-600'
}`} />
)}
</div>
)
})}
</div>
{/* Progress bar + info */}
<div className="flex items-center justify-between text-sm">
<span className="text-dark-300">
{rawPhase.startsWith('skipping_to_')
? `Skipping to ${rawPhase.replace('skipping_to_', '')}...`
: currentScan.current_phase || 'Initializing...'
}
</span>
<span className="text-white font-medium">{currentScan.progress}%</span>
</div>
<div className="h-1.5 bg-dark-900 rounded-full overflow-hidden">
<div
className="h-full bg-brand-500 rounded-full transition-all duration-300"
style={{ width: `${currentScan.progress}%` }}
/>
</div>
</div>
<div className="h-2 bg-dark-900 rounded-full overflow-hidden">
<div
className="h-full bg-primary-500 rounded-full transition-all duration-300"
style={{ width: `${currentScan.progress}%` }}
/>
</div>
</div>
</Card>
)}
</Card>
)
})()}
{/* Auto-generated Report Notification */}
{autoGeneratedReport && (
@@ -472,6 +785,31 @@ export default function ScanDetailsPage() {
{/* Vulnerabilities Tab */}
{activeTab === 'vulns' && (
<div className="space-y-3">
{/* Validation Filter Tabs */}
{vulnerabilities.length > 0 && (
<div className="flex gap-2 mb-2">
{(['all', 'confirmed', 'rejected', 'validated'] as const).map((filter) => {
const count = filter === 'all' ? vulnerabilities.length
: filter === 'confirmed' ? vulnerabilities.filter(v => !v.validation_status || v.validation_status === 'ai_confirmed' || v.validation_status === 'validated').length
: filter === 'rejected' ? vulnerabilities.filter(v => v.validation_status === 'ai_rejected' || v.validation_status === 'false_positive').length
: vulnerabilities.filter(v => v.validation_status === 'validated').length
return (
<button
key={filter}
onClick={() => setValidationFilter(filter)}
className={`px-3 py-1 text-xs rounded-full transition-colors ${
validationFilter === filter
? 'bg-primary-500/20 text-primary-400 border border-primary-500/30'
: 'bg-dark-700 text-dark-400 border border-dark-600 hover:text-dark-300'
}`}
>
{filter.charAt(0).toUpperCase() + filter.slice(1)} ({count})
</button>
)
})}
</div>
)}
{vulnerabilities.length === 0 ? (
<Card>
<p className="text-dark-400 text-center py-8">
@@ -479,10 +817,23 @@ export default function ScanDetailsPage() {
</p>
</Card>
) : (
vulnerabilities.map((vuln, idx) => (
vulnerabilities
.filter((vuln) => {
if (validationFilter === 'all') return true
if (validationFilter === 'confirmed') return !vuln.validation_status || vuln.validation_status === 'ai_confirmed' || vuln.validation_status === 'validated'
if (validationFilter === 'rejected') return vuln.validation_status === 'ai_rejected' || vuln.validation_status === 'false_positive'
if (validationFilter === 'validated') return vuln.validation_status === 'validated'
return true
})
.map((vuln, idx) => (
<div
key={vuln.id || `vuln-${idx}`}
className="bg-dark-800 rounded-lg border border-dark-700 overflow-hidden"
className={`bg-dark-800 rounded-lg border overflow-hidden ${
vuln.validation_status === 'ai_rejected' ? 'border-orange-500/40 opacity-70' :
vuln.validation_status === 'false_positive' ? 'border-dark-600 opacity-50' :
vuln.validation_status === 'validated' ? 'border-green-500/40' :
'border-dark-700'
}`}
>
{/* Vulnerability Header */}
<div
@@ -513,6 +864,37 @@ export default function ScanDetailsPage() {
</span>
)}
<SeverityBadge severity={vuln.severity} />
{/* Confidence Score */}
{(() => {
const conf = getConfidenceDisplay(vuln as any)
if (!conf) return null
return (
<span className={`text-xs font-semibold px-2 py-0.5 rounded-full border ${CONFIDENCE_STYLES[conf.color]}`}>
{conf.score}/100
</span>
)
})()}
{/* Validation Status Badge */}
{vuln.validation_status === 'ai_rejected' && (
<span className="text-xs px-2 py-0.5 rounded-full bg-orange-500/20 text-orange-400 border border-orange-500/30 flex items-center gap-1">
<AlertTriangle className="w-3 h-3" /> AI Rejected
</span>
)}
{vuln.validation_status === 'validated' && (
<span className="text-xs px-2 py-0.5 rounded-full bg-green-500/20 text-green-400 border border-green-500/30 flex items-center gap-1">
<CheckCircle className="w-3 h-3" /> Validated
</span>
)}
{vuln.validation_status === 'false_positive' && (
<span className="text-xs px-2 py-0.5 rounded-full bg-dark-600 text-dark-400 border border-dark-500 flex items-center gap-1">
<XCircle className="w-3 h-3" /> False Positive
</span>
)}
{(!vuln.validation_status || vuln.validation_status === 'ai_confirmed') && (
<span className="text-xs px-2 py-0.5 rounded-full bg-emerald-500/15 text-emerald-400 border border-emerald-500/30">
AI Confirmed
</span>
)}
</div>
</div>
</div>
@@ -545,6 +927,59 @@ export default function ScanDetailsPage() {
)}
</div>
{/* Validation Pipeline Details */}
{(() => {
const conf = getConfidenceDisplay(vuln as any)
if (!conf) return null
return (
<div className={`rounded-lg p-3 border ${CONFIDENCE_STYLES[conf.color]}`}>
<div className="flex items-center gap-2 mb-2">
<Shield className="w-4 h-4" />
<span className="text-sm font-semibold">Validation Pipeline</span>
<span className={`text-xs px-2 py-0.5 rounded-full font-medium ${
conf.score >= 90 ? 'bg-green-500/20 text-green-400' :
conf.score >= 60 ? 'bg-yellow-500/20 text-yellow-400' :
'bg-red-500/20 text-red-400'
}`}>
{conf.score}/100 {conf.label}
</span>
</div>
{/* Scoring Breakdown */}
{vuln.confidence_breakdown && Object.keys(vuln.confidence_breakdown).length > 0 && (
<div className="grid grid-cols-2 gap-x-4 gap-y-1 text-xs mt-1 mb-2">
{Object.entries(vuln.confidence_breakdown).map(([key, val]) => (
<div key={key} className="flex justify-between">
<span className="opacity-70 capitalize">{key.replace(/_/g, ' ')}</span>
<span className={`font-mono font-medium ${
Number(val) > 0 ? 'text-green-400' : Number(val) < 0 ? 'text-red-400' : 'opacity-50'
}`}>
{Number(val) > 0 ? '+' : ''}{val}
</span>
</div>
))}
</div>
)}
{/* Proof of Execution */}
{vuln.proof_of_execution && (
<div className="text-xs mt-1 flex items-start gap-1">
<CheckCircle className="w-3 h-3 mt-0.5 flex-shrink-0 text-green-400" />
<span className="opacity-80">{vuln.proof_of_execution}</span>
</div>
)}
{/* Negative Controls */}
{vuln.negative_controls && (
<div className="text-xs mt-1 flex items-start gap-1">
<Shield className="w-3 h-3 mt-0.5 flex-shrink-0 text-blue-400" />
<span className="opacity-80">{vuln.negative_controls}</span>
</div>
)}
</div>
)
})()}
{/* Description */}
{vuln.description && (
<div>
@@ -602,6 +1037,14 @@ export default function ScanDetailsPage() {
</div>
)}
{/* Exploitation Code */}
{vuln.poc_code && (
<div className="mt-3">
<p className="text-xs font-medium text-dark-400 mb-1">Exploitation Code</p>
<pre className="p-3 bg-dark-950 rounded text-xs text-green-400 overflow-x-auto max-h-[400px] overflow-y-auto whitespace-pre-wrap">{vuln.poc_code}</pre>
</div>
)}
{/* Remediation */}
{vuln.remediation && (
<div>
@@ -618,6 +1061,90 @@ export default function ScanDetailsPage() {
</div>
)}
{/* AI Rejection Reason */}
{vuln.validation_status === 'ai_rejected' && vuln.ai_rejection_reason && (
<div className="bg-orange-500/10 border border-orange-500/20 rounded-lg p-3">
<p className="text-sm font-medium text-orange-400 mb-1 flex items-center gap-1">
<AlertTriangle className="w-4 h-4" /> AI Rejection Reason
</p>
<p className="text-sm text-orange-300/80">{vuln.ai_rejection_reason}</p>
</div>
)}
{/* Manual Validation Actions */}
{vuln.validation_status !== 'validated' && vuln.validation_status !== 'false_positive' && (
<div className="flex items-center gap-2 pt-2 border-t border-dark-700">
<span className="text-xs text-dark-500 mr-2">Manual Review:</span>
<Button
variant="ghost"
size="sm"
className="text-green-400 hover:bg-green-500/10 border border-green-500/30"
onClick={async (e) => {
e.stopPropagation()
try {
await vulnerabilitiesApi.validate(vuln.id, 'validated')
// Update local state
const updated = vulnerabilities.map(v =>
v.id === vuln.id ? { ...v, validation_status: 'validated' as const } : v
)
setVulnerabilities(updated)
} catch (err) { console.error('Validate error:', err) }
}}
>
<CheckCircle className="w-3 h-3 mr-1" />
Validate
</Button>
<Button
variant="ghost"
size="sm"
className="text-dark-400 hover:bg-red-500/10 border border-dark-600"
onClick={async (e) => {
e.stopPropagation()
try {
await vulnerabilitiesApi.validate(vuln.id, 'false_positive')
const updated = vulnerabilities.map(v =>
v.id === vuln.id ? { ...v, validation_status: 'false_positive' as const } : v
)
setVulnerabilities(updated)
} catch (err) { console.error('Mark FP error:', err) }
}}
>
<XCircle className="w-3 h-3 mr-1" />
False Positive
</Button>
{vuln.validation_status === 'ai_rejected' && (
<span className="text-xs text-orange-400/60 ml-2">
AI rejected this finding - review the evidence above
</span>
)}
</div>
)}
{(vuln.validation_status === 'validated' || vuln.validation_status === 'false_positive') && (
<div className="flex items-center gap-2 pt-2 border-t border-dark-700">
<span className="text-xs text-dark-500">
{vuln.validation_status === 'validated' ? 'Manually validated by pentester' : 'Marked as false positive'}
</span>
<Button
variant="ghost"
size="sm"
className="text-dark-500 hover:text-dark-300 text-xs"
onClick={async (e) => {
e.stopPropagation()
try {
const revertTo = vuln.ai_rejection_reason ? 'ai_rejected' : 'ai_confirmed'
await vulnerabilitiesApi.validate(vuln.id, revertTo)
const updated = vulnerabilities.map(v =>
v.id === vuln.id ? { ...v, validation_status: revertTo as any } : v
)
setVulnerabilities(updated)
} catch (err) { console.error('Revert error:', err) }
}}
>
Undo
</Button>
</div>
)}
{/* References */}
{vuln.references?.length > 0 && (
<div>
+734
View File
@@ -0,0 +1,734 @@
import { useState, useEffect, useCallback } from 'react'
import { Plus, Trash2, Play, Pause, Clock, RefreshCw, Target, Calendar, ChevronDown, Shield, Zap, Search, Settings2, AlertTriangle, CheckCircle2 } from 'lucide-react'
import Card from '../components/common/Card'
import Button from '../components/common/Button'
import Input from '../components/common/Input'
import { schedulerApi } from '../services/api'
import type { ScheduleJob, AgentRole } from '../types'
// Cron presets for quick selection
const CRON_PRESETS = [
{ label: 'Every Hour', value: '0 * * * *', desc: 'Runs at the start of every hour' },
{ label: 'Every 6 Hours', value: '0 */6 * * *', desc: 'Runs every 6 hours' },
{ label: 'Daily at 2 AM', value: '0 2 * * *', desc: 'Runs once a day at 2:00 AM' },
{ label: 'Daily at Midnight', value: '0 0 * * *', desc: 'Runs once a day at midnight' },
{ label: 'Weekdays at 9 AM', value: '0 9 * * 1-5', desc: 'Monday to Friday at 9:00 AM' },
{ label: 'Weekly (Monday)', value: '0 0 * * 1', desc: 'Every Monday at midnight' },
{ label: 'Weekly (Friday)', value: '0 18 * * 5', desc: 'Every Friday at 6:00 PM' },
{ label: 'Monthly (1st)', value: '0 0 1 * *', desc: 'First day of each month' },
{ label: 'Custom', value: 'custom', desc: 'Enter a custom cron expression' },
]
const SCAN_TYPES = [
{ id: 'quick', label: 'Quick', icon: Zap, desc: 'Fast surface scan' },
{ id: 'full', label: 'Full', icon: Search, desc: 'Comprehensive analysis' },
{ id: 'custom', label: 'Custom', icon: Settings2, desc: 'Custom configuration' },
]
const DAYS_OF_WEEK = [
{ id: 0, short: 'Sun', full: 'Sunday' },
{ id: 1, short: 'Mon', full: 'Monday' },
{ id: 2, short: 'Tue', full: 'Tuesday' },
{ id: 3, short: 'Wed', full: 'Wednesday' },
{ id: 4, short: 'Thu', full: 'Thursday' },
{ id: 5, short: 'Fri', full: 'Friday' },
{ id: 6, short: 'Sat', full: 'Saturday' },
]
export default function SchedulerPage() {
const [jobs, setJobs] = useState<ScheduleJob[]>([])
const [agentRoles, setAgentRoles] = useState<AgentRole[]>([])
const [loading, setLoading] = useState(true)
const [showForm, setShowForm] = useState(false)
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null)
const [deleteConfirm, setDeleteConfirm] = useState<string | null>(null)
// Form state
const [jobId, setJobId] = useState('')
const [target, setTarget] = useState('')
const [scanType, setScanType] = useState('quick')
const [scheduleMode, setScheduleMode] = useState<'interval' | 'preset' | 'days'>('preset')
const [cronPreset, setCronPreset] = useState('0 2 * * *')
const [customCron, setCustomCron] = useState('')
const [intervalMinutes, setIntervalMinutes] = useState('60')
const [selectedDays, setSelectedDays] = useState<number[]>([1, 2, 3, 4, 5])
const [executionHour, setExecutionHour] = useState('02')
const [executionMinute, setExecutionMinute] = useState('00')
const [agentRole, setAgentRole] = useState('')
const [showRoleDropdown, setShowRoleDropdown] = useState(false)
const [isCreating, setIsCreating] = useState(false)
const fetchData = useCallback(async () => {
setLoading(true)
try {
const [jobsData, rolesData] = await Promise.all([
schedulerApi.list(),
schedulerApi.getAgentRoles(),
])
setJobs(jobsData)
setAgentRoles(rolesData)
} catch (error) {
console.error('Failed to fetch scheduler data:', error)
} finally {
setLoading(false)
}
}, [])
useEffect(() => {
fetchData()
}, [fetchData])
// Auto-dismiss messages after 4 seconds
useEffect(() => {
if (message) {
const timer = setTimeout(() => setMessage(null), 4000)
return () => clearTimeout(timer)
}
}, [message])
const buildCronExpression = (): string | undefined => {
if (scheduleMode === 'interval') return undefined
if (scheduleMode === 'preset') {
return cronPreset === 'custom' ? customCron : cronPreset
}
// days mode: build cron from selected days + time
if (selectedDays.length === 0) return undefined
const daysStr = selectedDays.sort((a, b) => a - b).join(',')
return `${executionMinute} ${executionHour} * * ${daysStr}`
}
const getIntervalMinutes = (): number | undefined => {
if (scheduleMode !== 'interval') return undefined
return parseInt(intervalMinutes) || 60
}
const handleCreate = async () => {
if (!jobId.trim()) {
setMessage({ type: 'error', text: 'Job ID is required' })
return
}
if (!target.trim()) {
setMessage({ type: 'error', text: 'Target URL is required' })
return
}
const cron = buildCronExpression()
const interval = getIntervalMinutes()
if (!cron && !interval) {
setMessage({ type: 'error', text: 'Please configure a schedule (select days or set interval)' })
return
}
setIsCreating(true)
setMessage(null)
try {
await schedulerApi.create({
job_id: jobId.trim(),
target: target.trim(),
scan_type: scanType,
cron_expression: cron,
interval_minutes: interval,
agent_role: agentRole || undefined,
})
setMessage({ type: 'success', text: `Schedule "${jobId}" created successfully` })
setShowForm(false)
resetForm()
fetchData()
} catch (error: any) {
const detail = error?.response?.data?.detail || 'Failed to create schedule'
setMessage({ type: 'error', text: detail })
} finally {
setIsCreating(false)
}
}
const handleDelete = async (id: string) => {
try {
await schedulerApi.delete(id)
setMessage({ type: 'success', text: `Schedule "${id}" deleted` })
setDeleteConfirm(null)
fetchData()
} catch (error) {
setMessage({ type: 'error', text: `Failed to delete "${id}"` })
}
}
const handlePause = async (id: string) => {
try {
await schedulerApi.pause(id)
fetchData()
} catch (error) {
setMessage({ type: 'error', text: `Failed to pause "${id}"` })
}
}
const handleResume = async (id: string) => {
try {
await schedulerApi.resume(id)
fetchData()
} catch (error) {
setMessage({ type: 'error', text: `Failed to resume "${id}"` })
}
}
const toggleDay = (dayId: number) => {
setSelectedDays(prev =>
prev.includes(dayId) ? prev.filter(d => d !== dayId) : [...prev, dayId]
)
}
const resetForm = () => {
setJobId('')
setTarget('')
setScanType('quick')
setScheduleMode('preset')
setCronPreset('0 2 * * *')
setCustomCron('')
setIntervalMinutes('60')
setSelectedDays([1, 2, 3, 4, 5])
setExecutionHour('02')
setExecutionMinute('00')
setAgentRole('')
setShowRoleDropdown(false)
}
const selectedRole = agentRoles.find(r => r.id === agentRole)
return (
<div className="max-w-5xl mx-auto space-y-6 animate-fadeIn">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h2 className="text-2xl font-bold text-white flex items-center gap-3">
<div className="p-2 bg-brand-500/20 rounded-lg">
<Calendar className="w-6 h-6 text-brand-400" />
</div>
Scan Scheduler
</h2>
<p className="text-dark-400 mt-1 ml-14">Schedule automated recurring scans with agent specialization</p>
</div>
<div className="flex gap-2">
<Button variant="secondary" onClick={fetchData}>
<RefreshCw className="w-4 h-4 mr-2" />
Refresh
</Button>
<Button onClick={() => { setShowForm(!showForm); if (showForm) resetForm() }}>
<Plus className="w-4 h-4 mr-2" />
New Schedule
</Button>
</div>
</div>
{/* Status Message */}
{message && (
<div className={`flex items-center gap-3 p-4 rounded-lg border transition-all ${
message.type === 'success'
? 'bg-green-500/10 border-green-500/30 text-green-400'
: 'bg-red-500/10 border-red-500/30 text-red-400'
}`}>
{message.type === 'success'
? <CheckCircle2 className="w-5 h-5 flex-shrink-0" />
: <AlertTriangle className="w-5 h-5 flex-shrink-0" />
}
<span>{message.text}</span>
</div>
)}
{/* Create Form */}
{showForm && (
<div className="bg-dark-800 border border-dark-700 rounded-xl overflow-hidden">
<div className="p-5 border-b border-dark-700">
<h3 className="text-lg font-semibold text-white">Create New Schedule</h3>
<p className="text-dark-400 text-sm mt-1">Configure a recurring scan with specialized agent roles</p>
</div>
<div className="p-5 space-y-6">
{/* Row 1: Job ID + Target */}
<div className="grid grid-cols-2 gap-4">
<Input
label="Job ID"
placeholder="daily-scan-prod"
value={jobId}
onChange={(e) => setJobId(e.target.value)}
helperText="Unique identifier for this schedule"
/>
<Input
label="Target URL"
placeholder="https://example.com"
value={target}
onChange={(e) => setTarget(e.target.value)}
helperText="URL to scan on each execution"
/>
</div>
{/* Row 2: Scan Type */}
<div>
<label className="block text-sm font-medium text-dark-200 mb-3">Scan Type</label>
<div className="grid grid-cols-3 gap-3">
{SCAN_TYPES.map(({ id, label, icon: Icon, desc }) => (
<button
key={id}
onClick={() => setScanType(id)}
className={`p-4 rounded-lg border-2 text-left transition-all ${
scanType === id
? 'border-brand-500 bg-brand-500/10'
: 'border-dark-600 bg-dark-900/50 hover:border-dark-500'
}`}
>
<div className="flex items-center gap-2 mb-1">
<Icon className={`w-4 h-4 ${scanType === id ? 'text-brand-400' : 'text-dark-400'}`} />
<span className={`font-medium ${scanType === id ? 'text-white' : 'text-dark-300'}`}>{label}</span>
</div>
<p className="text-xs text-dark-500">{desc}</p>
</button>
))}
</div>
</div>
{/* Row 3: Agent Role Dropdown */}
<div>
<label className="block text-sm font-medium text-dark-200 mb-3">
<Shield className="w-4 h-4 inline mr-1 -mt-0.5" />
Agent Role
</label>
<div className="relative">
<button
onClick={() => setShowRoleDropdown(!showRoleDropdown)}
className="w-full flex items-center justify-between p-3 rounded-lg border border-dark-600 bg-dark-900/50 hover:border-dark-500 transition-colors text-left"
>
<div>
{selectedRole ? (
<>
<span className="text-white font-medium">{selectedRole.name}</span>
<span className="text-dark-500 text-sm ml-2">- {selectedRole.description}</span>
</>
) : (
<span className="text-dark-500">Select an agent role (optional)</span>
)}
</div>
<ChevronDown className={`w-4 h-4 text-dark-400 transition-transform ${showRoleDropdown ? 'rotate-180' : ''}`} />
</button>
{showRoleDropdown && (
<div className="absolute z-20 w-full mt-1 bg-dark-800 border border-dark-600 rounded-lg shadow-xl max-h-72 overflow-y-auto">
{/* None option */}
<button
onClick={() => { setAgentRole(''); setShowRoleDropdown(false) }}
className={`w-full flex items-start gap-3 p-3 text-left hover:bg-dark-700/50 transition-colors border-b border-dark-700/50 ${
!agentRole ? 'bg-dark-700/30' : ''
}`}
>
<div className="w-8 h-8 rounded-lg bg-dark-600 flex items-center justify-center flex-shrink-0 mt-0.5">
<Target className="w-4 h-4 text-dark-400" />
</div>
<div>
<p className="text-dark-300 font-medium">Default Agent</p>
<p className="text-dark-500 text-xs">No specialization - uses general pentest agent</p>
</div>
</button>
{agentRoles.map((role) => (
<button
key={role.id}
onClick={() => { setAgentRole(role.id); setShowRoleDropdown(false) }}
className={`w-full flex items-start gap-3 p-3 text-left hover:bg-dark-700/50 transition-colors ${
agentRole === role.id ? 'bg-brand-500/10 border-l-2 border-brand-500' : ''
}`}
>
<div className={`w-8 h-8 rounded-lg flex items-center justify-center flex-shrink-0 mt-0.5 ${
agentRole === role.id ? 'bg-brand-500/20' : 'bg-dark-600'
}`}>
<Shield className={`w-4 h-4 ${agentRole === role.id ? 'text-brand-400' : 'text-dark-400'}`} />
</div>
<div className="min-w-0">
<p className={`font-medium ${agentRole === role.id ? 'text-brand-400' : 'text-white'}`}>
{role.name}
</p>
<p className="text-dark-500 text-xs mt-0.5">{role.description}</p>
{role.tools.length > 0 && (
<div className="flex flex-wrap gap-1 mt-1.5">
{role.tools.slice(0, 4).map(tool => (
<span key={tool} className="px-1.5 py-0.5 text-[10px] bg-dark-600 text-dark-300 rounded">
{tool}
</span>
))}
{role.tools.length > 4 && (
<span className="px-1.5 py-0.5 text-[10px] bg-dark-600 text-dark-400 rounded">
+{role.tools.length - 4}
</span>
)}
</div>
)}
</div>
</button>
))}
</div>
)}
</div>
</div>
{/* Row 4: Schedule Configuration */}
<div>
<label className="block text-sm font-medium text-dark-200 mb-3">
<Clock className="w-4 h-4 inline mr-1 -mt-0.5" />
Schedule
</label>
{/* Schedule mode tabs */}
<div className="flex gap-1 p-1 bg-dark-900/50 rounded-lg mb-4">
{[
{ id: 'preset' as const, label: 'Presets' },
{ id: 'days' as const, label: 'Days & Time' },
{ id: 'interval' as const, label: 'Interval' },
].map(tab => (
<button
key={tab.id}
onClick={() => setScheduleMode(tab.id)}
className={`flex-1 py-2 px-3 rounded-md text-sm font-medium transition-all ${
scheduleMode === tab.id
? 'bg-brand-500 text-white shadow-sm'
: 'text-dark-400 hover:text-dark-200'
}`}
>
{tab.label}
</button>
))}
</div>
{/* Preset mode */}
{scheduleMode === 'preset' && (
<div className="space-y-3">
<div className="grid grid-cols-2 gap-2">
{CRON_PRESETS.map(preset => (
<button
key={preset.value}
onClick={() => setCronPreset(preset.value)}
className={`p-3 rounded-lg border text-left transition-all ${
cronPreset === preset.value
? 'border-brand-500 bg-brand-500/10'
: 'border-dark-600 bg-dark-900/30 hover:border-dark-500'
}`}
>
<p className={`text-sm font-medium ${cronPreset === preset.value ? 'text-brand-400' : 'text-dark-200'}`}>
{preset.label}
</p>
<p className="text-xs text-dark-500 mt-0.5">{preset.desc}</p>
</button>
))}
</div>
{cronPreset === 'custom' && (
<Input
label="Custom Cron Expression"
placeholder="*/30 * * * *"
value={customCron}
onChange={(e) => setCustomCron(e.target.value)}
helperText="Format: minute hour day-of-month month day-of-week"
/>
)}
</div>
)}
{/* Days & Time mode */}
{scheduleMode === 'days' && (
<div className="space-y-4">
<div>
<p className="text-sm text-dark-400 mb-2">Select days of the week</p>
<div className="flex gap-2">
{DAYS_OF_WEEK.map(day => (
<button
key={day.id}
onClick={() => toggleDay(day.id)}
className={`flex-1 py-3 rounded-lg border-2 text-center text-sm font-medium transition-all ${
selectedDays.includes(day.id)
? 'border-brand-500 bg-brand-500/15 text-brand-400'
: 'border-dark-600 bg-dark-900/30 text-dark-400 hover:border-dark-500'
}`}
title={day.full}
>
{day.short}
</button>
))}
</div>
<div className="flex gap-2 mt-2">
<button
onClick={() => setSelectedDays([1, 2, 3, 4, 5])}
className="text-xs text-brand-400 hover:text-brand-300 transition-colors"
>
Weekdays
</button>
<span className="text-dark-600">|</span>
<button
onClick={() => setSelectedDays([0, 6])}
className="text-xs text-brand-400 hover:text-brand-300 transition-colors"
>
Weekends
</button>
<span className="text-dark-600">|</span>
<button
onClick={() => setSelectedDays([0, 1, 2, 3, 4, 5, 6])}
className="text-xs text-brand-400 hover:text-brand-300 transition-colors"
>
Every Day
</button>
</div>
</div>
<div>
<p className="text-sm text-dark-400 mb-2">Execution Time</p>
<div className="flex items-center gap-2">
<select
value={executionHour}
onChange={(e) => setExecutionHour(e.target.value)}
className="bg-dark-900 border border-dark-600 rounded-lg px-3 py-2.5 text-white text-sm focus:border-brand-500 focus:outline-none"
>
{Array.from({ length: 24 }, (_, i) => (
<option key={i} value={String(i).padStart(2, '0')}>
{String(i).padStart(2, '0')}
</option>
))}
</select>
<span className="text-dark-400 text-lg font-bold">:</span>
<select
value={executionMinute}
onChange={(e) => setExecutionMinute(e.target.value)}
className="bg-dark-900 border border-dark-600 rounded-lg px-3 py-2.5 text-white text-sm focus:border-brand-500 focus:outline-none"
>
{['00', '15', '30', '45'].map(m => (
<option key={m} value={m}>{m}</option>
))}
</select>
<span className="text-dark-500 text-sm ml-2">UTC</span>
</div>
</div>
{selectedDays.length > 0 && (
<div className="p-3 bg-dark-900/50 rounded-lg border border-dark-700/50">
<p className="text-xs text-dark-400">
Cron: <code className="text-brand-400 bg-dark-700 px-1.5 py-0.5 rounded">
{`${executionMinute} ${executionHour} * * ${selectedDays.sort((a, b) => a - b).join(',')}`}
</code>
</p>
</div>
)}
</div>
)}
{/* Interval mode */}
{scheduleMode === 'interval' && (
<div className="space-y-3">
<div className="grid grid-cols-4 gap-2">
{[
{ label: '15 min', value: '15' },
{ label: '30 min', value: '30' },
{ label: '1 hour', value: '60' },
{ label: '2 hours', value: '120' },
{ label: '4 hours', value: '240' },
{ label: '6 hours', value: '360' },
{ label: '12 hours', value: '720' },
{ label: '24 hours', value: '1440' },
].map(opt => (
<button
key={opt.value}
onClick={() => setIntervalMinutes(opt.value)}
className={`py-2.5 px-3 rounded-lg border text-sm font-medium transition-all ${
intervalMinutes === opt.value
? 'border-brand-500 bg-brand-500/10 text-brand-400'
: 'border-dark-600 bg-dark-900/30 text-dark-400 hover:border-dark-500'
}`}
>
{opt.label}
</button>
))}
</div>
<Input
label="Custom interval (minutes)"
type="number"
min="1"
value={intervalMinutes}
onChange={(e) => setIntervalMinutes(e.target.value)}
helperText={`Scan runs every ${parseInt(intervalMinutes) >= 60 ? `${Math.floor(parseInt(intervalMinutes) / 60)}h ${parseInt(intervalMinutes) % 60}m` : `${intervalMinutes} minutes`}`}
/>
</div>
)}
</div>
{/* Actions */}
<div className="flex items-center justify-between pt-2 border-t border-dark-700">
<p className="text-xs text-dark-500">
{scheduleMode === 'interval'
? `Runs every ${parseInt(intervalMinutes) >= 60 ? `${Math.floor(parseInt(intervalMinutes) / 60)} hour(s)` : `${intervalMinutes} min`}`
: scheduleMode === 'days' && selectedDays.length > 0
? `Runs on ${selectedDays.sort((a,b)=>a-b).map(d => DAYS_OF_WEEK[d].short).join(', ')} at ${executionHour}:${executionMinute}`
: scheduleMode === 'preset' && cronPreset !== 'custom'
? CRON_PRESETS.find(p => p.value === cronPreset)?.desc || ''
: ''
}
</p>
<div className="flex gap-3">
<Button variant="secondary" onClick={() => { setShowForm(false); resetForm() }}>
Cancel
</Button>
<Button onClick={handleCreate} isLoading={isCreating}>
<Plus className="w-4 h-4 mr-2" />
Create Schedule
</Button>
</div>
</div>
</div>
</div>
)}
{/* Summary Stats */}
{jobs.length > 0 && (
<div className="grid grid-cols-3 gap-4">
<div className="bg-dark-800/50 border border-dark-700/50 rounded-lg p-4">
<p className="text-dark-400 text-sm">Total Schedules</p>
<p className="text-2xl font-bold text-white mt-1">{jobs.length}</p>
</div>
<div className="bg-dark-800/50 border border-dark-700/50 rounded-lg p-4">
<p className="text-dark-400 text-sm">Active</p>
<p className="text-2xl font-bold text-green-400 mt-1">{jobs.filter(j => j.status === 'active').length}</p>
</div>
<div className="bg-dark-800/50 border border-dark-700/50 rounded-lg p-4">
<p className="text-dark-400 text-sm">Total Runs</p>
<p className="text-2xl font-bold text-brand-400 mt-1">{jobs.reduce((sum, j) => sum + j.run_count, 0)}</p>
</div>
</div>
)}
{/* Jobs List */}
<div>
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-white">
Scheduled Jobs
<span className="text-dark-500 text-sm font-normal ml-2">
{jobs.length} job{jobs.length !== 1 ? 's' : ''}
</span>
</h3>
</div>
{loading ? (
<div className="flex items-center justify-center py-16">
<RefreshCw className="w-6 h-6 text-dark-400 animate-spin" />
</div>
) : jobs.length === 0 ? (
<Card>
<div className="text-center py-16">
<div className="w-16 h-16 bg-dark-700/50 rounded-full flex items-center justify-center mx-auto mb-4">
<Calendar className="w-8 h-8 text-dark-500" />
</div>
<p className="text-dark-300 font-medium">No scheduled jobs yet</p>
<p className="text-dark-500 text-sm mt-1 mb-4">Create a schedule to run automated recurring scans</p>
<Button onClick={() => setShowForm(true)}>
<Plus className="w-4 h-4 mr-2" />
Create First Schedule
</Button>
</div>
</Card>
) : (
<div className="space-y-3">
{jobs.map((job) => (
<div
key={job.id}
className="bg-dark-800 border border-dark-700/50 rounded-xl p-5 hover:border-dark-600 transition-colors"
>
<div className="flex items-start justify-between">
<div className="flex items-start gap-4">
{/* Status indicator */}
<div className={`w-10 h-10 rounded-lg flex items-center justify-center flex-shrink-0 ${
job.status === 'active' ? 'bg-green-500/15' : 'bg-yellow-500/15'
}`}>
{job.status === 'active'
? <Play className="w-5 h-5 text-green-400" />
: <Pause className="w-5 h-5 text-yellow-400" />
}
</div>
<div>
<div className="flex items-center gap-3">
<p className="font-semibold text-white text-lg">{job.id}</p>
<span className={`px-2.5 py-0.5 text-xs rounded-full font-medium ${
job.status === 'active'
? 'bg-green-500/15 text-green-400 border border-green-500/30'
: 'bg-yellow-500/15 text-yellow-400 border border-yellow-500/30'
}`}>
{job.status}
</span>
<span className="px-2 py-0.5 text-xs rounded bg-dark-700 text-dark-300">
{job.scan_type}
</span>
{job.agent_role && (
<span className="px-2 py-0.5 text-xs rounded bg-brand-500/15 text-brand-400 border border-brand-500/30">
{job.agent_role.replace(/_/g, ' ')}
</span>
)}
</div>
<div className="flex items-center gap-4 mt-2 text-sm text-dark-400">
<span className="flex items-center gap-1.5">
<Target className="w-3.5 h-3.5" />
{job.target}
</span>
<span className="flex items-center gap-1.5">
<Clock className="w-3.5 h-3.5" />
{job.schedule}
</span>
{job.run_count > 0 && (
<span className="flex items-center gap-1.5">
<RefreshCw className="w-3.5 h-3.5" />
{job.run_count} run{job.run_count !== 1 ? 's' : ''}
</span>
)}
</div>
{(job.next_run || job.last_run) && (
<div className="flex items-center gap-4 mt-1.5 text-xs text-dark-500">
{job.next_run && (
<span>Next: {new Date(job.next_run).toLocaleString()}</span>
)}
{job.last_run && (
<span>Last: {new Date(job.last_run).toLocaleString()}</span>
)}
</div>
)}
</div>
</div>
{/* Actions */}
<div className="flex items-center gap-1">
{job.status === 'active' ? (
<Button variant="ghost" size="sm" onClick={() => handlePause(job.id)} title="Pause schedule">
<Pause className="w-4 h-4 text-yellow-400" />
</Button>
) : (
<Button variant="ghost" size="sm" onClick={() => handleResume(job.id)} title="Resume schedule">
<Play className="w-4 h-4 text-green-400" />
</Button>
)}
{deleteConfirm === job.id ? (
<div className="flex items-center gap-1 ml-2">
<Button variant="ghost" size="sm" onClick={() => handleDelete(job.id)}>
<span className="text-red-400 text-xs font-medium">Confirm</span>
</Button>
<Button variant="ghost" size="sm" onClick={() => setDeleteConfirm(null)}>
<span className="text-dark-400 text-xs">Cancel</span>
</Button>
</div>
) : (
<Button variant="ghost" size="sm" onClick={() => setDeleteConfirm(job.id)} title="Delete schedule">
<Trash2 className="w-4 h-4 text-red-400" />
</Button>
)}
</div>
</div>
</div>
))}
</div>
)}
</div>
</div>
)
}
+106 -13
View File
@@ -1,5 +1,5 @@
import { useState, useEffect } from 'react'
import { Save, Shield, Trash2, RefreshCw, AlertTriangle } from 'lucide-react'
import { Save, Shield, Trash2, RefreshCw, AlertTriangle, Brain, Router, Eye } from 'lucide-react'
import Card from '../components/common/Card'
import Button from '../components/common/Button'
import Input from '../components/common/Input'
@@ -8,10 +8,15 @@ interface Settings {
llm_provider: string
has_anthropic_key: boolean
has_openai_key: boolean
has_openrouter_key: boolean
max_concurrent_scans: number
aggressive_mode: boolean
default_scan_type: string
recon_enabled_by_default: boolean
enable_model_routing: boolean
enable_knowledge_augmentation: boolean
enable_browser_validation: boolean
max_output_tokens: number | null
}
interface DbStats {
@@ -26,9 +31,14 @@ export default function SettingsPage() {
const [dbStats, setDbStats] = useState<DbStats | null>(null)
const [apiKey, setApiKey] = useState('')
const [openaiKey, setOpenaiKey] = useState('')
const [openrouterKey, setOpenrouterKey] = useState('')
const [llmProvider, setLlmProvider] = useState('claude')
const [maxConcurrentScans, setMaxConcurrentScans] = useState('3')
const [maxOutputTokens, setMaxOutputTokens] = useState('')
const [aggressiveMode, setAggressiveMode] = useState(false)
const [enableModelRouting, setEnableModelRouting] = useState(false)
const [enableKnowledgeAugmentation, setEnableKnowledgeAugmentation] = useState(false)
const [enableBrowserValidation, setEnableBrowserValidation] = useState(false)
const [isSaving, setIsSaving] = useState(false)
const [isClearing, setIsClearing] = useState(false)
const [showClearConfirm, setShowClearConfirm] = useState(false)
@@ -48,6 +58,10 @@ export default function SettingsPage() {
setLlmProvider(data.llm_provider)
setMaxConcurrentScans(String(data.max_concurrent_scans))
setAggressiveMode(data.aggressive_mode)
setEnableModelRouting(data.enable_model_routing ?? false)
setEnableKnowledgeAugmentation(data.enable_knowledge_augmentation ?? false)
setEnableBrowserValidation(data.enable_browser_validation ?? false)
setMaxOutputTokens(data.max_output_tokens ? String(data.max_output_tokens) : '')
}
} catch (error) {
console.error('Failed to fetch settings:', error)
@@ -78,8 +92,13 @@ export default function SettingsPage() {
llm_provider: llmProvider,
anthropic_api_key: apiKey || undefined,
openai_api_key: openaiKey || undefined,
openrouter_api_key: openrouterKey || undefined,
max_concurrent_scans: parseInt(maxConcurrentScans),
aggressive_mode: aggressiveMode
aggressive_mode: aggressiveMode,
enable_model_routing: enableModelRouting,
enable_knowledge_augmentation: enableKnowledgeAugmentation,
enable_browser_validation: enableBrowserValidation,
max_output_tokens: maxOutputTokens ? parseInt(maxOutputTokens) : null
})
})
@@ -88,6 +107,7 @@ export default function SettingsPage() {
setSettings(data)
setApiKey('')
setOpenaiKey('')
setOpenrouterKey('')
setMessage({ type: 'success', text: 'Settings saved successfully!' })
} else {
setMessage({ type: 'error', text: 'Failed to save settings' })
@@ -123,6 +143,15 @@ export default function SettingsPage() {
}
}
const ToggleSwitch = ({ enabled, onToggle }: { enabled: boolean; onToggle: () => void }) => (
<button
onClick={onToggle}
className={`w-12 h-6 rounded-full transition-colors ${enabled ? 'bg-primary-500' : 'bg-dark-700'}`}
>
<div className={`w-5 h-5 bg-white rounded-full shadow-md transform transition-transform ${enabled ? 'translate-x-6' : 'translate-x-0.5'}`} />
</button>
)
return (
<div className="max-w-2xl mx-auto space-y-6 animate-fadeIn">
{/* Status Message */}
@@ -139,14 +168,14 @@ export default function SettingsPage() {
<label className="block text-sm font-medium text-dark-200 mb-2">
LLM Provider
</label>
<div className="flex gap-2">
{['claude', 'openai', 'ollama'].map((provider) => (
<div className="flex gap-2 flex-wrap">
{['claude', 'openai', 'openrouter', 'ollama'].map((provider) => (
<Button
key={provider}
variant={llmProvider === provider ? 'primary' : 'secondary'}
onClick={() => setLlmProvider(provider)}
>
{provider.charAt(0).toUpperCase() + provider.slice(1)}
{provider === 'openrouter' ? 'OpenRouter' : provider.charAt(0).toUpperCase() + provider.slice(1)}
</Button>
))}
</div>
@@ -173,6 +202,72 @@ export default function SettingsPage() {
helperText={settings?.has_openai_key ? 'API key is configured. Enter a new key to update.' : 'Required for OpenAI-powered analysis'}
/>
)}
{llmProvider === 'openrouter' && (
<Input
label="OpenRouter API Key"
type="password"
placeholder={settings?.has_openrouter_key ? '••••••••••••••••' : 'sk-or-...'}
value={openrouterKey}
onChange={(e) => setOpenrouterKey(e.target.value)}
helperText={settings?.has_openrouter_key ? 'API key is configured. Enter a new key to update.' : 'Required for OpenRouter model access'}
/>
)}
<Input
label="Max Output Tokens"
type="number"
min="1024"
max="64000"
placeholder="Default (profile-based)"
value={maxOutputTokens}
onChange={(e) => setMaxOutputTokens(e.target.value)}
helperText="Override max output tokens (up to 64000 for Claude). Leave empty for profile defaults."
/>
</div>
</Card>
{/* Advanced Features */}
<Card title="Advanced Features" subtitle="Optional AI enhancement modules">
<div className="space-y-3">
<div className="flex items-center justify-between p-4 bg-dark-900/50 rounded-lg">
<div className="flex items-center gap-3">
<Router className="w-5 h-5 text-blue-400" />
<div>
<p className="font-medium text-white">Model Routing</p>
<p className="text-sm text-dark-400">
Route tasks to specialized LLM profiles by type (reasoning, analysis, generation)
</p>
</div>
</div>
<ToggleSwitch enabled={enableModelRouting} onToggle={() => setEnableModelRouting(!enableModelRouting)} />
</div>
<div className="flex items-center justify-between p-4 bg-dark-900/50 rounded-lg">
<div className="flex items-center gap-3">
<Brain className="w-5 h-5 text-purple-400" />
<div>
<p className="font-medium text-white">Knowledge Augmentation</p>
<p className="text-sm text-dark-400">
Enrich AI context with bug bounty pattern datasets (19 vuln types)
</p>
</div>
</div>
<ToggleSwitch enabled={enableKnowledgeAugmentation} onToggle={() => setEnableKnowledgeAugmentation(!enableKnowledgeAugmentation)} />
</div>
<div className="flex items-center justify-between p-4 bg-dark-900/50 rounded-lg">
<div className="flex items-center gap-3">
<Eye className="w-5 h-5 text-green-400" />
<div>
<p className="font-medium text-white">Browser Validation</p>
<p className="text-sm text-dark-400">
Playwright-based browser validation with screenshot capture
</p>
</div>
</div>
<ToggleSwitch enabled={enableBrowserValidation} onToggle={() => setEnableBrowserValidation(!enableBrowserValidation)} />
</div>
</div>
</Card>
@@ -196,12 +291,7 @@ export default function SettingsPage() {
Use more payloads and bypass techniques (may be slower)
</p>
</div>
<button
onClick={() => setAggressiveMode(!aggressiveMode)}
className={`w-12 h-6 rounded-full transition-colors ${aggressiveMode ? 'bg-primary-500' : 'bg-dark-700'}`}
>
<div className={`w-5 h-5 bg-white rounded-full shadow-md transform transition-transform ${aggressiveMode ? 'translate-x-6' : 'translate-x-0.5'}`} />
</button>
<ToggleSwitch enabled={aggressiveMode} onToggle={() => setAggressiveMode(!aggressiveMode)} />
</div>
</div>
</Card>
@@ -290,8 +380,11 @@ export default function SettingsPage() {
<div className="text-sm text-dark-400 space-y-1">
<p>Dynamic vulnerability testing driven by AI prompts</p>
<p>50+ vulnerability types across 10 categories</p>
<p>Real-time dashboard with WebSocket updates</p>
<p>Professional HTML/PDF/JSON reports</p>
<p>Multi-provider LLM support (Claude, GPT, OpenRouter, Ollama)</p>
<p>Task-type model routing and knowledge augmentation</p>
<p>Playwright browser validation with screenshot capture</p>
<p>OHVR-structured PoC reporting</p>
<p>Scheduled recurring scans with cron/interval triggers</p>
</div>
</div>
</Card>
File diff suppressed because it is too large. Load diff
+995
View File
@@ -0,0 +1,995 @@
import { useState, useEffect, useRef, useCallback } from 'react'
import { useNavigate } from 'react-router-dom'
import {
FlaskConical, ChevronDown, ChevronUp, Loader2, Lock,
AlertTriangle, CheckCircle2, XCircle, Play, Square,
Trash2, Eye, Search, BarChart3, Clock, Target,
Terminal, Shield, Globe, FileText, ChevronRight
} from 'lucide-react'
import { vulnLabApi } from '../services/api'
import type { VulnTypeCategory, VulnLabChallenge, VulnLabStats, VulnLabLogEntry } from '../types'
const SEVERITY_COLORS: Record<string, string> = {
critical: 'bg-red-500',
high: 'bg-orange-500',
medium: 'bg-yellow-500',
low: 'bg-blue-500',
info: 'bg-gray-500',
}
const RESULT_BADGE: Record<string, { bg: string; text: string; label: string }> = {
detected: { bg: 'bg-green-500/20', text: 'text-green-400', label: 'Detected' },
not_detected: { bg: 'bg-red-500/20', text: 'text-red-400', label: 'Not Detected' },
error: { bg: 'bg-yellow-500/20', text: 'text-yellow-400', label: 'Error' },
}
const STATUS_BADGE: Record<string, { bg: string; text: string }> = {
running: { bg: 'bg-blue-500/20', text: 'text-blue-400' },
completed: { bg: 'bg-green-500/20', text: 'text-green-400' },
failed: { bg: 'bg-red-500/20', text: 'text-red-400' },
stopped: { bg: 'bg-orange-500/20', text: 'text-orange-400' },
pending: { bg: 'bg-gray-500/20', text: 'text-gray-400' },
}
const LOG_LEVEL_COLORS: Record<string, string> = {
error: 'text-red-400',
warning: 'text-yellow-400',
info: 'text-blue-300',
debug: 'text-dark-500',
critical: 'text-red-500 font-bold',
}
function LogLine({ log }: { log: VulnLabLogEntry }) {
const color = LOG_LEVEL_COLORS[log.level] || 'text-dark-400'
const time = log.time ? new Date(log.time).toLocaleTimeString() : ''
const isLlm = log.source === 'llm'
return (
<div className={`flex gap-2 text-xs font-mono leading-relaxed ${color}`}>
<span className="text-dark-600 shrink-0 w-16">{time}</span>
<span className={`shrink-0 w-12 uppercase ${
log.level === 'error' ? 'text-red-500' :
log.level === 'warning' ? 'text-yellow-500' :
'text-dark-600'
}`}>{log.level}</span>
{isLlm && <span className="text-purple-500 shrink-0">[AI]</span>}
<span className="break-all">{log.message}</span>
</div>
)
}
export default function VulnLabPage() {
const navigate = useNavigate()
// Form state
const [targetUrl, setTargetUrl] = useState('')
const [challengeName, setChallengeName] = useState('')
const [selectedVulnType, setSelectedVulnType] = useState('')
const [showAuth, setShowAuth] = useState(false)
const [authType, setAuthType] = useState('')
const [authValue, setAuthValue] = useState('')
const [notes, setNotes] = useState('')
const [searchFilter, setSearchFilter] = useState('')
// Data state
const [categories, setCategories] = useState<Record<string, VulnTypeCategory>>({})
const [expandedCat, setExpandedCat] = useState<string | null>(null)
const [challenges, setChallenges] = useState<VulnLabChallenge[]>([])
const [stats, setStats] = useState<VulnLabStats | null>(null)
// Running state
const [isRunning, setIsRunning] = useState(false)
const [runningChallengeId, setRunningChallengeId] = useState<string | null>(null)
const [runningStatus, setRunningStatus] = useState<any>(null)
const [runningLogs, setRunningLogs] = useState<VulnLabLogEntry[]>([])
const [error, setError] = useState<string | null>(null)
const [activeTab, setActiveTab] = useState<'test' | 'history' | 'stats'>('test')
const [showLogs, setShowLogs] = useState(true)
const [logFilter, setLogFilter] = useState<'all' | 'info' | 'warning' | 'error'>('all')
// History expansion state
const [expandedChallenge, setExpandedChallenge] = useState<string | null>(null)
const [expandedChallengeData, setExpandedChallengeData] = useState<any>(null)
const [loadingChallenge, setLoadingChallenge] = useState(false)
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null)
const logsEndRef = useRef<HTMLDivElement>(null)
const autoScrollRef = useRef(true)
// Load vuln types on mount
useEffect(() => {
vulnLabApi.getTypes().then(data => {
setCategories(data.categories)
}).catch(() => {})
loadChallenges()
loadStats()
}, [])
// Auto-scroll logs
useEffect(() => {
if (autoScrollRef.current && logsEndRef.current) {
logsEndRef.current.scrollIntoView({ behavior: 'smooth' })
}
}, [runningLogs])
// Poll running challenge (3s for faster updates)
useEffect(() => {
if (!runningChallengeId || !isRunning) return
const poll = async () => {
try {
const s = await vulnLabApi.getChallenge(runningChallengeId)
setRunningStatus(s)
if (s.logs) setRunningLogs(s.logs)
if (['completed', 'failed', 'stopped', 'error'].includes(s.status)) {
setIsRunning(false)
if (pollRef.current) clearInterval(pollRef.current)
loadChallenges()
loadStats()
}
} catch {}
}
poll()
pollRef.current = setInterval(poll, 3000)
return () => { if (pollRef.current) clearInterval(pollRef.current) }
}, [runningChallengeId, isRunning])
const loadChallenges = async () => {
try {
const data = await vulnLabApi.listChallenges({ limit: 50 })
setChallenges(data.challenges)
} catch {}
}
const loadStats = async () => {
try {
const data = await vulnLabApi.getStats()
setStats(data)
} catch {}
}
const handleStart = async () => {
if (!targetUrl.trim() || !selectedVulnType) return
setError(null)
setIsRunning(true)
setRunningStatus(null)
setRunningLogs([])
setShowLogs(true)
try {
const resp = await vulnLabApi.run({
target_url: targetUrl.trim(),
vuln_type: selectedVulnType,
challenge_name: challengeName || undefined,
auth_type: authType || undefined,
auth_value: authValue || undefined,
notes: notes || undefined,
})
setRunningChallengeId(resp.challenge_id)
} catch (err: any) {
setError(err?.response?.data?.detail || err?.message || 'Failed to start test')
setIsRunning(false)
}
}
const handleStop = async () => {
if (!runningChallengeId) return
try {
await vulnLabApi.stopChallenge(runningChallengeId)
setIsRunning(false)
} catch {}
}
const handleDelete = async (id: string) => {
try {
await vulnLabApi.deleteChallenge(id)
if (expandedChallenge === id) {
setExpandedChallenge(null)
setExpandedChallengeData(null)
}
loadChallenges()
loadStats()
} catch {}
}
const toggleChallengeExpand = useCallback(async (challengeId: string) => {
if (expandedChallenge === challengeId) {
setExpandedChallenge(null)
setExpandedChallengeData(null)
return
}
setExpandedChallenge(challengeId)
setLoadingChallenge(true)
try {
const data = await vulnLabApi.getChallenge(challengeId)
setExpandedChallengeData(data)
} catch {
setExpandedChallengeData(null)
} finally {
setLoadingChallenge(false)
}
}, [expandedChallenge])
// Get selected vuln type info
const getSelectedVulnInfo = () => {
for (const cat of Object.values(categories)) {
const found = cat.types.find(t => t.key === selectedVulnType)
if (found) return found
}
return null
}
const selectedInfo = getSelectedVulnInfo()
// Filter vuln types by search
const filteredCategories = Object.entries(categories).map(([key, cat]) => {
const filtered = searchFilter
? cat.types.filter(t =>
t.key.includes(searchFilter.toLowerCase()) ||
t.title.toLowerCase().includes(searchFilter.toLowerCase())
)
: cat.types
return { key, ...cat, types: filtered }
}).filter(c => c.types.length > 0)
const formatDuration = (seconds: number | null | undefined) => {
if (!seconds) return '-'
if (seconds < 60) return `${seconds}s`
const m = Math.floor(seconds / 60)
const s = seconds % 60
return `${m}m ${s}s`
}
// Filter running logs
const filteredLogs = logFilter === 'all'
? runningLogs
: runningLogs.filter(l => l.level === logFilter)
return (
<div className="min-h-screen flex flex-col items-center py-8 px-4">
{/* Header */}
<div className="text-center mb-8">
<div className="inline-flex items-center justify-center w-16 h-16 bg-purple-500/20 rounded-2xl mb-4">
<FlaskConical className="w-8 h-8 text-purple-400" />
</div>
<h1 className="text-3xl font-bold text-white mb-2">Vulnerability Lab</h1>
<p className="text-dark-400 max-w-lg">
Test individual vulnerability types against labs, CTFs, and PortSwigger challenges.
Track detection performance per vuln type.
</p>
</div>
{/* Tab Bar */}
<div className="flex gap-2 mb-6">
{[
{ key: 'test' as const, label: 'New Test', icon: Play },
{ key: 'history' as const, label: 'History', icon: Clock },
{ key: 'stats' as const, label: 'Stats', icon: BarChart3 },
].map(tab => (
<button
key={tab.key}
onClick={() => setActiveTab(tab.key)}
className={`flex items-center gap-2 px-5 py-2.5 rounded-lg text-sm font-medium transition-colors ${
activeTab === tab.key
? 'bg-purple-500/20 text-purple-400 border border-purple-500/30'
: 'bg-dark-800 text-dark-400 border border-dark-700 hover:text-white'
}`}
>
<tab.icon className="w-4 h-4" />
{tab.label}
</button>
))}
</div>
{/* ========== NEW TEST TAB ========== */}
{activeTab === 'test' && (
<div className="w-full max-w-3xl">
<div className="bg-dark-800 border border-dark-700 rounded-2xl p-8">
{/* Target URL */}
<div className="mb-6">
<label className="block text-sm font-medium text-dark-300 mb-2">Target URL</label>
<input
type="url"
value={targetUrl}
onChange={e => setTargetUrl(e.target.value)}
placeholder="https://lab.example.com/vuln-page"
disabled={isRunning}
className="w-full px-4 py-4 bg-dark-900 border border-dark-600 rounded-xl text-white text-lg placeholder-dark-500 focus:outline-none focus:border-purple-500 focus:ring-1 focus:ring-purple-500 disabled:opacity-50"
/>
</div>
{/* Challenge Name (optional) */}
<div className="mb-6">
<label className="block text-sm font-medium text-dark-300 mb-2">Challenge Name (optional)</label>
<input
type="text"
value={challengeName}
onChange={e => setChallengeName(e.target.value)}
placeholder={selectedVulnType?.startsWith('xss')
? 'e.g. "Reflected XSS with most tags and attributes blocked"'
: "e.g. PortSwigger Lab: Reflected XSS into HTML context"}
disabled={isRunning}
className="w-full px-4 py-3 bg-dark-900 border border-dark-600 rounded-xl text-white placeholder-dark-500 focus:outline-none focus:border-purple-500 disabled:opacity-50"
/>
</div>
{/* Vulnerability Type Selector */}
<div className="mb-6">
<label className="block text-sm font-medium text-dark-300 mb-2">
Vulnerability Type {selectedInfo && (
<span className={`ml-2 px-2 py-0.5 rounded text-xs ${SEVERITY_COLORS[selectedInfo.severity]} text-white`}>
{selectedInfo.severity}
</span>
)}
</label>
{/* Search */}
<div className="relative mb-3">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-dark-500" />
<input
type="text"
value={searchFilter}
onChange={e => setSearchFilter(e.target.value)}
placeholder="Search vuln types..."
disabled={isRunning}
className="w-full pl-10 pr-4 py-2.5 bg-dark-900 border border-dark-600 rounded-lg text-white text-sm placeholder-dark-500 focus:outline-none focus:border-purple-500 disabled:opacity-50"
/>
</div>
{/* Selected indicator */}
{selectedInfo && (
<div className="mb-3 p-3 bg-purple-500/10 border border-purple-500/20 rounded-lg flex items-center justify-between">
<div>
<span className="text-purple-400 font-medium">{selectedInfo.title}</span>
{selectedInfo.cwe_id && (
<span className="ml-2 text-dark-500 text-xs">{selectedInfo.cwe_id}</span>
)}
</div>
<button
onClick={() => setSelectedVulnType('')}
disabled={isRunning}
className="text-dark-500 hover:text-white text-xs"
>
Clear
</button>
</div>
)}
{/* Category accordion */}
<div className="max-h-80 overflow-y-auto border border-dark-600 rounded-xl bg-dark-900">
{filteredCategories.map(cat => (
<div key={cat.key} className="border-b border-dark-700 last:border-b-0">
<button
onClick={() => setExpandedCat(expandedCat === cat.key ? null : cat.key)}
disabled={isRunning}
className="w-full flex items-center justify-between px-4 py-3 text-sm font-medium text-dark-300 hover:text-white hover:bg-dark-800 transition-colors disabled:opacity-50"
>
<span>{cat.label} ({cat.types.length})</span>
{expandedCat === cat.key ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />}
</button>
{expandedCat === cat.key && (
<div className="px-2 pb-2">
{cat.types.map(vtype => (
<button
key={vtype.key}
onClick={() => setSelectedVulnType(vtype.key)}
disabled={isRunning}
className={`w-full flex items-center justify-between px-3 py-2 rounded-lg text-sm transition-colors disabled:opacity-50 ${
selectedVulnType === vtype.key
? 'bg-purple-500/20 text-purple-400'
: 'text-dark-400 hover:bg-dark-800 hover:text-white'
}`}
>
<span className="text-left">{vtype.title}</span>
<div className="flex items-center gap-2">
{vtype.cwe_id && <span className="text-dark-600 text-xs">{vtype.cwe_id}</span>}
<span className={`w-2 h-2 rounded-full ${SEVERITY_COLORS[vtype.severity]}`} />
</div>
</button>
))}
</div>
)}
</div>
))}
</div>
</div>
{/* Auth Section */}
<div className="mb-6">
<button
onClick={() => setShowAuth(!showAuth)}
disabled={isRunning}
className="flex items-center gap-2 text-sm text-dark-400 hover:text-white transition-colors disabled:opacity-50"
>
<Lock className="w-4 h-4" />
<span>Authentication (Optional)</span>
{showAuth ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />}
</button>
{showAuth && (
<div className="mt-3 space-y-3 pl-6">
<select
value={authType}
onChange={e => setAuthType(e.target.value)}
disabled={isRunning}
className="w-full px-3 py-2 bg-dark-900 border border-dark-600 rounded-lg text-white text-sm focus:outline-none focus:border-purple-500"
>
<option value="">No Authentication</option>
<option value="bearer">Bearer Token</option>
<option value="cookie">Cookie</option>
<option value="basic">Basic Auth (user:pass)</option>
<option value="header">Custom Header (Name:Value)</option>
</select>
{authType && (
<input
type="text"
value={authValue}
onChange={e => setAuthValue(e.target.value)}
disabled={isRunning}
placeholder={
authType === 'bearer' ? 'eyJhbGciOiJIUzI1NiIs...' :
authType === 'cookie' ? 'session=abc123; token=xyz' :
authType === 'basic' ? 'admin:password123' :
'X-API-Key:your-api-key'
}
className="w-full px-3 py-2 bg-dark-900 border border-dark-600 rounded-lg text-white text-sm placeholder-dark-500 focus:outline-none focus:border-purple-500"
/>
)}
</div>
)}
</div>
{/* Notes */}
<div className="mb-6">
<label className="block text-sm font-medium text-dark-300 mb-2">Notes (optional)</label>
<textarea
value={notes}
onChange={e => setNotes(e.target.value)}
rows={2}
disabled={isRunning}
placeholder={selectedVulnType?.startsWith('xss')
? "Hints: blocked chars/tags, encoding observed, context (e.g. 'angle brackets HTML-encoded, input in onclick attribute')"
: "e.g. PortSwigger Apprentice level, no WAF"}
className="w-full px-4 py-3 bg-dark-900 border border-dark-600 rounded-xl text-white placeholder-dark-500 focus:outline-none focus:border-purple-500 disabled:opacity-50"
/>
</div>
{/* Error */}
{error && (
<div className="mb-6 p-3 bg-red-500/10 border border-red-500/20 rounded-lg flex items-center gap-2">
<AlertTriangle className="w-5 h-5 text-red-400" />
<span className="text-red-400 text-sm">{error}</span>
</div>
)}
{/* Start/Stop */}
{!isRunning ? (
<button
onClick={handleStart}
disabled={!targetUrl.trim() || !selectedVulnType}
className="w-full py-4 bg-purple-500 hover:bg-purple-600 disabled:bg-dark-600 disabled:text-dark-400 text-white font-bold text-lg rounded-xl transition-colors flex items-center justify-center gap-3"
>
<FlaskConical className="w-6 h-6" />
START TEST
</button>
) : (
<button
onClick={handleStop}
className="w-full py-4 bg-red-500 hover:bg-red-600 text-white font-bold text-lg rounded-xl transition-colors flex items-center justify-center gap-3"
>
<Square className="w-6 h-6" />
STOP TEST
</button>
)}
</div>
{/* Running Progress + Live Logs */}
{runningStatus && (
<div className="mt-6 space-y-4">
{/* Status Card */}
<div className="bg-dark-800 border border-dark-700 rounded-2xl p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-white font-semibold flex items-center gap-2">
{runningStatus.status === 'running' && <Loader2 className="w-5 h-5 animate-spin text-purple-400" />}
{runningStatus.status === 'completed' && <CheckCircle2 className="w-5 h-5 text-green-400" />}
{['failed', 'error'].includes(runningStatus.status) && <XCircle className="w-5 h-5 text-red-400" />}
{runningStatus.status === 'stopped' && <Square className="w-5 h-5 text-orange-400" />}
{selectedInfo?.title || selectedVulnType}
</h3>
<span className="text-sm text-dark-400">{runningStatus.progress || 0}%</span>
</div>
{/* Progress bar */}
<div className="w-full bg-dark-900 rounded-full h-2 mb-4">
<div
className={`h-2 rounded-full transition-all duration-500 ${
runningStatus.status === 'completed' ? 'bg-green-500' :
runningStatus.status === 'error' || runningStatus.status === 'failed' ? 'bg-red-500' :
'bg-purple-500'
}`}
style={{ width: `${runningStatus.progress || 0}%` }}
/>
</div>
{/* Info row */}
<div className="flex items-center gap-4 text-sm text-dark-400 mb-3">
{runningStatus.phase && (
<span className="flex items-center gap-1">
<Shield className="w-3.5 h-3.5" />
{runningStatus.phase}
</span>
)}
<span className="flex items-center gap-1">
<Terminal className="w-3.5 h-3.5" />
{runningLogs.length} log entries
</span>
{runningStatus.findings_count > 0 && (
<span className="flex items-center gap-1 text-green-400">
<AlertTriangle className="w-3.5 h-3.5" />
{runningStatus.findings_count} finding(s)
</span>
)}
</div>
{/* Findings preview */}
{runningStatus.findings && runningStatus.findings.length > 0 && (
<div className="mb-3 space-y-2">
{runningStatus.findings.slice(-3).map((f: any, i: number) => (
<div key={i} className="p-2 bg-green-500/5 border border-green-500/20 rounded-lg">
<div className="flex items-center gap-2">
<span className={`px-1.5 py-0.5 rounded text-xs font-bold ${SEVERITY_COLORS[f.severity || 'medium']} text-white`}>
{(f.severity || 'medium').toUpperCase()}
</span>
<span className="text-sm text-green-300">{f.title || f.vulnerability_type || 'Finding'}</span>
</div>
{f.affected_endpoint && (
<p className="text-xs text-dark-500 mt-1 truncate">{f.affected_endpoint}</p>
)}
</div>
))}
</div>
)}
{/* Result badge on completion */}
{runningStatus.result && (
<div className="mt-4 flex items-center gap-3">
<span className={`px-3 py-1 rounded-full text-sm font-medium ${
RESULT_BADGE[runningStatus.result]?.bg || 'bg-gray-500/20'
} ${RESULT_BADGE[runningStatus.result]?.text || 'text-gray-400'}`}>
{RESULT_BADGE[runningStatus.result]?.label || runningStatus.result}
</span>
{runningStatus.scan_id && (
<button
onClick={() => navigate(`/scan/${runningStatus.scan_id}`)}
className="text-sm text-purple-400 hover:text-purple-300 flex items-center gap-1"
>
<Eye className="w-4 h-4" /> View Scan Details
</button>
)}
</div>
)}
{/* Error */}
{runningStatus.error && (
<div className="mt-3 p-2 bg-red-500/10 border border-red-500/20 rounded text-red-400 text-sm">
{runningStatus.error}
</div>
)}
</div>
{/* Live Logs Panel */}
<div className="bg-dark-800 border border-dark-700 rounded-2xl overflow-hidden">
<div className="flex items-center justify-between px-4 py-3 border-b border-dark-700">
<button
onClick={() => setShowLogs(!showLogs)}
className="flex items-center gap-2 text-sm font-medium text-dark-300 hover:text-white transition-colors"
>
<Terminal className="w-4 h-4 text-purple-400" />
Live Agent Logs
<span className="text-dark-600 text-xs">({runningLogs.length})</span>
{showLogs ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />}
</button>
{showLogs && (
<div className="flex items-center gap-1">
{(['all', 'info', 'warning', 'error'] as const).map(level => (
<button
key={level}
onClick={() => setLogFilter(level)}
className={`px-2 py-1 rounded text-xs transition-colors ${
logFilter === level
? 'bg-purple-500/20 text-purple-400'
: 'text-dark-500 hover:text-white'
}`}
>
{level}
</button>
))}
</div>
)}
</div>
{showLogs && (
<div
className="p-3 bg-dark-900 max-h-80 overflow-y-auto space-y-0.5"
onScroll={(e) => {
const el = e.currentTarget
autoScrollRef.current = el.scrollTop + el.clientHeight >= el.scrollHeight - 50
}}
>
{filteredLogs.length === 0 ? (
<p className="text-dark-600 text-xs font-mono">Waiting for logs...</p>
) : (
filteredLogs.map((log, i) => <LogLine key={i} log={log} />)
)}
<div ref={logsEndRef} />
</div>
)}
</div>
</div>
)}
</div>
)}
{/* ========== HISTORY TAB ========== */}
{activeTab === 'history' && (
<div className="w-full max-w-4xl">
<div className="bg-dark-800 border border-dark-700 rounded-2xl overflow-hidden">
<div className="p-4 border-b border-dark-700 flex items-center justify-between">
<h3 className="text-white font-semibold">Challenge History ({challenges.length})</h3>
<button
onClick={loadChallenges}
className="text-sm text-dark-400 hover:text-white"
>
Refresh
</button>
</div>
{challenges.length === 0 ? (
<div className="p-8 text-center text-dark-500">
No challenges yet. Start your first test!
</div>
) : (
<div className="divide-y divide-dark-700">
{challenges.map(ch => {
const statusBadge = STATUS_BADGE[ch.status] || STATUS_BADGE.pending
const resultBadge = ch.result ? RESULT_BADGE[ch.result] : null
const isExpanded = expandedChallenge === ch.id
return (
<div key={ch.id}>
{/* Challenge row */}
<div
className={`p-4 cursor-pointer transition-colors ${
isExpanded ? 'bg-dark-900/80' : 'hover:bg-dark-900/50'
}`}
onClick={() => toggleChallengeExpand(ch.id)}
>
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-3">
<ChevronRight className={`w-4 h-4 text-dark-500 transition-transform ${isExpanded ? 'rotate-90' : ''}`} />
<span className="text-white font-medium">
{ch.challenge_name || ch.vuln_type.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase())}
</span>
<span className={`px-2 py-0.5 rounded text-xs ${statusBadge.bg} ${statusBadge.text}`}>
{ch.status}
</span>
{resultBadge && (
<span className={`px-2 py-0.5 rounded text-xs ${resultBadge.bg} ${resultBadge.text}`}>
{resultBadge.label}
</span>
)}
</div>
<div className="flex items-center gap-2" onClick={e => e.stopPropagation()}>
{ch.scan_id && (
<button
onClick={() => navigate(`/scan/${ch.scan_id}`)}
className="p-1.5 text-dark-400 hover:text-white rounded"
title="View scan details"
>
<Eye className="w-4 h-4" />
</button>
)}
<button
onClick={() => handleDelete(ch.id)}
className="p-1.5 text-dark-400 hover:text-red-400 rounded"
title="Delete"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
</div>
<div className="flex items-center gap-4 text-xs text-dark-500 ml-7">
<span className="flex items-center gap-1">
<Target className="w-3 h-3" />
{ch.target_url.length > 50 ? ch.target_url.slice(0, 50) + '...' : ch.target_url}
</span>
<span className="text-dark-600">|</span>
<span>{ch.vuln_type}</span>
{ch.vuln_category && (
<>
<span className="text-dark-600">|</span>
<span>{ch.vuln_category}</span>
</>
)}
<span className="text-dark-600">|</span>
<span className="flex items-center gap-1">
<Clock className="w-3 h-3" />
{formatDuration(ch.duration)}
</span>
{(ch.endpoints_count ?? 0) > 0 && (
<>
<span className="text-dark-600">|</span>
<span className="flex items-center gap-1">
<Globe className="w-3 h-3" />
{ch.endpoints_count} endpoints
</span>
</>
)}
{(ch.logs_count ?? 0) > 0 && (
<>
<span className="text-dark-600">|</span>
<span className="flex items-center gap-1">
<Terminal className="w-3 h-3" />
{ch.logs_count} logs
</span>
</>
)}
</div>
{/* Findings summary */}
{ch.findings_count > 0 && (
<div className="flex gap-2 mt-2 ml-7">
{(['critical', 'high', 'medium', 'low', 'info'] as const).map(sev => {
const count = ch[`${sev}_count` as keyof VulnLabChallenge] as number
if (!count) return null
return (
<span key={sev} className={`${SEVERITY_COLORS[sev]} text-white px-2 py-0.5 rounded text-xs font-bold`}>
{count} {sev}
</span>
)
})}
</div>
)}
</div>
{/* Expanded detail section */}
{isExpanded && (
<div className="border-t border-dark-700 bg-dark-900/50">
{loadingChallenge ? (
<div className="p-6 flex items-center justify-center gap-2 text-dark-400">
<Loader2 className="w-5 h-5 animate-spin" />
Loading details...
</div>
) : expandedChallengeData ? (
<div className="p-4 space-y-4">
{/* Findings Detail */}
{(expandedChallengeData.findings_detail || expandedChallengeData.findings || []).length > 0 && (
<div>
<h4 className="text-sm font-medium text-dark-300 mb-2 flex items-center gap-2">
<Shield className="w-4 h-4 text-green-400" />
Findings ({(expandedChallengeData.findings_detail || expandedChallengeData.findings || []).length})
</h4>
<div className="space-y-2">
{(expandedChallengeData.findings_detail || expandedChallengeData.findings || []).map((f: any, i: number) => (
<div key={i} className="p-3 bg-dark-800 border border-dark-700 rounded-lg">
<div className="flex items-center gap-2 mb-1">
<span className={`px-1.5 py-0.5 rounded text-xs font-bold ${SEVERITY_COLORS[f.severity || 'medium']} text-white`}>
{(f.severity || 'medium').toUpperCase()}
</span>
<span className="text-sm text-white font-medium">
{f.title || f.vulnerability_type || 'Finding'}
</span>
</div>
{f.vulnerability_type && (
<p className="text-xs text-dark-500 mb-1">Type: {f.vulnerability_type}</p>
)}
{f.affected_endpoint && (
<p className="text-xs text-dark-400 mb-1 flex items-center gap-1">
<Globe className="w-3 h-3" />
{f.affected_endpoint}
</p>
)}
{f.payload && (
<div className="mt-1">
<span className="text-xs text-dark-600">Payload: </span>
<code className="text-xs text-purple-400 bg-dark-900 px-1.5 py-0.5 rounded break-all">
{f.payload}
</code>
</div>
)}
{f.evidence && (
<div className="mt-1">
<span className="text-xs text-dark-600">Evidence: </span>
<span className="text-xs text-dark-400">{f.evidence.slice(0, 300)}</span>
</div>
)}
</div>
))}
</div>
</div>
)}
{/* No findings message */}
{(expandedChallengeData.findings_detail || expandedChallengeData.findings || []).length === 0 &&
expandedChallengeData.status !== 'running' && (
<div className="p-3 bg-dark-800 border border-dark-700 rounded-lg text-center text-dark-500 text-sm">
No findings detected for this challenge.
</div>
)}
{/* Agent Logs */}
{(expandedChallengeData.logs || []).length > 0 && (
<ChallengeLogsViewer logs={expandedChallengeData.logs} />
)}
{/* Notes */}
{expandedChallengeData.notes && (
<div className="p-3 bg-dark-800 border border-dark-700 rounded-lg">
<h4 className="text-xs font-medium text-dark-500 mb-1 flex items-center gap-1">
<FileText className="w-3 h-3" /> Notes
</h4>
<p className="text-sm text-dark-300">{expandedChallengeData.notes}</p>
</div>
)}
</div>
) : (
<div className="p-6 text-center text-dark-500 text-sm">
Failed to load challenge details.
</div>
)}
</div>
)}
</div>
)
})}
</div>
)}
</div>
</div>
)}
{/* ========== STATS TAB ========== */}
{activeTab === 'stats' && (
<div className="w-full max-w-4xl">
{!stats ? (
<div className="text-center text-dark-500 py-12">Loading stats...</div>
) : (
<div className="space-y-6">
{/* Overview cards */}
<div className="grid grid-cols-4 gap-4">
{[
{ label: 'Total Tests', value: stats.total, color: 'text-white' },
{ label: 'Running', value: stats.running, color: 'text-blue-400' },
{ label: 'Detection Rate', value: `${stats.detection_rate}%`, color: 'text-green-400' },
{ label: 'Detected', value: stats.result_counts?.detected || 0, color: 'text-green-400' },
].map((card, i) => (
<div key={i} className="bg-dark-800 border border-dark-700 rounded-xl p-4 text-center">
<div className={`text-2xl font-bold ${card.color}`}>{card.value}</div>
<div className="text-xs text-dark-500 mt-1">{card.label}</div>
</div>
))}
</div>
{/* Per-category breakdown */}
{Object.keys(stats.by_category).length > 0 && (
<div className="bg-dark-800 border border-dark-700 rounded-2xl p-6">
<h3 className="text-white font-semibold mb-4">Detection by Category</h3>
<div className="space-y-3">
{Object.entries(stats.by_category).map(([cat, data]) => {
const rate = data.total > 0 ? Math.round(data.detected / data.total * 100) : 0
const catLabel = categories[cat]?.label || cat
return (
<div key={cat}>
<div className="flex items-center justify-between mb-1">
<span className="text-sm text-dark-300">{catLabel}</span>
<span className="text-sm text-dark-400">
{data.detected}/{data.total} ({rate}%)
</span>
</div>
<div className="w-full bg-dark-900 rounded-full h-2">
<div
className={`h-2 rounded-full transition-all ${rate >= 70 ? 'bg-green-500' : rate >= 40 ? 'bg-yellow-500' : 'bg-red-500'}`}
style={{ width: `${rate}%` }}
/>
</div>
</div>
)
})}
</div>
</div>
)}
{/* Per-type breakdown */}
{Object.keys(stats.by_type).length > 0 && (
<div className="bg-dark-800 border border-dark-700 rounded-2xl p-6">
<h3 className="text-white font-semibold mb-4">Detection by Vulnerability Type</h3>
<div className="grid grid-cols-2 gap-3">
{Object.entries(stats.by_type).map(([vtype, data]) => {
const rate = data.total > 0 ? Math.round(data.detected / data.total * 100) : 0
return (
<div key={vtype} className="flex items-center justify-between p-2 bg-dark-900 rounded-lg">
<span className="text-sm text-dark-300">{vtype.replace(/_/g, ' ')}</span>
<div className="flex items-center gap-2">
<span className={`text-xs font-bold ${rate >= 70 ? 'text-green-400' : rate >= 40 ? 'text-yellow-400' : 'text-red-400'}`}>
{rate}%
</span>
<span className="text-xs text-dark-600">({data.detected}/{data.total})</span>
</div>
</div>
)
})}
</div>
</div>
)}
{stats.total === 0 && (
<div className="text-center text-dark-500 py-8">
No test data yet. Run some vulnerability tests to see stats!
</div>
)}
</div>
)}
</div>
)}
</div>
)
}
/* ===== Challenge Logs Viewer Component ===== */
function ChallengeLogsViewer({ logs }: { logs: VulnLabLogEntry[] }) {
const [expanded, setExpanded] = useState(false)
const [filter, setFilter] = useState<'all' | 'info' | 'warning' | 'error'>('all')
const filtered = filter === 'all' ? logs : logs.filter(l => l.level === filter)
const displayed = expanded ? filtered : filtered.slice(-30)
const errorCount = logs.filter(l => l.level === 'error').length
const warnCount = logs.filter(l => l.level === 'warning').length
return (
<div className="border border-dark-700 rounded-lg overflow-hidden">
<div className="flex items-center justify-between px-3 py-2 bg-dark-800 border-b border-dark-700">
<button
onClick={() => setExpanded(!expanded)}
className="flex items-center gap-2 text-sm font-medium text-dark-300 hover:text-white transition-colors"
>
<Terminal className="w-4 h-4 text-purple-400" />
Agent Logs ({logs.length})
{errorCount > 0 && <span className="text-red-400 text-xs">({errorCount} errors)</span>}
{warnCount > 0 && <span className="text-yellow-400 text-xs">({warnCount} warnings)</span>}
{expanded ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />}
</button>
<div className="flex items-center gap-1">
{(['all', 'info', 'warning', 'error'] as const).map(level => (
<button
key={level}
onClick={() => setFilter(level)}
className={`px-2 py-0.5 rounded text-xs transition-colors ${
filter === level
? 'bg-purple-500/20 text-purple-400'
: 'text-dark-600 hover:text-white'
}`}
>
{level}
</button>
))}
</div>
</div>
<div className={`p-2 bg-dark-900 overflow-y-auto space-y-0.5 ${expanded ? 'max-h-96' : 'max-h-48'}`}>
{!expanded && filtered.length > 30 && (
<p className="text-dark-600 text-xs font-mono mb-1">
... {filtered.length - 30} older entries hidden (click to expand)
</p>
)}
{displayed.map((log, i) => <LogLine key={i} log={log} />)}
{displayed.length === 0 && (
<p className="text-dark-600 text-xs font-mono">No logs matching filter.</p>
)}
</div>
</div>
)
}
+248 -1
View File
@@ -2,7 +2,9 @@ import axios from 'axios'
import type {
Scan, Vulnerability, Prompt, PromptPreset, Report, DashboardStats,
AgentTask, AgentRequest, AgentResponse, AgentStatus, AgentLog, AgentMode,
ScanAgentTask, ActivityFeedItem
ScanAgentTask, ActivityFeedItem, ScheduleJob, ScheduleJobRequest, AgentRole,
VulnLabChallenge, VulnLabRunRequest, VulnLabRunResponse, VulnLabRealtimeStatus,
VulnTypeCategory, VulnLabStats, SandboxPoolStatus
} from '../types'
const api = axios.create({
@@ -48,11 +50,26 @@ export const scansApi = {
return response.data
},
pause: async (scanId: string) => {
const response = await api.post(`/scans/${scanId}/pause`)
return response.data
},
resume: async (scanId: string) => {
const response = await api.post(`/scans/${scanId}/resume`)
return response.data
},
delete: async (scanId: string) => {
const response = await api.delete(`/scans/${scanId}`)
return response.data
},
skipToPhase: async (scanId: string, phase: string) => {
const response = await api.post(`/scans/${scanId}/skip-to/${phase}`)
return response.data
},
getEndpoints: async (scanId: string, page = 1, perPage = 50) => {
const response = await api.get(`/scans/${scanId}/endpoints?page=${page}&per_page=${perPage}`)
return response.data
@@ -154,10 +171,20 @@ export const reportsApi = {
return response.data
},
generateAiReport: async (data: {
scan_id: string
title?: string
}): Promise<Report> => {
const response = await api.post('/reports/ai-generate', data)
return response.data
},
getViewUrl: (reportId: string) => `/api/v1/reports/${reportId}/view`,
getDownloadUrl: (reportId: string, format: string) => `/api/v1/reports/${reportId}/download/${format}`,
getDownloadZipUrl: (reportId: string) => `/api/v1/reports/${reportId}/download-zip`,
delete: async (reportId: string) => {
const response = await api.delete(`/reports/${reportId}`)
return response.data
@@ -210,6 +237,14 @@ export const vulnerabilitiesApi = {
const response = await api.get(`/vulnerabilities/${vulnId}`)
return response.data
},
validate: async (vulnId: string, validationStatus: string, notes?: string) => {
const response = await api.patch(`/scans/vulnerabilities/${vulnId}/validate`, {
validation_status: validationStatus,
notes,
})
return response.data
},
}
// Scan Agent Tasks API (for tracking scan-specific tasks)
@@ -261,6 +296,16 @@ export const agentApi = {
return response.data
},
// Get agent status by scan_id (reverse lookup)
getByScan: async (scanId: string): Promise<AgentStatus | null> => {
try {
const response = await api.get(`/agent/by-scan/${scanId}`)
return response.data
} catch {
return null
}
},
// Get agent logs
getLogs: async (agentId: string, limit = 100): Promise<{ agent_id: string; total_logs: number; logs: AgentLog[] }> => {
const response = await api.get(`/agent/logs/${agentId}?limit=${limit}`)
@@ -285,12 +330,44 @@ export const agentApi = {
return response.data
},
// Pause a running agent
pause: async (agentId: string) => {
const response = await api.post(`/agent/pause/${agentId}`)
return response.data
},
// Resume a paused agent
resume: async (agentId: string) => {
const response = await api.post(`/agent/resume/${agentId}`)
return response.data
},
// Skip to a specific phase
skipToPhase: async (agentId: string, phase: string) => {
const response = await api.post(`/agent/skip-to/${agentId}/${phase}`)
return response.data
},
// Send custom prompt to agent
sendPrompt: async (agentId: string, prompt: string) => {
const response = await api.post(`/agent/prompt/${agentId}`, { prompt })
return response.data
},
// One-click auto pentest
autoPentest: async (target: string, options?: { subdomain_discovery?: boolean; targets?: string[]; auth_type?: string; auth_value?: string; prompt?: string }): Promise<AgentResponse> => {
const response = await api.post('/agent/run', {
target,
mode: 'auto_pentest',
subdomain_discovery: options?.subdomain_discovery || false,
targets: options?.targets,
auth_type: options?.auth_type,
auth_value: options?.auth_value,
prompt: options?.prompt,
})
return response.data
},
// Quick synchronous run (for small targets)
quickRun: async (target: string, mode: AgentMode = 'full_auto') => {
const response = await api.post(`/agent/quick?target=${encodeURIComponent(target)}&mode=${mode}`)
@@ -393,4 +470,174 @@ export const agentApi = {
},
}
// Vulnerability Lab API
export const vulnLabApi = {
getTypes: async (): Promise<{ categories: Record<string, VulnTypeCategory>; total_types: number }> => {
const response = await api.get('/vuln-lab/types')
return response.data
},
run: async (request: VulnLabRunRequest): Promise<VulnLabRunResponse> => {
const response = await api.post('/vuln-lab/run', request)
return response.data
},
listChallenges: async (filters?: {
vuln_type?: string
vuln_category?: string
status?: string
result?: string
limit?: number
}): Promise<{ challenges: VulnLabChallenge[]; total: number }> => {
const params = new URLSearchParams()
if (filters?.vuln_type) params.append('vuln_type', filters.vuln_type)
if (filters?.vuln_category) params.append('vuln_category', filters.vuln_category)
if (filters?.status) params.append('status', filters.status)
if (filters?.result) params.append('result', filters.result)
if (filters?.limit) params.append('limit', String(filters.limit))
const qs = params.toString()
const response = await api.get(`/vuln-lab/challenges${qs ? `?${qs}` : ''}`)
return response.data
},
getChallenge: async (challengeId: string): Promise<VulnLabRealtimeStatus | VulnLabChallenge> => {
const response = await api.get(`/vuln-lab/challenges/${challengeId}`)
return response.data
},
getStats: async (): Promise<VulnLabStats> => {
const response = await api.get('/vuln-lab/stats')
return response.data
},
stopChallenge: async (challengeId: string) => {
const response = await api.post(`/vuln-lab/challenges/${challengeId}/stop`)
return response.data
},
deleteChallenge: async (challengeId: string) => {
const response = await api.delete(`/vuln-lab/challenges/${challengeId}`)
return response.data
},
getLogs: async (challengeId: string, limit = 100) => {
const response = await api.get(`/vuln-lab/logs/${challengeId}?limit=${limit}`)
return response.data
},
}
// Scheduler API
export const schedulerApi = {
list: async (): Promise<ScheduleJob[]> => {
const response = await api.get('/scheduler/')
return response.data
},
create: async (data: ScheduleJobRequest): Promise<ScheduleJob> => {
const response = await api.post('/scheduler/', data)
return response.data
},
delete: async (jobId: string) => {
const response = await api.delete(`/scheduler/${jobId}`)
return response.data
},
pause: async (jobId: string) => {
const response = await api.post(`/scheduler/${jobId}/pause`)
return response.data
},
resume: async (jobId: string) => {
const response = await api.post(`/scheduler/${jobId}/resume`)
return response.data
},
getAgentRoles: async (): Promise<AgentRole[]> => {
const response = await api.get('/scheduler/agent-roles')
return response.data
},
}
// Terminal Agent API
export const terminalApi = {
createSession: async (target: string, name?: string, template_id?: string) => {
const response = await api.post('/terminal/session', { target, name, template_id })
return response.data
},
listSessions: async () => {
const response = await api.get('/terminal/sessions')
return response.data
},
getSession: async (sessionId: string) => {
const response = await api.get(`/terminal/sessions/${sessionId}`)
return response.data
},
deleteSession: async (sessionId: string) => {
const response = await api.delete(`/terminal/sessions/${sessionId}`)
return response.data
},
sendMessage: async (sessionId: string, message: string) => {
const response = await api.post(`/terminal/sessions/${sessionId}/message`, { message })
return response.data
},
executeCommand: async (sessionId: string, command: string, execution_method: string) => {
const response = await api.post(`/terminal/sessions/${sessionId}/execute`, { command, execution_method })
return response.data
},
addExploitationStep: async (sessionId: string, step: { description: string; command: string; result: string; step_type: string }) => {
const response = await api.post(`/terminal/sessions/${sessionId}/exploitation-path`, step)
return response.data
},
getExploitationPath: async (sessionId: string) => {
const response = await api.get(`/terminal/sessions/${sessionId}/exploitation-path`)
return response.data
},
getVpnStatus: async (sessionId: string) => {
const response = await api.get(`/terminal/sessions/${sessionId}/vpn-status`)
return response.data
},
listTemplates: async () => {
const response = await api.get('/terminal/templates')
return response.data
},
}
// Sandbox API
export const sandboxApi = {
list: async (): Promise<SandboxPoolStatus> => {
const response = await api.get('/sandbox/')
return response.data
},
healthCheck: async (scanId: string) => {
const response = await api.get(`/sandbox/${scanId}`)
return response.data
},
destroy: async (scanId: string) => {
const response = await api.delete(`/sandbox/${scanId}`)
return response.data
},
cleanup: async () => {
const response = await api.post('/sandbox/cleanup')
return response.data
},
cleanupOrphans: async () => {
const response = await api.post('/sandbox/cleanup-orphans')
return response.data
},
}
export default api
+176 -3
View File
@@ -2,7 +2,7 @@
export interface Scan {
id: string
name: string | null
status: 'pending' | 'running' | 'completed' | 'failed' | 'stopped'
status: 'pending' | 'running' | 'paused' | 'completed' | 'failed' | 'stopped'
scan_type: 'quick' | 'full' | 'custom'
recon_enabled: boolean
progress: number
@@ -52,10 +52,19 @@ export interface Vulnerability {
poc_request: string | null
poc_response: string | null
poc_payload: string | null
poc_parameter: string | null
poc_evidence: string | null
impact: string | null
remediation: string | null
references: string[]
ai_analysis: string | null
poc_code?: string | null
validation_status?: 'ai_confirmed' | 'ai_rejected' | 'validated' | 'false_positive' | 'pending_review'
ai_rejection_reason?: string | null
confidence_score?: number // 0-100 numeric (from agent findings)
confidence_breakdown?: Record<string, number>
proof_of_execution?: string
negative_controls?: string
created_at: string
}
@@ -160,7 +169,7 @@ export interface ScanAgentTask {
}
// Agent types
export type AgentMode = 'full_auto' | 'recon_only' | 'prompt_only' | 'analyze_only'
export type AgentMode = 'full_auto' | 'recon_only' | 'prompt_only' | 'analyze_only' | 'auto_pentest'
export interface AgentTask {
id: string
@@ -186,6 +195,8 @@ export interface AgentRequest {
auth_value?: string
custom_headers?: Record<string, string>
max_depth?: number
subdomain_discovery?: boolean
targets?: string[]
}
export interface AgentResponse {
@@ -198,7 +209,7 @@ export interface AgentResponse {
export interface AgentStatus {
agent_id: string
scan_id?: string // Link to database scan
status: 'running' | 'completed' | 'error' | 'stopped'
status: 'running' | 'paused' | 'completed' | 'error' | 'stopped'
mode: string
target: string
task?: string
@@ -209,6 +220,8 @@ export interface AgentStatus {
logs_count: number
findings_count: number
findings: AgentFinding[]
rejected_findings_count?: number
rejected_findings?: AgentFinding[]
report?: AgentReport
error?: string
}
@@ -234,6 +247,12 @@ export interface AgentFinding {
references: string[]
ai_verified: boolean
confidence?: string
confidence_score?: number // 0-100 numeric
confidence_breakdown?: Record<string, number> // Scoring breakdown
proof_of_execution?: string // What proof was found
negative_controls?: string // Control test results
ai_status?: 'confirmed' | 'rejected' | 'pending'
rejection_reason?: string
}
export interface AgentReport {
@@ -316,6 +335,160 @@ export interface RealtimeSessionSummary {
messages_count: number
}
// Agent Role type (from config.json)
export interface AgentRole {
id: string
name: string
description: string
tools: string[]
}
// Scheduler types
export interface ScheduleJob {
id: string
target: string
scan_type: string
schedule: string
status: 'active' | 'paused'
next_run: string | null
last_run: string | null
run_count: number
agent_role: string | null
llm_profile: string | null
}
export interface ScheduleJobRequest {
job_id: string
target: string
scan_type: string
cron_expression?: string
interval_minutes?: number
agent_role?: string
llm_profile?: string
}
// Vulnerability Lab types
export interface VulnLabLogEntry {
level: string
message: string
time: string
source: string
}
export interface VulnLabChallenge {
id: string
target_url: string
challenge_name: string | null
vuln_type: string
vuln_category: string | null
auth_type: string | null
status: 'pending' | 'running' | 'paused' | 'completed' | 'failed' | 'stopped'
result: 'detected' | 'not_detected' | 'error' | null
agent_id: string | null
scan_id: string | null
findings_count: number
critical_count: number
high_count: number
medium_count: number
low_count: number
info_count: number
findings_detail: Array<{
title: string
vulnerability_type: string
severity: string
affected_endpoint: string
evidence: string
payload?: string
}>
started_at: string | null
completed_at: string | null
duration: number | null
notes: string | null
logs?: VulnLabLogEntry[]
logs_count?: number
endpoints_count?: number
created_at: string | null
}
export interface VulnLabRunRequest {
target_url: string
vuln_type: string
challenge_name?: string
auth_type?: string
auth_value?: string
custom_headers?: Record<string, string>
notes?: string
}
export interface VulnLabRunResponse {
challenge_id: string
agent_id: string
status: string
message: string
}
export interface VulnLabRealtimeStatus {
challenge_id: string
status: string
progress: number
phase: string
findings_count: number
findings: any[]
logs_count: number
logs?: VulnLabLogEntry[]
error: string | null
result: string | null
scan_id: string | null
agent_id: string | null
vuln_type?: string
target?: string
source: string
}
export interface VulnTypeCategory {
label: string
types: Array<{
key: string
title: string
severity: string
cwe_id: string
description: string
}>
count: number
}
export interface VulnLabStats {
total: number
running: number
status_counts: Record<string, number>
result_counts: Record<string, number>
detection_rate: number
by_type: Record<string, { detected: number; not_detected: number; error: number; total: number }>
by_category: Record<string, { detected: number; not_detected: number; error: number; total: number }>
}
// Sandbox Container types
export interface SandboxContainer {
scan_id: string
container_name: string
available: boolean
installed_tools: string[]
created_at: string | null
uptime_seconds: number
}
export interface SandboxPoolStatus {
pool: {
active: number
max_concurrent: number
image: string
container_ttl_minutes: number
docker_available: boolean
}
containers: SandboxContainer[]
error?: string
}
// Activity Feed types
export interface ActivityFeedItem {
type: 'scan' | 'vulnerability' | 'agent_task' | 'report'