diff --git a/web/ui/dist/index.html b/web/ui/dist/index.html index af8cf896..19a835fd 100644 --- a/web/ui/dist/index.html +++ b/web/ui/dist/index.html @@ -6,8 +6,8 @@ gowitness - a golang screenshotting tool by @leonjza - - + + diff --git a/web/ui/src/lib/api/api.ts b/web/ui/src/lib/api/api.ts index 27f992ed..5329ca7d 100644 --- a/web/ui/src/lib/api/api.ts +++ b/web/ui/src/lib/api/api.ts @@ -54,6 +54,10 @@ const endpoints = { submit: { path: `/submit`, returnas: "" as string + }, + submitsingle: { + path: `/submit/single`, + returnas: {} as detail } }; diff --git a/web/ui/src/lib/api/types.ts b/web/ui/src/lib/api/types.ts index 7e16386d..ad8639a3 100644 --- a/web/ui/src/lib/api/types.ts +++ b/web/ui/src/lib/api/types.ts @@ -160,6 +160,7 @@ interface detail { is_pdf: boolean; failed: boolean; failed_reason: string; + screenshot: string; tls: tls; technologies: technology[]; headers: header[]; diff --git a/web/ui/src/main.tsx b/web/ui/src/main.tsx index cc14a96c..309ea0ce 100644 --- a/web/ui/src/main.tsx +++ b/web/ui/src/main.tsx @@ -16,7 +16,7 @@ import JobSubmissionPage from '@/pages/submit/Submit'; import { searchAction } from '@/pages/search/action'; import { searchLoader } from '@/pages/search/loader'; import { deleteAction } from '@/pages/detail/actions'; -import { submitAction } from '@/pages/submit/action'; +import { submitImmediateAction, submitJobAction } from '@/pages/submit/action'; const router = createBrowserRouter([ { @@ -50,7 +50,20 @@ const router = createBrowserRouter([ { path: 'submit', element: , - action: submitAction, + action: async ({ request }) => { + const formData = await request.formData(); + const action = formData.get('action'); + + switch (action) { + case 'job': + return submitJobAction({ formData }); + case 'immediate': + return submitImmediateAction({ formData }); + + default: + throw new Error('unknown action for job submit route'); + } + }, }, ] } diff --git a/web/ui/src/pages/submit/Submit.tsx b/web/ui/src/pages/submit/Submit.tsx index 30acdd07..f56b790d 100644 --- a/web/ui/src/pages/submit/Submit.tsx +++ b/web/ui/src/pages/submit/Submit.tsx @@ -1,6 +1,6 @@ -import { useState } from 'react'; -import { Form, useNavigation } from 'react-router-dom'; -import { PlusCircle, Trash2, Send, Settings } from 'lucide-react'; +import { useEffect, useState } from 'react'; +import { Form, useActionData, useNavigation } from 'react-router-dom'; +import { PlusCircle, Trash2, Send, Settings, GlobeIcon, ExternalLinkIcon, ServerIcon, FileTypeIcon, ClockIcon } from 'lucide-react'; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; @@ -8,11 +8,25 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@ import { Switch } from "@/components/ui/switch"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'; +import { ScrollArea } from '@/components/ui/scroll-area'; +import * as apitypes from "@/lib/api/types"; export default function JobSubmissionPage() { const [urls, setUrls] = useState(['']); const [advancedOptions, setAdvancedOptions] = useState(false); + const [immediateUrl, setImmediateUrl] = useState(''); + const [isModalOpen, setIsModalOpen] = useState(false); + const navigation = useNavigation(); + const probeResult = useActionData() as apitypes.detail | null; + + useEffect(() => { + probeResult + ? setIsModalOpen(true) + : setIsModalOpen(false); + }, [probeResult]); const handleUrlChange = (index: number, value: string) => { const newUrls = [...urls]; @@ -29,148 +43,319 @@ export default function JobSubmissionPage() { setUrls(newUrls); }; + const ProbeOptions = () => ( + + + +
+ + Probe Options +
+
+ +
+
+
+ + +
+ +
+ + +
+
+ + +
+
+ +
+ + +
+ + {advancedOptions && ( +
+
+ + +
+ +
+
+ + +
+
+ + +
+
+
+ )} +
+
+
+
+ ); + return (
Launch a New Probe - Enter URLs and set options for your probe + Submit a job or run an immediate probe -
-
-

URLs

- {urls.map((url, index) => ( -
+ + + + Job Submission Probe + Immediate Probe + + + + +
+

URLs

+ {urls.map((url, index) => ( +
+ handleUrlChange(index, e.target.value)} + className="flex-grow" + /> + {index === urls.length - 1 ? ( + + ) : ( + + )} +
+ ))} +
+ + + + {!advancedOptions && ( + <> + + + + + + + + )} + + + +
+ +
+ +
+ +
+
+

URL

handleUrlChange(index, e.target.value)} + value={immediateUrl} + onChange={(e) => setImmediateUrl(e.target.value)} className="flex-grow" /> - {index === urls.length - 1 ? ( - - ) : ( - - )}
- ))} -
- - - -
- - Probe Options -
-
- -
-
-
- - -
- -
- - -
-
+ -
- - -
+ {!advancedOptions && ( + <> + + + + + + + + )} + + - {advancedOptions && ( -
-
- - +
+ +
+ + + + + + + + + + Probe Result + Details of the immediate probe + + {probeResult && ( +
+
+ +
+ + + + + URL Information + + + +
+ Initial URL: + + {probeResult.url} + +
+
+
-
-
- - -
-
- - + + + + + Response Details + + + +
+ Response Code: + {probeResult.response_code} +
+
+ Protocol: + {probeResult.protocol} +
+
+ Content Length: + {probeResult.content_length} bytes +
+
+
+ + + + + + Page Information + + + +
+ Title: + {probeResult.title} +
+
+ Failed: + {probeResult.failed ? 'Yes' : 'No'} +
+ {probeResult.failed && ( +
+ Failed Reason: + {probeResult.failed_reason}
+ )} +
+
+ + + + + + Timing Information + + + +
+ Probed At: + {new Date(probeResult.probed_at).toLocaleString()}
-
- )} + +
- - - - - {/* add defaults as hidden inputs because the form will only subit the rendered dom */} - {/* expanding the accordion will remove these */} - {!advancedOptions && ( - <> - - - - - - - - )} - -
- + +
+

Screenshot

+
+ Screenshot +
+
+
- - - + )} + +
+
); } \ No newline at end of file diff --git a/web/ui/src/pages/submit/action.ts b/web/ui/src/pages/submit/action.ts index b1ec5eee..42ded87c 100644 --- a/web/ui/src/pages/submit/action.ts +++ b/web/ui/src/pages/submit/action.ts @@ -2,8 +2,7 @@ import { toast } from "@/hooks/use-toast"; import * as api from "@/lib/api/api"; import { redirect } from "react-router-dom"; -const submitAction = async ({ request }: { request: Request; }) => { - const formData = await request.formData(); +const submitJobAction = async ({ formData }: { formData: FormData; }) => { // grab submitted urls const urls = Array.from(formData.entries()) @@ -18,6 +17,7 @@ const submitAction = async ({ request }: { request: Request; }) => { const options = { format: formData.get('format'), timeout: parseInt(formData.get('timeout') as string), + delay: parseInt(formData.get('delay') as string), user_agent: formData.get('user_agent'), window_x: parseInt(formData.get('window_x') as string), window_y: parseInt(formData.get('window_y') as string), @@ -42,4 +42,27 @@ const submitAction = async ({ request }: { request: Request; }) => { return redirect("/submit"); }; -export { submitAction }; \ No newline at end of file +const submitImmediateAction = async ({ formData }: { formData: FormData; }) => { + const url = formData.get('immediate-url') as string; + const options = { + format: formData.get('format'), + timeout: parseInt(formData.get('timeout') as string), + delay: parseInt(formData.get('delay') as string), + user_agent: formData.get('user_agent'), + window_x: parseInt(formData.get('window_x') as string), + window_y: parseInt(formData.get('window_y') as string), + }; + + try { + return await api.post('submitsingle', { url, options }); + } catch (err) { + toast({ + title: "Error", + description: `Could not submit new probe: ${err}`, + variant: "destructive" + }); + return null; + } +}; + +export { submitJobAction, submitImmediateAction }; \ No newline at end of file