From 86bb64a4d0312df908560404fe18fed43606f962 Mon Sep 17 00:00:00 2001 From: Alicia Sykes Date: Tue, 22 Aug 2023 21:40:22 +0100 Subject: [PATCH 01/17] Adds an endpoint for fetching TLS info via Moz observetory --- api/tls.js | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 api/tls.js diff --git a/api/tls.js b/api/tls.js new file mode 100644 index 0000000..20e4065 --- /dev/null +++ b/api/tls.js @@ -0,0 +1,28 @@ +const axios = require('axios'); +const middleware = require('./_common/middleware'); + +const MOZILLA_TLS_OBSERVATORY_API = 'https://tls-observatory.services.mozilla.com/api/v1'; + +const handler = async (url) => { + try { + const domain = new URL(url).hostname; + const scanResponse = await axios.post(`${MOZILLA_TLS_OBSERVATORY_API}/scan?target=${domain}`); + const scanId = scanResponse.data.scan_id; + + if (typeof scanId !== 'number') { + return { + statusCode: 500, + body: JSON.stringify({ error: 'Failed to get scan_id from TLS Observatory' }), + }; + } + const resultResponse = await axios.get(`${MOZILLA_TLS_OBSERVATORY_API}/results?id=${scanId}`); + return { + statusCode: 200, + body: JSON.stringify(resultResponse.data), + }; + } catch (error) { + return { error: error.message }; + } +}; + +exports.handler = middleware(handler); From 93aa496a3081d3df3c151c4e1118c546b3e708cb Mon Sep 17 00:00:00 2001 From: Alicia Sykes Date: Thu, 24 Aug 2023 12:35:46 +0100 Subject: [PATCH 02/17] Adds prop for button to show progress loader --- src/components/Form/Button.tsx | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/src/components/Form/Button.tsx b/src/components/Form/Button.tsx index c3c7da1..96e2acc 100644 --- a/src/components/Form/Button.tsx +++ b/src/components/Form/Button.tsx @@ -1,7 +1,9 @@ -import styled from 'styled-components'; +import styled, { keyframes } from 'styled-components'; import colors from 'styles/colors'; import { InputSize, applySize } from 'styles/dimensions'; +type LoadState = 'loading' | 'success' | 'error'; + interface ButtonProps { children: React.ReactNode; onClick?: React.MouseEventHandler; @@ -10,6 +12,7 @@ interface ButtonProps { fgColor?: string, styles?: string, title?: string, + loadState?: LoadState, }; const StyledButton = styled.button` @@ -19,6 +22,9 @@ const StyledButton = styled.button` font-family: PTMono; box-sizing: border-box; width: -moz-available; + display: flex; + justify-content: center; + gap: 1rem; box-shadow: 3px 3px 0px ${colors.fgShadowColor}; &:hover { box-shadow: 5px 5px 0px ${colors.fgShadowColor}; @@ -36,8 +42,29 @@ const StyledButton = styled.button` ${props => props.styles} `; + +const spinAnimation = keyframes` + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +`; +const SimpleLoader = styled.div` + border: 4px solid rgba(255, 255, 255, 0.3); + border-radius: 50%; + border-top: 4px solid ${colors.background}; + width: 1rem; + height: 1rem; + animation: ${spinAnimation} 1s linear infinite; +`; + +const Loader = (props: { loadState: LoadState }) => { + if (props.loadState === 'loading') return + if (props.loadState === 'success') return + if (props.loadState === 'error') return + return ; +}; + const Button = (props: ButtonProps): JSX.Element => { - const { children, size, bgColor, fgColor, onClick, styles, title } = props; + const { children, size, bgColor, fgColor, onClick, styles, title, loadState } = props; return ( null) } @@ -47,6 +74,7 @@ const Button = (props: ButtonProps): JSX.Element => { styles={styles} title={title?.toString()} > + { loadState && } {children} ); From 5e755c1dc26b94d8cc10c6b17eb0f95a1390ae70 Mon Sep 17 00:00:00 2001 From: Alicia Sykes Date: Thu, 24 Aug 2023 12:36:01 +0100 Subject: [PATCH 03/17] Adds prop for row to also show code or a list --- src/components/Form/Row.tsx | 54 ++++++++++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/src/components/Form/Row.tsx b/src/components/Form/Row.tsx index a46dbd2..96a38a5 100644 --- a/src/components/Form/Row.tsx +++ b/src/components/Form/Row.tsx @@ -11,11 +11,14 @@ export interface RowProps { rowList?: RowProps[], title?: string, open?: boolean, + plaintext?: string, + listResults?: string[], } export const StyledRow = styled.div` display: flex; justify-content: space-between; + flex-wrap: wrap; padding: 0.25rem; &:not(:last-child) { border-bottom: 1px solid ${colors.primary}; } span.lbl { font-weight: bold; } @@ -38,6 +41,7 @@ export const Details = styled.details` transition: all 0.2s ease-in-out; summary { padding-left: 1rem; + cursor: pointer; } summary:before { content: "►"; @@ -63,6 +67,37 @@ const SubRow = styled(StyledRow).attrs({ border-bottom: 1px dashed ${colors.primaryTransparent} !important; `; +const PlainText = styled.pre` + background: ${colors.background}; + width: 95%; + white-space: pre-wrap; + word-wrap: break-word; + border-radius: 4px; + padding: 0.25rem; +`; + +const List = styled.ul` + // background: ${colors.background}; + width: 95%; + white-space: pre-wrap; + word-wrap: break-word; + border-radius: 4px; + margin: 0; + padding: 0.25rem 0.25rem 0.25rem 1rem; + li { + // white-space: nowrap; + // overflow: hidden; + text-overflow: ellipsis; + list-style: circle; + &:first-letter{ + text-transform: capitalize + } + &::marker { + color: ${colors.primary}; + } + } +`; + const isValidDate = (date: any): boolean => { // Checks if a date is within reasonable range const isInRange = (date: Date): boolean => { @@ -106,6 +141,11 @@ const copyToClipboard = (text: string) => { navigator.clipboard.writeText(text); } +const snip = (text: string, length: number = 80) => { + if (text.length < length) return text; + return `${text.substring(0, length)}...`; +}; + export const ExpandableRow = (props: RowProps) => { const { lbl, val, title, rowList, open } = props; return ( @@ -123,6 +163,12 @@ export const ExpandableRow = (props: RowProps) => { copyToClipboard(row.val)}> {formatValue(row.val)} + { row.plaintext && {row.plaintext}</PlainText> } + { row.listResults && (<List> + {row.listResults.map((listItem: string, listIndex: number) => ( + <li key={listItem}>{snip(listItem)}</li> + ))} + </List>)} </SubRow> ) })} @@ -149,7 +195,7 @@ export const ListRow = (props: { list: string[], title: string }) => { } const Row = (props: RowProps) => { - const { lbl, val, title, children } = props; + const { lbl, val, title, plaintext, listResults, children } = props; if (children) return <StyledRow key={`${lbl}-${val}`}>{children}</StyledRow>; return ( <StyledRow key={`${lbl}-${val}`}> @@ -157,6 +203,12 @@ const Row = (props: RowProps) => { <span className="val" title={val} onClick={() => copyToClipboard(val)}> {formatValue(val)} </span> + { plaintext && <PlainText>{plaintext}</PlainText> } + { listResults && (<List> + {listResults.map((listItem: string, listIndex: number) => ( + <li key={listIndex} title={listItem}>{snip(listItem)}</li> + ))} + </List>)} </StyledRow> ); }; From d13db8d438562e52726663694eecb7c42d9482b8 Mon Sep 17 00:00:00 2001 From: Alicia Sykes <alicia@omg.lol> Date: Thu, 24 Aug 2023 12:51:01 +0100 Subject: [PATCH 04/17] Remove mail txt from val --- src/components/Results/MailConfig.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Results/MailConfig.tsx b/src/components/Results/MailConfig.tsx index 6ef7f3e..fddfe6a 100644 --- a/src/components/Results/MailConfig.tsx +++ b/src/components/Results/MailConfig.tsx @@ -27,7 +27,7 @@ const MailConfigCard = (props: {data: any, title: string, actionButtons: any }): } { mailServer.mailServices.length > 0 && <Heading as="h3" color={colors.primary} size="small">External Mail Services</Heading>} { mailServer.mailServices && mailServer.mailServices.map((service: any, index: number) => ( - <Row lbl={service.provider} val={service.value} key={index} /> + <Row lbl={service.provider} title={service.value} val="" key={index} /> )) } From 57481c87575f5e891f1ce7eff8ad44ce712aaf2b Mon Sep 17 00:00:00 2001 From: Alicia Sykes <alicia@omg.lol> Date: Thu, 24 Aug 2023 12:52:08 +0100 Subject: [PATCH 05/17] Updates display of threats view --- src/components/Results/Threats.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/Results/Threats.tsx b/src/components/Results/Threats.tsx index 1a77123..1b8d0ce 100644 --- a/src/components/Results/Threats.tsx +++ b/src/components/Results/Threats.tsx @@ -45,7 +45,7 @@ const MalwareCard = (props: {data: any, title: string, actionButtons: any }): JS <Row lbl="Threat Type" val={safeBrowsing?.details?.threatType || cloudmersive.WebsiteThreatType || 'None :)'} /> )} { phishTank && !phishTank.error && ( - <Row lbl="Phishing Status" val={phishTank.url0 ? '❌ Phishing Identified' : '✅ No Phishing Identified!'} /> + <Row lbl="Phishing Status" val={phishTank?.url0?.in_database !== 'false' ? '❌ Phishing Identified' : '✅ No Phishing Found'} /> )} { phishTank.url0 && phishTank.url0.phish_detail_page && ( <Row lbl="" val=""> @@ -53,10 +53,10 @@ const MalwareCard = (props: {data: any, title: string, actionButtons: any }): JS <span className="val"><a href={phishTank.url0.phish_detail_page}>{phishTank.url0.phish_id}</a></span> </Row> )} - { urlHaus.query_status === 'no_results' && <Row lbl="Status" val="✅ Nothing Found!" />} + { urlHaus.query_status === 'no_results' && <Row lbl="Malware Status" val="✅ No Malwares Found" />} { urlHaus.query_status === 'ok' && ( <> - <Row lbl="Status" val="❌ Malware Found" /> + <Row lbl="Status" val="❌ Malware Identified" /> <Row lbl="First Seen" val={convertToDate(urlHaus.firstseen)} /> <Row lbl="Bad URLs Count" val={urlHaus.url_count} /> </> From b6dfd3321ab9a0a1dd1888a28bd836b8f0876729 Mon Sep 17 00:00:00 2001 From: Alicia Sykes <alicia@omg.lol> Date: Thu, 24 Aug 2023 20:35:01 +0100 Subject: [PATCH 06/17] Updates hook to allow for array of job IDs --- src/hooks/motherOfAllHooks.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/hooks/motherOfAllHooks.ts b/src/hooks/motherOfAllHooks.ts index fae8b16..e807131 100644 --- a/src/hooks/motherOfAllHooks.ts +++ b/src/hooks/motherOfAllHooks.ts @@ -7,11 +7,11 @@ import { AddressType } from 'utils/address-type-checker'; interface UseIpAddressProps<ResultType = any> { // Unique identifier for this job type - jobId: string; + jobId: string | string[]; // The actual fetch request fetchRequest: () => Promise<ResultType>; // Function to call to update the loading state in parent - updateLoadingJobs: (job: string, newState: LoadingState, error?: string, retry?: (data?: any) => void | null, data?: any) => void; + updateLoadingJobs: (job: string | string[], newState: LoadingState, error?: string, retry?: (data?: any) => void | null, data?: any) => void; addressInfo: { // The hostname/ip address that we're checking address: string | undefined; From 49cfad2dbe00325fe4668af9786b6f014a611169 Mon Sep 17 00:00:00 2001 From: Alicia Sykes <alicia@omg.lol> Date: Thu, 24 Aug 2023 20:35:46 +0100 Subject: [PATCH 07/17] Update image URL --- src/components/Results/TechStack.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Results/TechStack.tsx b/src/components/Results/TechStack.tsx index 892eb8c..2771c84 100644 --- a/src/components/Results/TechStack.tsx +++ b/src/components/Results/TechStack.tsx @@ -80,7 +80,7 @@ h4 { const TechStackCard = (props: {data: any, title: string, actionButtons: any }): JSX.Element => { const technologies = props.data.technologies; - const iconsCdn = 'https://raw.githubusercontent.com/wappalyzer/wappalyzer/master/src/images/icons/'; + const iconsCdn = 'https://www.wappalyzer.com/images/icons/'; return ( <Card heading={props.title} actionButtons={props.actionButtons} styles={cardStyles}> {technologies.map((tech: any, index: number) => { From 823873bce26b2f56a3300204f4e9286f188ab71b Mon Sep 17 00:00:00 2001 From: Alicia Sykes <alicia@omg.lol> Date: Thu, 24 Aug 2023 20:36:03 +0100 Subject: [PATCH 08/17] Builds result cards for TLS results --- src/components/Results/TlsCipherSuites.tsx | 68 +++++++++++ src/components/Results/TlsClientSupport.tsx | 87 ++++++++++++++ src/components/Results/TlsIssueAnalysis.tsx | 120 ++++++++++++++++++++ 3 files changed, 275 insertions(+) create mode 100644 src/components/Results/TlsCipherSuites.tsx create mode 100644 src/components/Results/TlsClientSupport.tsx create mode 100644 src/components/Results/TlsIssueAnalysis.tsx diff --git a/src/components/Results/TlsCipherSuites.tsx b/src/components/Results/TlsCipherSuites.tsx new file mode 100644 index 0000000..57cc581 --- /dev/null +++ b/src/components/Results/TlsCipherSuites.tsx @@ -0,0 +1,68 @@ + +import { useState } from 'react'; +import styled from 'styled-components'; +import colors from 'styles/colors'; +import { Card } from 'components/Form/Card'; +import Button from 'components/Form/Button'; +import Row, { ExpandableRow } from 'components/Form/Row'; + +const makeCipherSuites = (results: any) => { + if (!results || !results.connection_info || (results.connection_info.ciphersuite || [])?.length === 0) { + return []; + } + return results.connection_info.ciphersuite.map((ciphersuite: any) => { + return { + title: ciphersuite.cipher, + fields: [ + { lbl: 'Code', val: ciphersuite.code }, + { lbl: 'Protocols', val: ciphersuite.protocols.join(', ') }, + { lbl: 'Pubkey', val: ciphersuite.pubkey }, + { lbl: 'Sigalg', val: ciphersuite.sigalg }, + { lbl: 'Ticket Hint', val: ciphersuite.ticket_hint }, + { lbl: 'OCSP Stapling', val: ciphersuite.ocsp_stapling ? '✅ Enabled' : '❌ Disabled' }, + { lbl: 'PFS', val: ciphersuite.pfs }, + ciphersuite.curves ? { lbl: 'Curves', val: ciphersuite.curves.join(', ') } : {}, + ]}; + }); +}; + +const TlsCard = (props: {data: any, title: string, actionButtons: any }): JSX.Element => { + + const [cipherSuites, setCipherSuites] = useState(makeCipherSuites(props.data)); + const [loadState, setLoadState] = useState<undefined | 'loading' | 'success' | 'error'>(undefined); + + const updateData = (id: number) => { + setCipherSuites([]); + setLoadState('loading'); + const fetchUrl = `https://tls-observatory.services.mozilla.com/api/v1/results?id=${id}`; + fetch(fetchUrl) + .then((response) => response.json()) + .then((data) => { + setCipherSuites(makeCipherSuites(data)); + setLoadState('success'); + }).catch((error) => { + setLoadState('error'); + }); + }; + + const scanId = props.data?.id; + return ( + <Card heading={props.title} actionButtons={props.actionButtons}> + { cipherSuites.length && cipherSuites.map((cipherSuite: any, index: number) => { + return ( + <ExpandableRow lbl={cipherSuite.title} val="" rowList={cipherSuite.fields} /> + ); + })} + { !cipherSuites.length && ( + <div> + <p>No cipher suites found.<br /> + This sometimes happens when the report didn't finish generating in-time, you can try re-requesting it. + </p> + <Button loadState={loadState} onClick={() => updateData(scanId)}>Refetch Report</Button> + </div> + )} + </Card> + ); +} + +export default TlsCard; diff --git a/src/components/Results/TlsClientSupport.tsx b/src/components/Results/TlsClientSupport.tsx new file mode 100644 index 0000000..609e69b --- /dev/null +++ b/src/components/Results/TlsClientSupport.tsx @@ -0,0 +1,87 @@ + +import { useState } from 'react'; +import styled from 'styled-components'; +import colors from 'styles/colors'; +import { Card } from 'components/Form/Card'; +import Button from 'components/Form/Button'; +import Row, { ExpandableRow } from 'components/Form/Row'; + + +// "name": "Android", +// "curve": "secp256r1", +// "version": "6.0", +// "platform": "", +// "protocol": "TLSv1.2", +// "curve_code": 23, +// "ciphersuite": "ECDHE-RSA-CHACHA20-POLY1305-OLD", +// "is_supported": true, +// "protocol_code": 771, +// "ciphersuite_code": 52243 + +const makeClientSupport = (results: any) => { + if (!results?.analysis) return []; + const target = results.target; + const sslLabsClientSupport = ( + results.analysis.find((a: any) => a.analyzer === 'sslLabsClientSupport') + ).result; + + return sslLabsClientSupport.map((sup: any) => { + return { + title: `${sup.name} ${sup.platform ? `(on ${sup.platform})`: sup.version}`, + value: sup.is_supported ? '✅' : '❌', + fields: sup.is_supported ? [ + sup.curve ? { lbl: 'Curve', val: sup.curve } : {}, + { lbl: 'Protocol', val: sup.protocol }, + { lbl: 'Cipher Suite', val: sup.ciphersuite }, + { lbl: 'Protocol Code', val: sup.protocol_code }, + { lbl: 'Cipher Suite Code', val: sup.ciphersuite_code }, + { lbl: 'Curve Code', val: sup.curve_code }, + ] : [ + { lbl: '', val: '', + plaintext: `The host ${target} does not support ${sup.name} ` + +`${sup.version ? `version ${sup.version} `: ''} ` + + `${sup.platform ? `on ${sup.platform} `: ''}`} + ], + }; + }); + +}; + +const TlsCard = (props: {data: any, title: string, actionButtons: any }): JSX.Element => { + + const [clientSupport, setClientSupport] = useState(makeClientSupport(props.data)); + const [loadState, setLoadState] = useState<undefined | 'loading' | 'success' | 'error'>(undefined); + + const updateData = (id: number) => { + setClientSupport([]); + setLoadState('loading'); + const fetchUrl = `https://tls-observatory.services.mozilla.com/api/v1/results?id=${id}`; + fetch(fetchUrl) + .then((response) => response.json()) + .then((data) => { + setClientSupport(makeClientSupport(data)); + setLoadState('success'); + }).catch(() => { + setLoadState('error'); + }); + }; + + const scanId = props.data?.id; + return ( + <Card heading={props.title} actionButtons={props.actionButtons}> + {clientSupport.map((support: any) => { + return (<ExpandableRow lbl={support.title} val={support.value || '?'} rowList={support.fields} />) + })} + { !clientSupport.length && ( + <div> + <p>No entries available to analyze.<br /> + This sometimes happens when the report didn't finish generating in-time, you can try re-requesting it. + </p> + <Button loadState={loadState} onClick={() => updateData(scanId)}>Refetch Report</Button> + </div> + )} + </Card> + ); +} + +export default TlsCard; diff --git a/src/components/Results/TlsIssueAnalysis.tsx b/src/components/Results/TlsIssueAnalysis.tsx new file mode 100644 index 0000000..c696a44 --- /dev/null +++ b/src/components/Results/TlsIssueAnalysis.tsx @@ -0,0 +1,120 @@ + +import { useState } from 'react'; +import styled from 'styled-components'; +import colors from 'styles/colors'; +import { Card } from 'components/Form/Card'; +import Button from 'components/Form/Button'; +import Row, { ExpandableRow } from 'components/Form/Row'; + +const Expandable = styled.details` +margin-top: 0.5rem; +cursor: pointer; +summary::marker { + color: ${colors.primary}; +} +`; + +const makeExpandableData = (results: any) => { + if (!results || !results.analysis || results.analysis.length === 0) { + return []; + } + return results.analysis.map((analysis: any) => { + const fields = Object.keys(analysis.result).map((label) => { + const lbl = isNaN(parseInt(label, 10)) ? label : ''; + const val = analysis.result[label] || 'None'; + if (typeof val !== 'object') { + return { lbl, val }; + } + return { lbl, val: '', plaintext: JSON.stringify(analysis.result[label])}; + }); + return { + title: analysis.analyzer, + value: analysis.success ? '✅' : '❌', + fields, + }; + }); +}; + +const makeResults = (results: any) => { + const rows: { lbl: string; val?: any; plaintext?: string; list?: string[] }[] = []; + if (!results || !results.analysis || results.analysis.length === 0) { + return rows; + } + const caaWorker = results.analysis.find((a: any) => a.analyzer === 'caaWorker'); + if (caaWorker.result.host) rows.push({ lbl: 'Host', val: caaWorker.result.host }); + if (typeof caaWorker.result.has_caa === 'boolean') rows.push({ lbl: 'CA Authorization', val: caaWorker.result.has_caa }); + if (caaWorker.result.issue) rows.push({ lbl: 'CAAs allowed to Issue Certs', plaintext: caaWorker.result.issue.join('\n') }); + + const mozillaGradingWorker = (results.analysis.find((a: any) => a.analyzer === 'mozillaGradingWorker')).result; + if (mozillaGradingWorker.grade) rows.push({ lbl: 'Mozilla Grading', val: mozillaGradingWorker.grade }); + if (mozillaGradingWorker.gradeTrust) rows.push({ lbl: 'Mozilla Trust', val: mozillaGradingWorker.gradeTrust }); + + const symantecDistrust = (results.analysis.find((a: any) => a.analyzer === 'symantecDistrust')).result; + if (typeof symantecDistrust.isDistrusted === 'boolean') rows.push({ lbl: 'No distrusted symantec SSL?', val: !symantecDistrust.isDistrusted }); + if (symantecDistrust.reasons) rows.push({ lbl: 'Symantec Distrust', plaintext: symantecDistrust.reasons.join('\n') }); + + const top1m = (results.analysis.find((a: any) => a.analyzer === 'top1m')).result; + if (top1m.certificate.rank) rows.push({ lbl: 'Certificate Rank', val: top1m.certificate.rank.toLocaleString() }); + + const mozillaEvaluationWorker = (results.analysis.find((a: any) => a.analyzer === 'mozillaEvaluationWorker')).result; + if (mozillaEvaluationWorker.level) rows.push({ lbl: 'Mozilla Evaluation Level', val: mozillaEvaluationWorker.level }); + if (mozillaEvaluationWorker.failures) { + const { bad, old, intermediate, modern } = mozillaEvaluationWorker.failures; + if (bad) rows.push({ lbl: `Critical Security Issues (${bad.length})`, list: bad }); + if (old) rows.push({ lbl: `Compatibility Config Issues (${old.length})`, list: old }); + if (intermediate) rows.push({ lbl: `Intermediate Issues (${intermediate.length})`, list: intermediate }); + if (modern) rows.push({ lbl: `Modern Issues (${modern.length})`, list: modern }); + } + return rows; +}; + +const TlsCard = (props: {data: any, title: string, actionButtons: any }): JSX.Element => { + + const [tlsRowData, setTlsRowWata] = useState(makeExpandableData(props.data)); + const [tlsResults, setTlsResults] = useState(makeResults(props.data)); + const [loadState, setLoadState] = useState<undefined | 'loading' | 'success' | 'error'>(undefined); + + const updateData = (id: number) => { + setTlsRowWata([]); + setLoadState('loading'); + const fetchUrl = `https://tls-observatory.services.mozilla.com/api/v1/results?id=${id}`; + fetch(fetchUrl) + .then((response) => response.json()) + .then((data) => { + setTlsRowWata(makeExpandableData(data)); + setTlsResults(makeResults(data)); + setLoadState('success'); + }).catch(() => { + setLoadState('error'); + }); + }; + + const scanId = props.data?.id; + return ( + <Card heading={props.title} actionButtons={props.actionButtons}> + { tlsResults.length > 0 && tlsResults.map((row: any, index: number) => { + return ( + <Row lbl={row.lbl} val={row.val} plaintext={row.plaintext} listResults={row.list} /> + ); + })} + <Expandable> + <summary>Full Analysis Results</summary> + { tlsRowData.length > 0 && tlsRowData.map((cipherSuite: any, index: number) => { + return ( + <ExpandableRow lbl={cipherSuite.title} val={cipherSuite.value || '?'} rowList={cipherSuite.fields} /> + ); + })} + </Expandable> + { !tlsRowData.length && ( + <div> + <p>No entries available to analyze.<br /> + This sometimes happens when the report didn't finish generating in-time, you can try re-requesting it. + </p> + <Button loadState={loadState} onClick={() => updateData(scanId)}>Refetch Report</Button> + </div> + )} + </Card> + ); +} + +export default TlsCard; From 4bd3085fe94a25ef2efc22d78f008a5210468283 Mon Sep 17 00:00:00 2001 From: Alicia Sykes <alicia@omg.lol> Date: Thu, 24 Aug 2023 20:36:41 +0100 Subject: [PATCH 09/17] Adds TLS results, uses str array for IDs, reorders components --- src/pages/Results.tsx | 59 +++++++++++++++++++++++++++---------------- 1 file changed, 37 insertions(+), 22 deletions(-) diff --git a/src/pages/Results.tsx b/src/pages/Results.tsx index 69cb682..03cf14d 100644 --- a/src/pages/Results.tsx +++ b/src/pages/Results.tsx @@ -54,6 +54,9 @@ import ArchivesCard from 'components/Results/Archives'; import RankCard from 'components/Results/Rank'; import BlockListsCard from 'components/Results/BlockLists'; import ThreatsCard from 'components/Results/Threats'; +import TlsCipherSuitesCard from 'components/Results/TlsCipherSuites'; +import TlsIssueAnalysisCard from 'components/Results/TlsIssueAnalysis'; +import TlsClientSupportCard from 'components/Results/TlsClientSupport'; import keys from 'utils/get-keys'; import { determineAddressType, AddressType } from 'utils/address-type-checker'; @@ -96,12 +99,13 @@ const Results = (): JSX.Element => { const [modalOpen, setModalOpen] = useState(false); const [modalContent, setModalContent] = useState<ReactNode>(<></>); - const updateLoadingJobs = useCallback((job: string, newState: LoadingState, error?: string, retry?: () => void, data?: any) => { + const updateLoadingJobs = useCallback((jobs: string | string[], newState: LoadingState, error?: string, retry?: () => void, data?: any) => { + (typeof jobs === 'string' ? [jobs] : jobs).forEach((job: string) => { const now = new Date(); const timeTaken = now.getTime() - startTime; setLoadingJobs((prevJobs) => { const newJobs = prevJobs.map((loadingJob: LoadingJob) => { - if (loadingJob.name === job) { + if (job.includes(loadingJob.name)) { return { ...loadingJob, error, state: newState, timeTaken, retry }; } return loadingJob; @@ -137,6 +141,7 @@ const Results = (): JSX.Element => { } return newJobs; }); + }); }, [startTime]); const parseJson = (response: Response): Promise<any> => { @@ -239,7 +244,7 @@ const Results = (): JSX.Element => { // Get hostnames and associated domains from Shodan const [shoadnResults, updateShodanResults] = useMotherHook<ShodanResults>({ - jobId: 'hosts', + jobId: ['hosts', 'server-info'], updateLoadingJobs, addressInfo: { address: ipAddress, addressType: 'ipV4', expectedAddressTypes: ['ipV4', 'ipV6'] }, fetchRequest: () => fetch(`https://api.shodan.io/shodan/host/${ipAddress}?key=${keys.shodan}`) @@ -457,6 +462,14 @@ const Results = (): JSX.Element => { fetchRequest: () => fetch(`${api}/threats?url=${address}`).then(res => parseJson(res)), }); + // Get TLS security info, from Mozilla Observatory + const [tlsResults, updateTlsResults] = useMotherHook({ + jobId: ['tls-cipher-suites', 'tls-security-config', 'tls-client-support'], + updateLoadingJobs, + addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, + fetchRequest: () => fetch(`${api}/tls?url=${address}`).then(res => parseJson(res)), + }); + /* Cancel remaining jobs after 10 second timeout */ useEffect(() => { const checkJobs = () => { @@ -470,7 +483,7 @@ const Results = (): JSX.Element => { return () => { clearTimeout(timeoutId); }; - }, [loadingJobs, updateLoadingJobs]); // dependencies for the effect + }, [loadingJobs, updateLoadingJobs]); const makeSiteName = (address: string): string => { try { @@ -483,40 +496,42 @@ const Results = (): JSX.Element => { // A list of state sata, corresponding component and title for each card const resultCardData = [ { id: 'location', title: 'Server Location', result: locationResults, Component: ServerLocationCard, refresh: updateLocationResults }, - { id: 'ssl', title: 'SSL Info', result: sslResults, Component: SslCertCard, refresh: updateSslResults }, + { id: 'ssl', title: 'SSL Certificate', result: sslResults, Component: SslCertCard, refresh: updateSslResults }, + { id: 'domain', title: 'Domain Whois', result: domainLookupResults, Component: DomainLookup, refresh: updateDomainLookupResults }, + { id: 'quality', title: 'Quality Summary', result: lighthouseResults, Component: LighthouseCard, refresh: updateLighthouseResults }, + { id: 'tech-stack', title: 'Tech Stack', result: techStackResults, Component: TechStackCard, refresh: updateTechStackResults }, + { id: 'server-info', title: 'Server Info', result: shoadnResults?.serverInfo, Component: ServerInfoCard, refresh: updateShodanResults }, + { id: 'cookies', title: 'Cookies', result: cookieResults, Component: CookiesCard, refresh: updateCookieResults }, { id: 'headers', title: 'Headers', result: headersResults, Component: HeadersCard, refresh: updateHeadersResults }, - { id: 'domain', title: 'Whois', result: domainLookupResults, Component: DomainLookup, refresh: updateDomainLookupResults }, { id: 'dns', title: 'DNS Records', result: dnsResults, Component: DnsRecordsCard, refresh: updateDnsResults }, { id: 'hosts', title: 'Host Names', result: shoadnResults?.hostnames, Component: HostNamesCard, refresh: updateShodanResults }, { id: 'http-security', title: 'HTTP Security', result: httpSecurityResults, Component: HttpSecurityCard, refresh: updateHttpSecurityResults }, - { id: 'tech-stack', title: 'Tech Stack', result: techStackResults, Component: TechStackCard, refresh: updateTechStackResults }, - { id: 'quality', title: 'Quality Summary', result: lighthouseResults, Component: LighthouseCard, refresh: updateLighthouseResults }, { id: 'social-tags', title: 'Social Tags', result: socialTagResults, Component: SocialTagsCard, refresh: updateSocialTagResults }, - { id: 'cookies', title: 'Cookies', result: cookieResults, Component: CookiesCard, refresh: updateCookieResults }, { id: 'trace-route', title: 'Trace Route', result: traceRouteResults, Component: TraceRouteCard, refresh: updateTraceRouteResults }, + { id: 'screenshot', title: 'Screenshot', result: screenshotResult || lighthouseResults?.fullPageScreenshot?.screenshot, Component: ScreenshotCard, refresh: updateScreenshotResult }, + { id: 'security-txt', title: 'Security.Txt', result: securityTxtResults, Component: SecurityTxtCard, refresh: updateSecurityTxtResults }, + { id: 'dns-server', title: 'DNS Server', result: dnsServerResults, Component: DnsServerCard, refresh: updateDnsServerResults }, { id: 'firewall', title: 'Firewall', result: firewallResults, Component: FirewallCard, refresh: updateFirewallResults }, - { id: 'server-info', title: 'Server Info', result: shoadnResults?.serverInfo, Component: ServerInfoCard, refresh: updateShodanResults }, - { id: 'redirects', title: 'Redirects', result: redirectResults, Component: RedirectsCard, refresh: updateRedirectResults }, - { id: 'robots-txt', title: 'Crawl Rules', result: robotsTxtResults, Component: RobotsTxtCard, refresh: updateRobotsTxtResults }, { id: 'dnssec', title: 'DNSSEC', result: dnsSecResults, Component: DnsSecCard, refresh: updateDnsSecResults }, + { id: 'hsts', title: 'HSTS Check', result: hstsResults, Component: HstsCard, refresh: updateHstsResults }, + { id: 'threats', title: 'Threats', result: threatResults, Component: ThreatsCard, refresh: updateThreatResults }, + { id: 'mail-config', title: 'Email Configuration', result: mailConfigResults, Component: MailConfigCard, refresh: updateMailConfigResults }, + { id: 'archives', title: 'Archive History', result: archivesResults, Component: ArchivesCard, refresh: updateArchivesResults }, + { id: 'rank', title: 'Global Ranking', result: rankResults, Component: RankCard, refresh: updateRankResults }, + { id: 'tls-cipher-suites', title: 'TLS Cipher Suites', result: tlsResults, Component: TlsCipherSuitesCard, refresh: updateTlsResults }, + { id: 'tls-security-config', title: 'TLS Security Issues', result: tlsResults, Component: TlsIssueAnalysisCard, refresh: updateTlsResults }, + { id: 'tls-client-support', title: 'TLS Handshake Simulation', result: tlsResults, Component: TlsClientSupportCard, refresh: updateTlsResults }, + { id: 'redirects', title: 'Redirects', result: redirectResults, Component: RedirectsCard, refresh: updateRedirectResults }, + { id: 'linked-pages', title: 'Linked Pages', result: linkedPagesResults, Component: ContentLinksCard, refresh: updateLinkedPagesResults }, + { id: 'robots-txt', title: 'Crawl Rules', result: robotsTxtResults, Component: RobotsTxtCard, refresh: updateRobotsTxtResults }, { id: 'status', title: 'Server Status', result: serverStatusResults, Component: ServerStatusCard, refresh: updateServerStatusResults }, { id: 'ports', title: 'Open Ports', result: portsResults, Component: OpenPortsCard, refresh: updatePortsResults }, - { id: 'security-txt', title: 'Security.Txt', result: securityTxtResults, Component: SecurityTxtCard, refresh: updateSecurityTxtResults }, - { id: 'rank', title: 'Global Ranking', result: rankResults, Component: RankCard, refresh: updateRankResults }, - { id: 'screenshot', title: 'Screenshot', result: screenshotResult || lighthouseResults?.fullPageScreenshot?.screenshot, Component: ScreenshotCard, refresh: updateScreenshotResult }, - { id: 'mail-config', title: 'Email Configuration', result: mailConfigResults, Component: MailConfigCard, refresh: updateMailConfigResults }, - { id: 'hsts', title: 'HSTS Check', result: hstsResults, Component: HstsCard, refresh: updateHstsResults }, - { id: 'archives', title: 'Archive History', result: archivesResults, Component: ArchivesCard, refresh: updateArchivesResults }, { id: 'whois', title: 'Domain Info', result: whoIsResults, Component: WhoIsCard, refresh: updateWhoIsResults }, - { id: 'dns-server', title: 'DNS Server', result: dnsServerResults, Component: DnsServerCard, refresh: updateDnsServerResults }, - { id: 'linked-pages', title: 'Linked Pages', result: linkedPagesResults, Component: ContentLinksCard, refresh: updateLinkedPagesResults }, { id: 'txt-records', title: 'TXT Records', result: txtRecordResults, Component: TxtRecordCard, refresh: updateTxtRecordResults }, { id: 'block-lists', title: 'Block Lists', result: blockListsResults, Component: BlockListsCard, refresh: updateBlockListsResults }, - { id: 'threats', title: 'Threats', result: threatResults, Component: ThreatsCard, refresh: updateThreatResults }, { id: 'features', title: 'Site Features', result: siteFeaturesResults, Component: SiteFeaturesCard, refresh: updateSiteFeaturesResults }, { id: 'sitemap', title: 'Pages', result: sitemapResults, Component: SitemapCard, refresh: updateSitemapResults }, { id: 'carbon', title: 'Carbon Footprint', result: carbonResults, Component: CarbonFootprintCard, refresh: updateCarbonResults }, - ]; const MakeActionButtons = (title: string, refresh: () => void, showInfo: (id: string) => void): ReactNode => { From 805cc41bce1759ab97875d8cbb329e63acd38f6e Mon Sep 17 00:00:00 2001 From: Alicia Sykes <alicia@omg.lol> Date: Thu, 24 Aug 2023 20:36:59 +0100 Subject: [PATCH 10/17] TLS features --- src/components/misc/Loader.tsx | 12 +++++++++++- src/components/misc/ProgressBar.tsx | 5 ++++- src/utils/docs.ts | 8 ++++++++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/components/misc/Loader.tsx b/src/components/misc/Loader.tsx index 02230cc..0661368 100644 --- a/src/components/misc/Loader.tsx +++ b/src/components/misc/Loader.tsx @@ -16,6 +16,12 @@ const LoaderContainer = styled(StyledCard)` gap: 2rem; height: 50vh; transition: all 0.3s ease-in-out; + p.loadTimeInfo { + text-align: center; + margin: 0; + color: ${colors.textColorSecondary}; + opacity: 0.5; + } &.flex { display: flex; } @@ -46,7 +52,7 @@ const StyledSvg = styled.svg` const Loader = (props: { show: boolean }): JSX.Element => { return ( <LoaderContainer className={props.show ? '' : 'finished'}> - <Heading as="h4" color={colors.primary}>Fetching data...</Heading> + <Heading as="h4" color={colors.primary}>Crunching data...</Heading> <StyledSvg version="1.1" id="L7" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" viewBox="0 0 100 100" enableBackground="new 0 0 100 100"> <path fill="#fff" d="M31.6,3.5C5.9,13.6-6.6,42.7,3.5,68.4c10.1,25.7,39.2,38.3,64.9,28.1l-3.1-7.9c-21.3,8.4-45.4-2-53.8-23.3 @@ -83,6 +89,10 @@ const Loader = (props: { show: boolean }): JSX.Element => { repeatCount="indefinite" /> </path> </StyledSvg> + <p className="loadTimeInfo"> + It may take up-to a minute for all jobs to complete<br /> + You can view preliminary results as they come in below + </p> </LoaderContainer> ); } diff --git a/src/components/misc/ProgressBar.tsx b/src/components/misc/ProgressBar.tsx index 81eb98e..bf0b29e 100644 --- a/src/components/misc/ProgressBar.tsx +++ b/src/components/misc/ProgressBar.tsx @@ -195,7 +195,7 @@ const jobNames = [ 'quality', 'cookies', 'ssl', - // 'server-info', + 'server-info', 'redirects', 'robots-txt', 'dnssec', @@ -219,6 +219,9 @@ const jobNames = [ 'archives', 'block-lists', 'threats', + 'tls-cipher-suites', + 'tls-security-config', + 'tls-client-support', ] as const; export const initialJobs = jobNames.map((job: string) => { diff --git a/src/utils/docs.ts b/src/utils/docs.ts index ea4cbd8..0b2a753 100644 --- a/src/utils/docs.ts +++ b/src/utils/docs.ts @@ -481,6 +481,14 @@ const docs: Doc[] = [ ], screenshot: '', }, + { + id: 'tls', + title: 'TLS Configuration', + description: '', + use: '', + resources: [], + screenshot: '', + }, // { // id: '', // title: '', From 09f5af26dff18db7cc9d794710c281589df2949b Mon Sep 17 00:00:00 2001 From: Alicia Sykes <alicia@omg.lol> Date: Fri, 25 Aug 2023 18:08:44 +0100 Subject: [PATCH 11/17] Adds filter functionality to results --- src/components/Form/Heading.tsx | 1 + src/components/Results/TlsCipherSuites.tsx | 4 +- src/components/Results/TlsClientSupport.tsx | 16 +- src/pages/Results.tsx | 403 +++++++++++++++++--- 4 files changed, 360 insertions(+), 64 deletions(-) diff --git a/src/components/Form/Heading.tsx b/src/components/Form/Heading.tsx index 582ae3a..cdd40e8 100644 --- a/src/components/Form/Heading.tsx +++ b/src/components/Form/Heading.tsx @@ -28,6 +28,7 @@ const StyledHeading = styled.h1<HeadingProps>` a { // If a title is a link, keep title styles color: inherit; text-decoration: none; + display: flex; } ${props => { switch (props.size) { diff --git a/src/components/Results/TlsCipherSuites.tsx b/src/components/Results/TlsCipherSuites.tsx index 57cc581..bfa3de7 100644 --- a/src/components/Results/TlsCipherSuites.tsx +++ b/src/components/Results/TlsCipherSuites.tsx @@ -1,10 +1,8 @@ import { useState } from 'react'; -import styled from 'styled-components'; -import colors from 'styles/colors'; import { Card } from 'components/Form/Card'; import Button from 'components/Form/Button'; -import Row, { ExpandableRow } from 'components/Form/Row'; +import { ExpandableRow } from 'components/Form/Row'; const makeCipherSuites = (results: any) => { if (!results || !results.connection_info || (results.connection_info.ciphersuite || [])?.length === 0) { diff --git a/src/components/Results/TlsClientSupport.tsx b/src/components/Results/TlsClientSupport.tsx index 609e69b..84464c5 100644 --- a/src/components/Results/TlsClientSupport.tsx +++ b/src/components/Results/TlsClientSupport.tsx @@ -1,22 +1,8 @@ import { useState } from 'react'; -import styled from 'styled-components'; -import colors from 'styles/colors'; import { Card } from 'components/Form/Card'; import Button from 'components/Form/Button'; -import Row, { ExpandableRow } from 'components/Form/Row'; - - -// "name": "Android", -// "curve": "secp256r1", -// "version": "6.0", -// "platform": "", -// "protocol": "TLSv1.2", -// "curve_code": 23, -// "ciphersuite": "ECDHE-RSA-CHACHA20-POLY1305-OLD", -// "is_supported": true, -// "protocol_code": 771, -// "ciphersuite_code": 52243 +import { ExpandableRow } from 'components/Form/Row'; const makeClientSupport = (results: any) => { if (!results?.analysis) return []; diff --git a/src/pages/Results.tsx b/src/pages/Results.tsx index 03cf14d..ff36926 100644 --- a/src/pages/Results.tsx +++ b/src/pages/Results.tsx @@ -1,5 +1,5 @@ import { useState, useEffect, useCallback, ReactNode } from 'react'; -import { useParams } from 'react-router-dom'; +import { useParams } from 'react-router-dom'; import styled from 'styled-components'; import { ToastContainer } from 'react-toastify'; import Masonry from 'react-masonry-css' @@ -89,6 +89,56 @@ const ResultsContent = styled.section` padding-bottom: 1rem; `; +const FilterButtons = styled.div` + width: 95vw; + margin: auto; + display: flex; + flex-wrap: wrap; + justify-content: space-between; + gap: 1rem; + .one-half { + display: flex; + flex-wrap: wrap; + gap: 1rem; + align-items: center; + } + button, input { + background: ${colors.backgroundLighter}; + color: ${colors.textColor}; + border: none; + border-radius: 4px; + font-family: 'PTMono'; + padding: 0.25rem 0.5rem; + border: 1px solid transparent; + transition: all 0.2s ease-in-out; + } + button { + cursor: pointer; + text-transform: capitalize; + box-shadow: 2px 2px 0px ${colors.bgShadowColor}; + transition: all 0.2s ease-in-out; + &:hover { + box-shadow: 4px 4px 0px ${colors.bgShadowColor}; + color: ${colors.primary}; + } + &.selected { + border: 1px solid ${colors.primary}; + color: ${colors.primary}; + } + } + input:focus { + border: 1px solid ${colors.primary}; + outline: none; + } + .clear { + color: ${colors.textColor}; + text-decoration: underline; + cursor: pointer; + font-size: 0.8rem; + opacity: 0.8; + } +`; + const Results = (): JSX.Element => { const startTime = new Date().getTime(); @@ -98,6 +148,18 @@ const Results = (): JSX.Element => { const [loadingJobs, setLoadingJobs] = useState<LoadingJob[]>(initialJobs); const [modalOpen, setModalOpen] = useState(false); const [modalContent, setModalContent] = useState<ReactNode>(<></>); + const [searchTerm, setSearchTerm] = useState<string>(''); + const [tags, setTags] = useState<string[]>([]); + + const clearFilters = () => { + setTags([]); + setSearchTerm(''); + }; + const updateTags = (tag: string) => { + // Remove current tag if it exists, otherwise add it + // setTags(tags.includes(tag) ? tags.filter(t => t !== tag) : [...tags, tag]); + setTags(tags.includes(tag) ? tags.filter(t => t !== tag) : [tag]); + }; const updateLoadingJobs = useCallback((jobs: string | string[], newState: LoadingState, error?: string, retry?: () => void, data?: any) => { (typeof jobs === 'string' ? [jobs] : jobs).forEach((job: string) => { @@ -494,47 +556,269 @@ const Results = (): JSX.Element => { } // A list of state sata, corresponding component and title for each card - const resultCardData = [ - { id: 'location', title: 'Server Location', result: locationResults, Component: ServerLocationCard, refresh: updateLocationResults }, - { id: 'ssl', title: 'SSL Certificate', result: sslResults, Component: SslCertCard, refresh: updateSslResults }, - { id: 'domain', title: 'Domain Whois', result: domainLookupResults, Component: DomainLookup, refresh: updateDomainLookupResults }, - { id: 'quality', title: 'Quality Summary', result: lighthouseResults, Component: LighthouseCard, refresh: updateLighthouseResults }, - { id: 'tech-stack', title: 'Tech Stack', result: techStackResults, Component: TechStackCard, refresh: updateTechStackResults }, - { id: 'server-info', title: 'Server Info', result: shoadnResults?.serverInfo, Component: ServerInfoCard, refresh: updateShodanResults }, - { id: 'cookies', title: 'Cookies', result: cookieResults, Component: CookiesCard, refresh: updateCookieResults }, - { id: 'headers', title: 'Headers', result: headersResults, Component: HeadersCard, refresh: updateHeadersResults }, - { id: 'dns', title: 'DNS Records', result: dnsResults, Component: DnsRecordsCard, refresh: updateDnsResults }, - { id: 'hosts', title: 'Host Names', result: shoadnResults?.hostnames, Component: HostNamesCard, refresh: updateShodanResults }, - { id: 'http-security', title: 'HTTP Security', result: httpSecurityResults, Component: HttpSecurityCard, refresh: updateHttpSecurityResults }, - { id: 'social-tags', title: 'Social Tags', result: socialTagResults, Component: SocialTagsCard, refresh: updateSocialTagResults }, - { id: 'trace-route', title: 'Trace Route', result: traceRouteResults, Component: TraceRouteCard, refresh: updateTraceRouteResults }, - { id: 'screenshot', title: 'Screenshot', result: screenshotResult || lighthouseResults?.fullPageScreenshot?.screenshot, Component: ScreenshotCard, refresh: updateScreenshotResult }, - { id: 'security-txt', title: 'Security.Txt', result: securityTxtResults, Component: SecurityTxtCard, refresh: updateSecurityTxtResults }, - { id: 'dns-server', title: 'DNS Server', result: dnsServerResults, Component: DnsServerCard, refresh: updateDnsServerResults }, - { id: 'firewall', title: 'Firewall', result: firewallResults, Component: FirewallCard, refresh: updateFirewallResults }, - { id: 'dnssec', title: 'DNSSEC', result: dnsSecResults, Component: DnsSecCard, refresh: updateDnsSecResults }, - { id: 'hsts', title: 'HSTS Check', result: hstsResults, Component: HstsCard, refresh: updateHstsResults }, - { id: 'threats', title: 'Threats', result: threatResults, Component: ThreatsCard, refresh: updateThreatResults }, - { id: 'mail-config', title: 'Email Configuration', result: mailConfigResults, Component: MailConfigCard, refresh: updateMailConfigResults }, - { id: 'archives', title: 'Archive History', result: archivesResults, Component: ArchivesCard, refresh: updateArchivesResults }, - { id: 'rank', title: 'Global Ranking', result: rankResults, Component: RankCard, refresh: updateRankResults }, - { id: 'tls-cipher-suites', title: 'TLS Cipher Suites', result: tlsResults, Component: TlsCipherSuitesCard, refresh: updateTlsResults }, - { id: 'tls-security-config', title: 'TLS Security Issues', result: tlsResults, Component: TlsIssueAnalysisCard, refresh: updateTlsResults }, - { id: 'tls-client-support', title: 'TLS Handshake Simulation', result: tlsResults, Component: TlsClientSupportCard, refresh: updateTlsResults }, - { id: 'redirects', title: 'Redirects', result: redirectResults, Component: RedirectsCard, refresh: updateRedirectResults }, - { id: 'linked-pages', title: 'Linked Pages', result: linkedPagesResults, Component: ContentLinksCard, refresh: updateLinkedPagesResults }, - { id: 'robots-txt', title: 'Crawl Rules', result: robotsTxtResults, Component: RobotsTxtCard, refresh: updateRobotsTxtResults }, - { id: 'status', title: 'Server Status', result: serverStatusResults, Component: ServerStatusCard, refresh: updateServerStatusResults }, - { id: 'ports', title: 'Open Ports', result: portsResults, Component: OpenPortsCard, refresh: updatePortsResults }, - { id: 'whois', title: 'Domain Info', result: whoIsResults, Component: WhoIsCard, refresh: updateWhoIsResults }, - { id: 'txt-records', title: 'TXT Records', result: txtRecordResults, Component: TxtRecordCard, refresh: updateTxtRecordResults }, - { id: 'block-lists', title: 'Block Lists', result: blockListsResults, Component: BlockListsCard, refresh: updateBlockListsResults }, - { id: 'features', title: 'Site Features', result: siteFeaturesResults, Component: SiteFeaturesCard, refresh: updateSiteFeaturesResults }, - { id: 'sitemap', title: 'Pages', result: sitemapResults, Component: SitemapCard, refresh: updateSitemapResults }, - { id: 'carbon', title: 'Carbon Footprint', result: carbonResults, Component: CarbonFootprintCard, refresh: updateCarbonResults }, - ]; + const resultCardData = [{ + id: 'location', + title: 'Server Location', + result: locationResults, + Component: ServerLocationCard, + refresh: updateLocationResults, + tags: ['server'], + }, { + id: 'ssl', + title: 'SSL Certificate', + result: sslResults, + Component: SslCertCard, + refresh: updateSslResults, + tags: ['server', 'security'], + }, { + id: 'domain', + title: 'Domain Whois', + result: domainLookupResults, + Component: DomainLookup, + refresh: updateDomainLookupResults, + tags: ['server'], + }, { + id: 'quality', + title: 'Quality Summary', + result: lighthouseResults, + Component: LighthouseCard, + refresh: updateLighthouseResults, + tags: ['client'], + }, { + id: 'tech-stack', + title: 'Tech Stack', + result: techStackResults, + Component: TechStackCard, + refresh: updateTechStackResults, + tags: ['client', 'meta'], + }, { + id: 'server-info', + title: 'Server Info', + result: shoadnResults?.serverInfo, + Component: ServerInfoCard, + refresh: updateShodanResults, + tags: ['server'], + }, { + id: 'cookies', + title: 'Cookies', + result: cookieResults, + Component: CookiesCard, + refresh: updateCookieResults, + tags: ['client', 'security'], + }, { + id: 'headers', + title: 'Headers', + result: headersResults, + Component: HeadersCard, + refresh: updateHeadersResults, + tags: ['client', 'security'], + }, { + id: 'dns', + title: 'DNS Records', + result: dnsResults, + Component: DnsRecordsCard, + refresh: updateDnsResults, + tags: ['server'], + }, { + id: 'hosts', + title: 'Host Names', + result: shoadnResults?.hostnames, + Component: HostNamesCard, + refresh: updateShodanResults, + tags: ['server'], + }, { + id: 'http-security', + title: 'HTTP Security', + result: httpSecurityResults, + Component: HttpSecurityCard, + refresh: updateHttpSecurityResults, + tags: ['security'], + }, { + id: 'social-tags', + title: 'Social Tags', + result: socialTagResults, + Component: SocialTagsCard, + refresh: updateSocialTagResults, + tags: ['client', 'meta'], + }, { + id: 'trace-route', + title: 'Trace Route', + result: traceRouteResults, + Component: TraceRouteCard, + refresh: updateTraceRouteResults, + tags: ['server'], + }, { + id: 'screenshot', + title: 'Screenshot', + result: screenshotResult || lighthouseResults?.fullPageScreenshot?.screenshot, + Component: ScreenshotCard, + refresh: updateScreenshotResult, + tags: ['client', 'meta'], + }, { + id: 'security-txt', + title: 'Security.Txt', + result: securityTxtResults, + Component: SecurityTxtCard, + refresh: updateSecurityTxtResults, + tags: ['security'], + }, { + id: 'dns-server', + title: 'DNS Server', + result: dnsServerResults, + Component: DnsServerCard, + refresh: updateDnsServerResults, + tags: ['server'], + }, { + id: 'firewall', + title: 'Firewall', + result: firewallResults, + Component: FirewallCard, + refresh: updateFirewallResults, + tags: ['server', 'security'], + }, { + id: 'dnssec', + title: 'DNSSEC', + result: dnsSecResults, + Component: DnsSecCard, + refresh: updateDnsSecResults, + tags: ['security'], + }, { + id: 'hsts', + title: 'HSTS Check', + result: hstsResults, + Component: HstsCard, + refresh: updateHstsResults, + tags: ['security'], + }, { + id: 'threats', + title: 'Threats', + result: threatResults, + Component: ThreatsCard, + refresh: updateThreatResults, + tags: ['security'], + }, { + id: 'mail-config', + title: 'Email Configuration', + result: mailConfigResults, + Component: MailConfigCard, + refresh: updateMailConfigResults, + tags: ['server'], + }, { + id: 'archives', + title: 'Archive History', + result: archivesResults, + Component: ArchivesCard, + refresh: updateArchivesResults, + tags: ['meta'], + }, { + id: 'rank', + title: 'Global Ranking', + result: rankResults, + Component: RankCard, + refresh: updateRankResults, + tags: ['meta'], + }, { + id: 'tls-cipher-suites', + title: 'TLS Cipher Suites', + result: tlsResults, + Component: TlsCipherSuitesCard, + refresh: updateTlsResults, + tags: ['server', 'security'], + }, { + id: 'tls-security-config', + title: 'TLS Security Issues', + result: tlsResults, + Component: TlsIssueAnalysisCard, + refresh: updateTlsResults, + tags: ['security'], + }, { + id: 'tls-client-support', + title: 'TLS Handshake Simulation', + result: tlsResults, + Component: TlsClientSupportCard, + refresh: updateTlsResults, + tags: ['security'], + }, { + id: 'redirects', + title: 'Redirects', + result: redirectResults, + Component: RedirectsCard, + refresh: updateRedirectResults, + tags: ['meta'], + }, { + id: 'linked-pages', + title: 'Linked Pages', + result: linkedPagesResults, + Component: ContentLinksCard, + refresh: updateLinkedPagesResults, + tags: ['client', 'meta'], + }, { + id: 'robots-txt', + title: 'Crawl Rules', + result: robotsTxtResults, + Component: RobotsTxtCard, + refresh: updateRobotsTxtResults, + tags: ['meta'], + }, { + id: 'status', + title: 'Server Status', + result: serverStatusResults, + Component: ServerStatusCard, + refresh: updateServerStatusResults, + tags: ['server'], + }, { + id: 'ports', + title: 'Open Ports', + result: portsResults, + Component: OpenPortsCard, + refresh: updatePortsResults, + tags: ['server'], + }, { + id: 'whois', + title: 'Domain Info', + result: whoIsResults, + Component: WhoIsCard, + refresh: updateWhoIsResults, + tags: ['server'], + }, { + id: 'txt-records', + title: 'TXT Records', + result: txtRecordResults, + Component: TxtRecordCard, + refresh: updateTxtRecordResults, + tags: ['server'], + }, { + id: 'block-lists', + title: 'Block Lists', + result: blockListsResults, + Component: BlockListsCard, + refresh: updateBlockListsResults, + tags: ['security', 'meta'], + }, { + id: 'features', + title: 'Site Features', + result: siteFeaturesResults, + Component: SiteFeaturesCard, + refresh: updateSiteFeaturesResults, + tags: ['meta'], + }, { + id: 'sitemap', + title: 'Pages', + result: sitemapResults, + Component: SitemapCard, + refresh: updateSitemapResults, + tags: ['meta'], + }, { + id: 'carbon', + title: 'Carbon Footprint', + result: carbonResults, + Component: CarbonFootprintCard, + refresh: updateCarbonResults, + tags: ['meta'], + }, +]; - const MakeActionButtons = (title: string, refresh: () => void, showInfo: (id: string) => void): ReactNode => { + const makeActionButtons = (title: string, refresh: () => void, showInfo: (id: string) => void): ReactNode => { const actions = [ { label: `Info about ${title}`, onClick: showInfo, icon: 'ⓘ'}, { label: `Re-fetch ${title} data`, onClick: refresh, icon: '↻'}, @@ -559,7 +843,7 @@ const Results = (): JSX.Element => { <Nav> { address && <Heading color={colors.textColor} size="medium"> - { addressType === 'url' && <img width="32px" src={`https://icon.horse/icon/${makeSiteName(address)}`} alt="" /> } + { addressType === 'url' && <a href={address}><img width="32px" src={`https://icon.horse/icon/${makeSiteName(address)}`} alt="" /></a> } {makeSiteName(address)} </Heading> } @@ -567,21 +851,48 @@ const Results = (): JSX.Element => { <ProgressBar loadStatus={loadingJobs} showModal={showErrorModal} showJobDocs={showInfo} /> { address?.includes(window?.location?.hostname || 'web-check.as93.net') && <SelfScanMsg />} <Loader show={loadingJobs.filter((job: LoadingJob) => job.state !== 'loading').length < 5} /> + <FilterButtons> + <div className="one-half"> + <span className="group-label">Filter by</span> + {['server', 'client', 'meta'].map((tag: string) => ( + <button + key={tag} + className={tags.includes(tag) ? 'selected' : ''} + onClick={() => updateTags(tag)}> + {tag} + </button> + ))} + {(tags.length > 0 || searchTerm.length > 0) && <span onClick={clearFilters} className="clear">Clear Filters</span> } + </div> + <div className="one-half"> + <input + type="text" + placeholder="Filter Results" + value={searchTerm} + onChange={(e) => setSearchTerm(e.target.value)} + /> + <span className="group-label">Search</span> + </div> + </FilterButtons> <ResultsContent> - - <Masonry + <Masonry breakpointCols={{ 10000: 12, 4000: 9, 3600: 8, 3200: 7, 2800: 6, 2400: 5, 2000: 4, 1600: 3, 1200: 2, 800: 1 }} className="masonry-grid" columnClassName="masonry-grid-col"> { - resultCardData.map(({ id, title, result, refresh, Component }, index: number) => ( + resultCardData + .filter(({ tags: cardTags, title }) => ( + tags.length === 0 || tags.some(tag => cardTags.includes(tag))) && + title.toLowerCase().includes(searchTerm.toLowerCase()) + ) + .map(({ id, title, result, refresh, Component }, index: number) => ( (result && !result.error) ? ( <ErrorBoundary title={title}> <Component key={`${title}-${index}`} data={{...result}} title={title} - actionButtons={refresh ? MakeActionButtons(title, refresh, () => showInfo(id)) : undefined} + actionButtons={refresh ? makeActionButtons(title, refresh, () => showInfo(id)) : undefined} /> </ErrorBoundary> ) : <></> From b6b0c25966b3a8af2e37d6331fec3a9e95788016 Mon Sep 17 00:00:00 2001 From: Alicia Sykes <alicia@omg.lol> Date: Fri, 25 Aug 2023 21:02:28 +0100 Subject: [PATCH 12/17] Refactor, toggle filters, re-order screenshot --- src/pages/Results.tsx | 41 +++++++++++++++++++++++------------------ 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/src/pages/Results.tsx b/src/pages/Results.tsx index ff36926..b486148 100644 --- a/src/pages/Results.tsx +++ b/src/pages/Results.tsx @@ -102,7 +102,7 @@ const FilterButtons = styled.div` gap: 1rem; align-items: center; } - button, input { + button, input, .toggle-filters { background: ${colors.backgroundLighter}; color: ${colors.textColor}; border: none; @@ -112,7 +112,7 @@ const FilterButtons = styled.div` border: 1px solid transparent; transition: all 0.2s ease-in-out; } - button { + button, .toggle-filters { cursor: pointer; text-transform: capitalize; box-shadow: 2px 2px 0px ${colors.bgShadowColor}; @@ -137,6 +137,9 @@ const FilterButtons = styled.div` font-size: 0.8rem; opacity: 0.8; } + .toggle-filters { + font-size: 0.8rem; + } `; const Results = (): JSX.Element => { @@ -148,6 +151,7 @@ const Results = (): JSX.Element => { const [loadingJobs, setLoadingJobs] = useState<LoadingJob[]>(initialJobs); const [modalOpen, setModalOpen] = useState(false); const [modalContent, setModalContent] = useState<ReactNode>(<></>); + const [showFilters, setShowFilters] = useState(false); const [searchTerm, setSearchTerm] = useState<string>(''); const [tags, setTags] = useState<string[]>([]); @@ -647,13 +651,6 @@ const Results = (): JSX.Element => { Component: TraceRouteCard, refresh: updateTraceRouteResults, tags: ['server'], - }, { - id: 'screenshot', - title: 'Screenshot', - result: screenshotResult || lighthouseResults?.fullPageScreenshot?.screenshot, - Component: ScreenshotCard, - refresh: updateScreenshotResult, - tags: ['client', 'meta'], }, { id: 'security-txt', title: 'Security.Txt', @@ -717,6 +714,13 @@ const Results = (): JSX.Element => { Component: RankCard, refresh: updateRankResults, tags: ['meta'], + }, { + id: 'screenshot', + title: 'Screenshot', + result: screenshotResult || lighthouseResults?.fullPageScreenshot?.screenshot, + Component: ScreenshotCard, + refresh: updateScreenshotResult, + tags: ['client', 'meta'], }, { id: 'tls-cipher-suites', title: 'TLS Cipher Suites', @@ -851,7 +855,7 @@ const Results = (): JSX.Element => { <ProgressBar loadStatus={loadingJobs} showModal={showErrorModal} showJobDocs={showInfo} /> { address?.includes(window?.location?.hostname || 'web-check.as93.net') && <SelfScanMsg />} <Loader show={loadingJobs.filter((job: LoadingJob) => job.state !== 'loading').length < 5} /> - <FilterButtons> + <FilterButtons>{ showFilters ? <> <div className="one-half"> <span className="group-label">Filter by</span> {['server', 'client', 'meta'].map((tag: string) => ( @@ -873,6 +877,9 @@ const Results = (): JSX.Element => { /> <span className="group-label">Search</span> </div> + </> : ( + <span className="toggle-filters" onClick={() => setShowFilters(true)}>Show Filters</span> + ) } </FilterButtons> <ResultsContent> <Masonry @@ -881,12 +888,11 @@ const Results = (): JSX.Element => { columnClassName="masonry-grid-col"> { resultCardData - .filter(({ tags: cardTags, title }) => ( - tags.length === 0 || tags.some(tag => cardTags.includes(tag))) && - title.toLowerCase().includes(searchTerm.toLowerCase()) - ) - .map(({ id, title, result, refresh, Component }, index: number) => ( - (result && !result.error) ? ( + .map(({ id, title, result, tags, refresh, Component }, index: number) => { + const show = (tags.length === 0 || tags.some(tag => tags.includes(tag))) + && title.toLowerCase().includes(searchTerm.toLowerCase()) + && (result && !result.error); + return show ? ( <ErrorBoundary title={title}> <Component key={`${title}-${index}`} @@ -895,8 +901,7 @@ const Results = (): JSX.Element => { actionButtons={refresh ? makeActionButtons(title, refresh, () => showInfo(id)) : undefined} /> </ErrorBoundary> - ) : <></> - )) + ) : null}) } </Masonry> </ResultsContent> From 749a61358ce7f3e01e66fe6826c9a1d8ed6ddc77 Mon Sep 17 00:00:00 2001 From: Alicia Sykes <alicia@omg.lol> Date: Sat, 26 Aug 2023 13:09:08 +0100 Subject: [PATCH 13/17] Improve SSL check, by useing direct TLS connection and listen for secureConnect event --- api/ssl.js | 47 ++++++++++++++++++++++------------------------- 1 file changed, 22 insertions(+), 25 deletions(-) diff --git a/api/ssl.js b/api/ssl.js index 2305867..fb2217d 100644 --- a/api/ssl.js +++ b/api/ssl.js @@ -1,43 +1,40 @@ -const https = require('https'); +const tls = require('tls'); const middleware = require('./_common/middleware'); -const urlModule = require('url'); const fetchSiteCertificateHandler = async (urlString) => { try { - const parsedUrl = urlModule.parse(urlString); + const parsedUrl = new URL(urlString); const options = { host: parsedUrl.hostname, - port: parsedUrl.port || 443, // Default port for HTTPS - method: 'GET', - servername: parsedUrl.hostname, // For SNI - rejectUnauthorized: false // Disable strict SSL verification (use with caution) + port: parsedUrl.port || 443, + servername: parsedUrl.hostname, + rejectUnauthorized: false, }; - const response = await new Promise((resolve, reject) => { - const req = https.request(options, res => { - - // Check if the SSL handshake was authorized - if (!res.socket.authorized) { - reject(new Error(`SSL handshake not authorized. Reason: ${res.socket.authorizationError}`)); - } else { - let cert = res.socket.getPeerCertificate(true); - if (!cert || Object.keys(cert).length === 0) { - reject(new Error("No certificate presented by the server.")); - } else { - const { raw, issuerCertificate, ...certWithoutRaw } = cert; - resolve(certWithoutRaw); - } + return new Promise((resolve, reject) => { + const socket = tls.connect(options, () => { + if (!socket.authorized) { + return reject(new Error(`SSL handshake not authorized. Reason: ${socket.authorizationError}`)); } + + const cert = socket.getPeerCertificate(); + if (!cert || Object.keys(cert).length === 0) { + return reject(new Error(` + No certificate presented by the server.\n + The server is possibly not using SNI (Server Name Indication) to identify itself, and you are connecting to a hostname-aliased IP address. + Or it may be due to an invalid SSL certificate, or an incomplete SSL handshake at the time the cert is being read.`)); + } + + const { raw, issuerCertificate, ...certWithoutRaw } = cert; + resolve(certWithoutRaw); + socket.end(); }); - req.on('error', error => { + socket.on('error', (error) => { reject(new Error(`Error fetching site certificate: ${error.message}`)); }); - - req.end(); }); - return response; } catch (error) { throw new Error(error.message); } From d805848dd7fd74d1186ba363ab94641cff9c48a7 Mon Sep 17 00:00:00 2001 From: Alicia Sykes <alicia@omg.lol> Date: Sat, 26 Aug 2023 13:09:57 +0100 Subject: [PATCH 14/17] Re-order fetch requests, and progress bar text to match the UI --- src/components/misc/ProgressBar.tsx | 58 +- src/pages/Results.tsx | 921 ++++++++++++++-------------- 2 files changed, 498 insertions(+), 481 deletions(-) diff --git a/src/components/misc/ProgressBar.tsx b/src/components/misc/ProgressBar.tsx index bf0b29e..b6d2c16 100644 --- a/src/components/misc/ProgressBar.tsx +++ b/src/components/misc/ProgressBar.tsx @@ -186,42 +186,42 @@ export interface LoadingJob { const jobNames = [ 'get-ip', 'location', - 'headers', - 'domain', - 'dns', - 'dns-server', - 'tech-stack', - 'hosts', - 'quality', - 'cookies', 'ssl', + 'domain', + 'quality', + 'tech-stack', 'server-info', - 'redirects', - 'robots-txt', - 'dnssec', - 'status', - 'ports', - 'screenshot', - 'txt-records', - 'sitemap', - 'hsts', - 'security-txt', - 'social-tags', - 'linked-pages', - 'mail-config', - // 'whois', - 'features', - 'carbon', - 'trace-route', - 'firewall', + 'cookies', + 'headers', + 'dns', + 'hosts', 'http-security', - 'rank', - 'archives', - 'block-lists', + 'social-tags', + 'trace-route', + 'security-txt', + 'dns-server', + 'firewall', + 'dnssec', + 'hsts', 'threats', + 'mail-config', + 'archives', + 'rank', + 'screenshot', 'tls-cipher-suites', 'tls-security-config', 'tls-client-support', + 'redirects', + 'linked-pages', + 'robots-txt', + 'status', + 'ports', + // 'whois', + 'txt-records', + 'block-lists', + 'features', + 'sitemap', + 'carbon', ] as const; export const initialJobs = jobNames.map((job: string) => { diff --git a/src/pages/Results.tsx b/src/pages/Results.tsx index b486148..3e21841 100644 --- a/src/pages/Results.tsx +++ b/src/pages/Results.tsx @@ -140,6 +140,15 @@ const FilterButtons = styled.div` .toggle-filters { font-size: 0.8rem; } + .control-options { + display: flex; + flex-wrap: wrap; + gap: 1rem; + align-items: center; + a { + text-decoration: none; + } + } `; const Results = (): JSX.Element => { @@ -246,6 +255,16 @@ const Results = (): JSX.Element => { } }, [address, addressType, setIpAddress]); + // Get IP address location info + const [locationResults, updateLocationResults] = useMotherHook<ServerLocation>({ + jobId: 'location', + updateLoadingJobs, + addressInfo: { address: ipAddress, addressType: 'ipV4', expectedAddressTypes: ['ipV4', 'ipV6'] }, + fetchRequest: () => fetch(`https://ipapi.co/${ipAddress}/json/`) + .then(res => parseJson(res)) + .then(res => getLocation(res)), + }); + // Fetch and parse SSL certificate info const [sslResults, updateSslResults] = useMotherHook({ jobId: 'ssl', @@ -254,6 +273,42 @@ const Results = (): JSX.Element => { fetchRequest: () => fetch(`${api}/ssl?url=${address}`).then((res) => parseJson(res)), }); + // Run a manual whois lookup on the domain + const [domainLookupResults, updateDomainLookupResults] = useMotherHook({ + jobId: 'domain', + updateLoadingJobs, + addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, + fetchRequest: () => fetch(`${api}/whois?url=${address}`).then(res => parseJson(res)), + }); + + // Fetch and parse Lighthouse performance data + const [lighthouseResults, updateLighthouseResults] = useMotherHook({ + jobId: 'quality', + updateLoadingJobs, + addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, + fetchRequest: () => fetch(`${api}/quality?url=${address}`) + .then(res => parseJson(res)) + .then(res => res?.lighthouseResult || { error: 'No Data'}), + }); + + // Get the technologies used to build site, using Wappalyzer + const [techStackResults, updateTechStackResults] = useMotherHook({ + jobId: 'tech-stack', + updateLoadingJobs, + addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, + fetchRequest: () => fetch(`${api}/tech-stack?url=${address}`).then(res => parseJson(res)), + }); + + // Get hostnames and associated domains from Shodan + const [shoadnResults, updateShodanResults] = useMotherHook<ShodanResults>({ + jobId: ['hosts', 'server-info'], + updateLoadingJobs, + addressInfo: { address: ipAddress, addressType: 'ipV4', expectedAddressTypes: ['ipV4', 'ipV6'] }, + fetchRequest: () => fetch(`https://api.shodan.io/shodan/host/${ipAddress}?key=${keys.shodan}`) + .then(res => parseJson(res)) + .then(res => parseShodanResults(res)), + }); + // Fetch and parse cookies info const [cookieResults, updateCookieResults] = useMotherHook<{cookies: Cookie[]}>({ jobId: 'cookies', @@ -263,15 +318,6 @@ const Results = (): JSX.Element => { .then(res => parseJson(res)), }); - // Fetch and parse crawl rules from robots.txt - const [robotsTxtResults, updateRobotsTxtResults] = useMotherHook<{robots: RowProps[]}>({ - jobId: 'robots-txt', - updateLoadingJobs, - addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, - fetchRequest: () => fetch(`${api}/robots-txt?url=${address}`) - .then(res => parseJson(res)), - }); - // Fetch and parse headers const [headersResults, updateHeadersResults] = useMotherHook({ jobId: 'headers', @@ -288,34 +334,149 @@ const Results = (): JSX.Element => { fetchRequest: () => fetch(`${api}/dns?url=${address}`).then(res => parseJson(res)), }); - // Fetch and parse Lighthouse performance data - const [lighthouseResults, updateLighthouseResults] = useMotherHook({ - jobId: 'quality', + // Get HTTP security + const [httpSecurityResults, updateHttpSecurityResults] = useMotherHook({ + jobId: 'http-security', updateLoadingJobs, addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, - fetchRequest: () => fetch(`${api}/quality?url=${address}`) - .then(res => parseJson(res)) - .then(res => res?.lighthouseResult || { error: 'No Data'}), + fetchRequest: () => fetch(`${api}/http-security?url=${address}`).then(res => parseJson(res)), }); - // Get IP address location info - const [locationResults, updateLocationResults] = useMotherHook<ServerLocation>({ - jobId: 'location', + // Get social media previews, from a sites social meta tags + const [socialTagResults, updateSocialTagResults] = useMotherHook({ + jobId: 'social-tags', updateLoadingJobs, - addressInfo: { address: ipAddress, addressType: 'ipV4', expectedAddressTypes: ['ipV4', 'ipV6'] }, - fetchRequest: () => fetch(`https://ipapi.co/${ipAddress}/json/`) - .then(res => parseJson(res)) - .then(res => getLocation(res)), + addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, + fetchRequest: () => fetch(`${api}/social-tags?url=${address}`).then(res => parseJson(res)), }); - // Get hostnames and associated domains from Shodan - const [shoadnResults, updateShodanResults] = useMotherHook<ShodanResults>({ - jobId: ['hosts', 'server-info'], + // Get trace route for a given hostname + const [traceRouteResults, updateTraceRouteResults] = useMotherHook({ + jobId: 'trace-route', updateLoadingJobs, - addressInfo: { address: ipAddress, addressType: 'ipV4', expectedAddressTypes: ['ipV4', 'ipV6'] }, - fetchRequest: () => fetch(`https://api.shodan.io/shodan/host/${ipAddress}?key=${keys.shodan}`) - .then(res => parseJson(res)) - .then(res => parseShodanResults(res)), + addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, + fetchRequest: () => fetch(`${api}/trace-route?url=${address}`).then(res => parseJson(res)), + }); + + // Get a websites listed pages, from sitemap + const [securityTxtResults, updateSecurityTxtResults] = useMotherHook({ + jobId: 'security-txt', + updateLoadingJobs, + addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, + fetchRequest: () => fetch(`${api}/security-txt?url=${address}`).then(res => parseJson(res)), + }); + + // Get the DNS server(s) for a domain, and test DoH/DoT support + const [dnsServerResults, updateDnsServerResults] = useMotherHook({ + jobId: 'dns-server', + updateLoadingJobs, + addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, + fetchRequest: () => fetch(`${api}/dns-server?url=${address}`).then(res => parseJson(res)), + }); + + // Get the WAF and Firewall info for a site + const [firewallResults, updateFirewallResults] = useMotherHook({ + jobId: 'firewall', + updateLoadingJobs, + addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, + fetchRequest: () => fetch(`${api}/firewall?url=${address}`).then(res => parseJson(res)), + }); + + // Get DNSSEC info + const [dnsSecResults, updateDnsSecResults] = useMotherHook({ + jobId: 'dnssec', + updateLoadingJobs, + addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, + fetchRequest: () => fetch(`${api}/dnssec?url=${address}`).then(res => parseJson(res)), + }); + + // Check if a site is on the HSTS preload list + const [hstsResults, updateHstsResults] = useMotherHook({ + jobId: 'hsts', + updateLoadingJobs, + addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, + fetchRequest: () => fetch(`${api}/hsts?url=${address}`).then(res => parseJson(res)), + }); + + // Check if a host is present on the URLHaus malware list + const [threatResults, updateThreatResults] = useMotherHook({ + jobId: 'threats', + updateLoadingJobs, + addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, + fetchRequest: () => fetch(`${api}/threats?url=${address}`).then(res => parseJson(res)), + }); + + // Get mail config for server, based on DNS records + const [mailConfigResults, updateMailConfigResults] = useMotherHook({ + jobId: 'mail-config', + updateLoadingJobs, + addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, + fetchRequest: () => fetch(`${api}/mail-config?url=${address}`).then(res => parseJson(res)), + }); + + // Get list of archives from the Wayback Machine + const [archivesResults, updateArchivesResults] = useMotherHook({ + jobId: 'archives', + updateLoadingJobs, + addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, + fetchRequest: () => fetch(`${api}/archives?url=${address}`).then(res => parseJson(res)), + }); + + // Get website's global ranking, from Tranco + const [rankResults, updateRankResults] = useMotherHook({ + jobId: 'rank', + updateLoadingJobs, + addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, + fetchRequest: () => fetch(`${api}/rank?url=${address}`).then(res => parseJson(res)), + }); + + // Take a screenshot of the website + const [screenshotResult, updateScreenshotResult] = useMotherHook({ + jobId: 'screenshot', + updateLoadingJobs, + addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, + fetchRequest: () => fetch(`${api}/screenshot?url=${address}`).then(res => parseJson(res)), + }); + + // Get TLS security info, from Mozilla Observatory + const [tlsResults, updateTlsResults] = useMotherHook({ + jobId: ['tls-cipher-suites', 'tls-security-config', 'tls-client-support'], + updateLoadingJobs, + addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, + fetchRequest: () => fetch(`${api}/tls?url=${address}`).then(res => parseJson(res)), + }); + + // Fetches URL redirects + const [redirectResults, updateRedirectResults] = useMotherHook({ + jobId: 'redirects', + updateLoadingJobs, + addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, + fetchRequest: () => fetch(`${api}/redirects?url=${address}`).then(res => parseJson(res)), + }); + + // Get list of links included in the page content + const [linkedPagesResults, updateLinkedPagesResults] = useMotherHook({ + jobId: 'linked-pages', + updateLoadingJobs, + addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, + fetchRequest: () => fetch(`${api}/linked-pages?url=${address}`).then(res => parseJson(res)), + }); + + // Fetch and parse crawl rules from robots.txt + const [robotsTxtResults, updateRobotsTxtResults] = useMotherHook<{robots: RowProps[]}>({ + jobId: 'robots-txt', + updateLoadingJobs, + addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, + fetchRequest: () => fetch(`${api}/robots-txt?url=${address}`) + .then(res => parseJson(res)), + }); + + // Get current status and response time of server + const [serverStatusResults, updateServerStatusResults] = useMotherHook({ + jobId: 'status', + updateLoadingJobs, + addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, + fetchRequest: () => fetch(`${api}/status?url=${address}`).then(res => parseJson(res)), }); // Check for open ports @@ -345,52 +506,12 @@ const Results = (): JSX.Element => { fetchRequest: () => fetch(`${api}/txt-records?url=${address}`).then(res => parseJson(res)), }); - // Fetches URL redirects - const [redirectResults, updateRedirectResults] = useMotherHook({ - jobId: 'redirects', + // Check site against DNS blocklists + const [blockListsResults, updateBlockListsResults] = useMotherHook({ + jobId: 'block-lists', updateLoadingJobs, addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, - fetchRequest: () => fetch(`${api}/redirects?url=${address}`).then(res => parseJson(res)), - }); - - // Get current status and response time of server - const [serverStatusResults, updateServerStatusResults] = useMotherHook({ - jobId: 'status', - updateLoadingJobs, - addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, - fetchRequest: () => fetch(`${api}/status?url=${address}`).then(res => parseJson(res)), - }); - - // Get current status and response time of server - const [techStackResults, updateTechStackResults] = useMotherHook({ - jobId: 'tech-stack', - updateLoadingJobs, - addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, - fetchRequest: () => fetch(`${api}/tech-stack?url=${address}`).then(res => parseJson(res)), - }); - - // Get trace route for a given hostname - const [traceRouteResults, updateTraceRouteResults] = useMotherHook({ - jobId: 'trace-route', - updateLoadingJobs, - addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, - fetchRequest: () => fetch(`${api}/trace-route?url=${address}`).then(res => parseJson(res)), - }); - - // Fetch carbon footprint data for a given site - const [carbonResults, updateCarbonResults] = useMotherHook({ - jobId: 'carbon', - updateLoadingJobs, - addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, - fetchRequest: () => fetch(`${api}/carbon?url=${address}`).then(res => parseJson(res)), - }); - - // Check if a site is on the HSTS preload list - const [hstsResults, updateHstsResults] = useMotherHook({ - jobId: 'hsts', - updateLoadingJobs, - addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, - fetchRequest: () => fetch(`${api}/hsts?url=${address}`).then(res => parseJson(res)), + fetchRequest: () => fetch(`${api}/block-lists?url=${address}`).then(res => parseJson(res)), }); // Get a websites listed pages, from sitemap @@ -401,28 +522,12 @@ const Results = (): JSX.Element => { fetchRequest: () => fetch(`${api}/sitemap?url=${address}`).then(res => parseJson(res)), }); - // Get a websites listed pages, from sitemap - const [screenshotResult, updateScreenshotResult] = useMotherHook({ - jobId: 'screenshot', + // Fetch carbon footprint data for a given site + const [carbonResults, updateCarbonResults] = useMotherHook({ + jobId: 'carbon', updateLoadingJobs, addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, - fetchRequest: () => fetch(`${api}/screenshot?url=${address}`).then(res => parseJson(res)), - }); - - // Get a websites listed pages, from sitemap - const [securityTxtResults, updateSecurityTxtResults] = useMotherHook({ - jobId: 'security-txt', - updateLoadingJobs, - addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, - fetchRequest: () => fetch(`${api}/security-txt?url=${address}`).then(res => parseJson(res)), - }); - - // Get social media previews, from a sites social meta tags - const [socialTagResults, updateSocialTagResults] = useMotherHook({ - jobId: 'social-tags', - updateLoadingJobs, - addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, - fetchRequest: () => fetch(`${api}/social-tags?url=${address}`).then(res => parseJson(res)), + fetchRequest: () => fetch(`${api}/carbon?url=${address}`).then(res => parseJson(res)), }); // Get site features from BuiltWith @@ -440,102 +545,6 @@ const Results = (): JSX.Element => { }), }); - // Get DNSSEC info - const [dnsSecResults, updateDnsSecResults] = useMotherHook({ - jobId: 'dnssec', - updateLoadingJobs, - addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, - fetchRequest: () => fetch(`${api}/dnssec?url=${address}`).then(res => parseJson(res)), - }); - - // Run a manual whois lookup on the domain - const [domainLookupResults, updateDomainLookupResults] = useMotherHook({ - jobId: 'domain', - updateLoadingJobs, - addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, - fetchRequest: () => fetch(`${api}/whois?url=${address}`).then(res => parseJson(res)), - }); - - // Get the DNS server(s) for a domain, and test DoH/DoT support - const [dnsServerResults, updateDnsServerResults] = useMotherHook({ - jobId: 'dns-server', - updateLoadingJobs, - addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, - fetchRequest: () => fetch(`${api}/dns-server?url=${address}`).then(res => parseJson(res)), - }); - - // Get list of links included in the page content - const [linkedPagesResults, updateLinkedPagesResults] = useMotherHook({ - jobId: 'linked-pages', - updateLoadingJobs, - addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, - fetchRequest: () => fetch(`${api}/linked-pages?url=${address}`).then(res => parseJson(res)), - }); - - // Get mail config for server, based on DNS records - const [mailConfigResults, updateMailConfigResults] = useMotherHook({ - jobId: 'mail-config', - updateLoadingJobs, - addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, - fetchRequest: () => fetch(`${api}/mail-config?url=${address}`).then(res => parseJson(res)), - }); - - // Get the WAF and Firewall info for a site - const [firewallResults, updateFirewallResults] = useMotherHook({ - jobId: 'firewall', - updateLoadingJobs, - addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, - fetchRequest: () => fetch(`${api}/firewall?url=${address}`).then(res => parseJson(res)), - }); - - // Get the WAF and Firewall info for a site - const [httpSecurityResults, updateHttpSecurityResults] = useMotherHook({ - jobId: 'firewall', - updateLoadingJobs, - addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, - fetchRequest: () => fetch(`${api}/http-security?url=${address}`).then(res => parseJson(res)), - }); - - // Get list of archives from the Wayback Machine - const [archivesResults, updateArchivesResults] = useMotherHook({ - jobId: 'archives', - updateLoadingJobs, - addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, - fetchRequest: () => fetch(`${api}/archives?url=${address}`).then(res => parseJson(res)), - }); - - // Get website's global ranking, from Tranco - const [rankResults, updateRankResults] = useMotherHook({ - jobId: 'rank', - updateLoadingJobs, - addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, - fetchRequest: () => fetch(`${api}/rank?url=${address}`).then(res => parseJson(res)), - }); - - // Check site against DNS blocklists - const [blockListsResults, updateBlockListsResults] = useMotherHook({ - jobId: 'block-lists', - updateLoadingJobs, - addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, - fetchRequest: () => fetch(`${api}/block-lists?url=${address}`).then(res => parseJson(res)), - }); - - // Check if a host is present on the URLHaus malware list - const [threatResults, updateThreatResults] = useMotherHook({ - jobId: 'threats', - updateLoadingJobs, - addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, - fetchRequest: () => fetch(`${api}/threats?url=${address}`).then(res => parseJson(res)), - }); - - // Get TLS security info, from Mozilla Observatory - const [tlsResults, updateTlsResults] = useMotherHook({ - jobId: ['tls-cipher-suites', 'tls-security-config', 'tls-client-support'], - updateLoadingJobs, - addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly }, - fetchRequest: () => fetch(`${api}/tls?url=${address}`).then(res => parseJson(res)), - }); - /* Cancel remaining jobs after 10 second timeout */ useEffect(() => { const checkJobs = () => { @@ -560,267 +569,268 @@ const Results = (): JSX.Element => { } // A list of state sata, corresponding component and title for each card - const resultCardData = [{ - id: 'location', - title: 'Server Location', - result: locationResults, - Component: ServerLocationCard, - refresh: updateLocationResults, - tags: ['server'], - }, { - id: 'ssl', - title: 'SSL Certificate', - result: sslResults, - Component: SslCertCard, - refresh: updateSslResults, - tags: ['server', 'security'], - }, { - id: 'domain', - title: 'Domain Whois', - result: domainLookupResults, - Component: DomainLookup, - refresh: updateDomainLookupResults, - tags: ['server'], - }, { - id: 'quality', - title: 'Quality Summary', - result: lighthouseResults, - Component: LighthouseCard, - refresh: updateLighthouseResults, - tags: ['client'], - }, { - id: 'tech-stack', - title: 'Tech Stack', - result: techStackResults, - Component: TechStackCard, - refresh: updateTechStackResults, - tags: ['client', 'meta'], - }, { - id: 'server-info', - title: 'Server Info', - result: shoadnResults?.serverInfo, - Component: ServerInfoCard, - refresh: updateShodanResults, - tags: ['server'], - }, { - id: 'cookies', - title: 'Cookies', - result: cookieResults, - Component: CookiesCard, - refresh: updateCookieResults, - tags: ['client', 'security'], - }, { - id: 'headers', - title: 'Headers', - result: headersResults, - Component: HeadersCard, - refresh: updateHeadersResults, - tags: ['client', 'security'], - }, { - id: 'dns', - title: 'DNS Records', - result: dnsResults, - Component: DnsRecordsCard, - refresh: updateDnsResults, - tags: ['server'], - }, { - id: 'hosts', - title: 'Host Names', - result: shoadnResults?.hostnames, - Component: HostNamesCard, - refresh: updateShodanResults, - tags: ['server'], - }, { - id: 'http-security', - title: 'HTTP Security', - result: httpSecurityResults, - Component: HttpSecurityCard, - refresh: updateHttpSecurityResults, - tags: ['security'], - }, { - id: 'social-tags', - title: 'Social Tags', - result: socialTagResults, - Component: SocialTagsCard, - refresh: updateSocialTagResults, - tags: ['client', 'meta'], - }, { - id: 'trace-route', - title: 'Trace Route', - result: traceRouteResults, - Component: TraceRouteCard, - refresh: updateTraceRouteResults, - tags: ['server'], - }, { - id: 'security-txt', - title: 'Security.Txt', - result: securityTxtResults, - Component: SecurityTxtCard, - refresh: updateSecurityTxtResults, - tags: ['security'], - }, { - id: 'dns-server', - title: 'DNS Server', - result: dnsServerResults, - Component: DnsServerCard, - refresh: updateDnsServerResults, - tags: ['server'], - }, { - id: 'firewall', - title: 'Firewall', - result: firewallResults, - Component: FirewallCard, - refresh: updateFirewallResults, - tags: ['server', 'security'], - }, { - id: 'dnssec', - title: 'DNSSEC', - result: dnsSecResults, - Component: DnsSecCard, - refresh: updateDnsSecResults, - tags: ['security'], - }, { - id: 'hsts', - title: 'HSTS Check', - result: hstsResults, - Component: HstsCard, - refresh: updateHstsResults, - tags: ['security'], - }, { - id: 'threats', - title: 'Threats', - result: threatResults, - Component: ThreatsCard, - refresh: updateThreatResults, - tags: ['security'], - }, { - id: 'mail-config', - title: 'Email Configuration', - result: mailConfigResults, - Component: MailConfigCard, - refresh: updateMailConfigResults, - tags: ['server'], - }, { - id: 'archives', - title: 'Archive History', - result: archivesResults, - Component: ArchivesCard, - refresh: updateArchivesResults, - tags: ['meta'], - }, { - id: 'rank', - title: 'Global Ranking', - result: rankResults, - Component: RankCard, - refresh: updateRankResults, - tags: ['meta'], - }, { - id: 'screenshot', - title: 'Screenshot', - result: screenshotResult || lighthouseResults?.fullPageScreenshot?.screenshot, - Component: ScreenshotCard, - refresh: updateScreenshotResult, - tags: ['client', 'meta'], - }, { - id: 'tls-cipher-suites', - title: 'TLS Cipher Suites', - result: tlsResults, - Component: TlsCipherSuitesCard, - refresh: updateTlsResults, - tags: ['server', 'security'], - }, { - id: 'tls-security-config', - title: 'TLS Security Issues', - result: tlsResults, - Component: TlsIssueAnalysisCard, - refresh: updateTlsResults, - tags: ['security'], - }, { - id: 'tls-client-support', - title: 'TLS Handshake Simulation', - result: tlsResults, - Component: TlsClientSupportCard, - refresh: updateTlsResults, - tags: ['security'], - }, { - id: 'redirects', - title: 'Redirects', - result: redirectResults, - Component: RedirectsCard, - refresh: updateRedirectResults, - tags: ['meta'], - }, { - id: 'linked-pages', - title: 'Linked Pages', - result: linkedPagesResults, - Component: ContentLinksCard, - refresh: updateLinkedPagesResults, - tags: ['client', 'meta'], - }, { - id: 'robots-txt', - title: 'Crawl Rules', - result: robotsTxtResults, - Component: RobotsTxtCard, - refresh: updateRobotsTxtResults, - tags: ['meta'], - }, { - id: 'status', - title: 'Server Status', - result: serverStatusResults, - Component: ServerStatusCard, - refresh: updateServerStatusResults, - tags: ['server'], - }, { - id: 'ports', - title: 'Open Ports', - result: portsResults, - Component: OpenPortsCard, - refresh: updatePortsResults, - tags: ['server'], - }, { - id: 'whois', - title: 'Domain Info', - result: whoIsResults, - Component: WhoIsCard, - refresh: updateWhoIsResults, - tags: ['server'], - }, { - id: 'txt-records', - title: 'TXT Records', - result: txtRecordResults, - Component: TxtRecordCard, - refresh: updateTxtRecordResults, - tags: ['server'], - }, { - id: 'block-lists', - title: 'Block Lists', - result: blockListsResults, - Component: BlockListsCard, - refresh: updateBlockListsResults, - tags: ['security', 'meta'], - }, { - id: 'features', - title: 'Site Features', - result: siteFeaturesResults, - Component: SiteFeaturesCard, - refresh: updateSiteFeaturesResults, - tags: ['meta'], - }, { - id: 'sitemap', - title: 'Pages', - result: sitemapResults, - Component: SitemapCard, - refresh: updateSitemapResults, - tags: ['meta'], - }, { - id: 'carbon', - title: 'Carbon Footprint', - result: carbonResults, - Component: CarbonFootprintCard, - refresh: updateCarbonResults, - tags: ['meta'], - }, -]; + const resultCardData = [ + { + id: 'location', + title: 'Server Location', + result: locationResults, + Component: ServerLocationCard, + refresh: updateLocationResults, + tags: ['server'], + }, { + id: 'ssl', + title: 'SSL Certificate', + result: sslResults, + Component: SslCertCard, + refresh: updateSslResults, + tags: ['server', 'security'], + }, { + id: 'domain', + title: 'Domain Whois', + result: domainLookupResults, + Component: DomainLookup, + refresh: updateDomainLookupResults, + tags: ['server'], + }, { + id: 'quality', + title: 'Quality Summary', + result: lighthouseResults, + Component: LighthouseCard, + refresh: updateLighthouseResults, + tags: ['client'], + }, { + id: 'tech-stack', + title: 'Tech Stack', + result: techStackResults, + Component: TechStackCard, + refresh: updateTechStackResults, + tags: ['client', 'meta'], + }, { + id: 'server-info', + title: 'Server Info', + result: shoadnResults?.serverInfo, + Component: ServerInfoCard, + refresh: updateShodanResults, + tags: ['server'], + }, { + id: 'cookies', + title: 'Cookies', + result: cookieResults, + Component: CookiesCard, + refresh: updateCookieResults, + tags: ['client', 'security'], + }, { + id: 'headers', + title: 'Headers', + result: headersResults, + Component: HeadersCard, + refresh: updateHeadersResults, + tags: ['client', 'security'], + }, { + id: 'dns', + title: 'DNS Records', + result: dnsResults, + Component: DnsRecordsCard, + refresh: updateDnsResults, + tags: ['server'], + }, { + id: 'hosts', + title: 'Host Names', + result: shoadnResults?.hostnames, + Component: HostNamesCard, + refresh: updateShodanResults, + tags: ['server'], + }, { + id: 'http-security', + title: 'HTTP Security', + result: httpSecurityResults, + Component: HttpSecurityCard, + refresh: updateHttpSecurityResults, + tags: ['security'], + }, { + id: 'social-tags', + title: 'Social Tags', + result: socialTagResults, + Component: SocialTagsCard, + refresh: updateSocialTagResults, + tags: ['client', 'meta'], + }, { + id: 'trace-route', + title: 'Trace Route', + result: traceRouteResults, + Component: TraceRouteCard, + refresh: updateTraceRouteResults, + tags: ['server'], + }, { + id: 'security-txt', + title: 'Security.Txt', + result: securityTxtResults, + Component: SecurityTxtCard, + refresh: updateSecurityTxtResults, + tags: ['security'], + }, { + id: 'dns-server', + title: 'DNS Server', + result: dnsServerResults, + Component: DnsServerCard, + refresh: updateDnsServerResults, + tags: ['server'], + }, { + id: 'firewall', + title: 'Firewall', + result: firewallResults, + Component: FirewallCard, + refresh: updateFirewallResults, + tags: ['server', 'security'], + }, { + id: 'dnssec', + title: 'DNSSEC', + result: dnsSecResults, + Component: DnsSecCard, + refresh: updateDnsSecResults, + tags: ['security'], + }, { + id: 'hsts', + title: 'HSTS Check', + result: hstsResults, + Component: HstsCard, + refresh: updateHstsResults, + tags: ['security'], + }, { + id: 'threats', + title: 'Threats', + result: threatResults, + Component: ThreatsCard, + refresh: updateThreatResults, + tags: ['security'], + }, { + id: 'mail-config', + title: 'Email Configuration', + result: mailConfigResults, + Component: MailConfigCard, + refresh: updateMailConfigResults, + tags: ['server'], + }, { + id: 'archives', + title: 'Archive History', + result: archivesResults, + Component: ArchivesCard, + refresh: updateArchivesResults, + tags: ['meta'], + }, { + id: 'rank', + title: 'Global Ranking', + result: rankResults, + Component: RankCard, + refresh: updateRankResults, + tags: ['meta'], + }, { + id: 'screenshot', + title: 'Screenshot', + result: screenshotResult || lighthouseResults?.fullPageScreenshot?.screenshot, + Component: ScreenshotCard, + refresh: updateScreenshotResult, + tags: ['client', 'meta'], + }, { + id: 'tls-cipher-suites', + title: 'TLS Cipher Suites', + result: tlsResults, + Component: TlsCipherSuitesCard, + refresh: updateTlsResults, + tags: ['server', 'security'], + }, { + id: 'tls-security-config', + title: 'TLS Security Issues', + result: tlsResults, + Component: TlsIssueAnalysisCard, + refresh: updateTlsResults, + tags: ['security'], + }, { + id: 'tls-client-support', + title: 'TLS Handshake Simulation', + result: tlsResults, + Component: TlsClientSupportCard, + refresh: updateTlsResults, + tags: ['security'], + }, { + id: 'redirects', + title: 'Redirects', + result: redirectResults, + Component: RedirectsCard, + refresh: updateRedirectResults, + tags: ['meta'], + }, { + id: 'linked-pages', + title: 'Linked Pages', + result: linkedPagesResults, + Component: ContentLinksCard, + refresh: updateLinkedPagesResults, + tags: ['client', 'meta'], + }, { + id: 'robots-txt', + title: 'Crawl Rules', + result: robotsTxtResults, + Component: RobotsTxtCard, + refresh: updateRobotsTxtResults, + tags: ['meta'], + }, { + id: 'status', + title: 'Server Status', + result: serverStatusResults, + Component: ServerStatusCard, + refresh: updateServerStatusResults, + tags: ['server'], + }, { + id: 'ports', + title: 'Open Ports', + result: portsResults, + Component: OpenPortsCard, + refresh: updatePortsResults, + tags: ['server'], + }, { + id: 'whois', + title: 'Domain Info', + result: whoIsResults, + Component: WhoIsCard, + refresh: updateWhoIsResults, + tags: ['server'], + }, { + id: 'txt-records', + title: 'TXT Records', + result: txtRecordResults, + Component: TxtRecordCard, + refresh: updateTxtRecordResults, + tags: ['server'], + }, { + id: 'block-lists', + title: 'Block Lists', + result: blockListsResults, + Component: BlockListsCard, + refresh: updateBlockListsResults, + tags: ['security', 'meta'], + }, { + id: 'features', + title: 'Site Features', + result: siteFeaturesResults, + Component: SiteFeaturesCard, + refresh: updateSiteFeaturesResults, + tags: ['meta'], + }, { + id: 'sitemap', + title: 'Pages', + result: sitemapResults, + Component: SitemapCard, + refresh: updateSitemapResults, + tags: ['meta'], + }, { + id: 'carbon', + title: 'Carbon Footprint', + result: carbonResults, + Component: CarbonFootprintCard, + refresh: updateCarbonResults, + tags: ['meta'], + }, + ]; const makeActionButtons = (title: string, refresh: () => void, showInfo: (id: string) => void): ReactNode => { const actions = [ @@ -869,16 +879,23 @@ const Results = (): JSX.Element => { {(tags.length > 0 || searchTerm.length > 0) && <span onClick={clearFilters} className="clear">Clear Filters</span> } </div> <div className="one-half"> + <span className="group-label">Search</span> <input type="text" placeholder="Filter Results" value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} /> - <span className="group-label">Search</span> + <span className="toggle-filters" onClick={() => setShowFilters(false)}>Hide</span> </div> </> : ( - <span className="toggle-filters" onClick={() => setShowFilters(true)}>Show Filters</span> + <div className="control-options"> + <span className="toggle-filters" onClick={() => setShowFilters(true)}>Show Filters</span> + <a href="#view-download-raw-data"><span className="toggle-filters">Export Data</span></a> + <a href="/about"><span className="toggle-filters">Learn about the Results</span></a> + <a href="/about#additional-resources"><span className="toggle-filters">More tools</span></a> + <a href="https://github.com/lissy93/web-check"><span className="toggle-filters">View GitHub</span></a> + </div> ) } </FilterButtons> <ResultsContent> From 0194ada819103266e14572a2cc0469c41bc7016c Mon Sep 17 00:00:00 2001 From: Alicia Sykes <alicia@omg.lol> Date: Sat, 26 Aug 2023 13:10:09 +0100 Subject: [PATCH 15/17] Update About page and docs --- src/pages/About.tsx | 13 +++++++-- src/utils/docs.ts | 64 +++++++++++++++++++++++++++++---------------- 2 files changed, 52 insertions(+), 25 deletions(-) diff --git a/src/pages/About.tsx b/src/pages/About.tsx index da002d4..7e85b6d 100644 --- a/src/pages/About.tsx +++ b/src/pages/About.tsx @@ -7,7 +7,7 @@ import Nav from 'components/Form/Nav'; import Button from 'components/Form/Button'; import AdditionalResources from 'components/misc/AdditionalResources'; import { StyledCard } from 'components/Form/Card'; -import docs, { about, license, fairUse, supportUs } from 'utils/docs'; +import docs, { about, featureIntro, license, fairUse, supportUs } from 'utils/docs'; const AboutContainer = styled.div` width: 95vw; @@ -120,10 +120,20 @@ const About = (): JSX.Element => { {about.map((para, index: number) => ( <p key={index}>{para}</p> ))} + <hr /> + <p> + Web-Check is developed and maintained by <a href="https://aliciasykes.com">Alicia Sykes</a>. + It's licensed under the <a href="https://github.com/Lissy93/web-check/blob/master/LICENSE">MIT license</a>, + and is completely free to use, modify and distribute in both personal and commercial settings.<br /> + Source code and self-hosting docs are available on <a href="https://github.com/lissy93/web-check">GitHub</a>. + If you've found this service useful, consider <a href="https://github.com/sponsors/Lissy93">sponsoring me</a> from $1/month, + to help with the ongoing hosting and development costs. + </p> </Section> <Heading as="h2" size="medium" color={colors.primary}>Features</Heading> <Section> + {featureIntro.map((fi: string, i: number) => (<p key={i}>{fi}</p>))} <div className="contents"> <Heading as="h3" size="small" id="#feature-contents" color={colors.primary}>Contents</Heading> <ul> @@ -165,7 +175,6 @@ const About = (): JSX.Element => { ))} </ul> </>} - {/* { sectionIndex < docs.length - 1 && <hr /> } */} </section> ))} </Section> diff --git a/src/utils/docs.ts b/src/utils/docs.ts index 0b2a753..517ec0f 100644 --- a/src/utils/docs.ts +++ b/src/utils/docs.ts @@ -468,26 +468,48 @@ const docs: Doc[] = [ description: 'Checks access to the URL using 10+ of the most popular privacy, malware and parental control blocking DNS servers.', use: '', resources: [], - screenshot: '', + screenshot: 'https://i.ibb.co/M5JSXbW/Screenshot-from-2023-08-26-12-12-43.png', }, { - id: 'malware', + id: 'threats', title: 'Malware & Phishing Detection', - description: '', - use: '', + description: 'Checks if a site appears in several common malware and phishing lists, to determine it\'s threat level.', + use: 'Knowing if a site is listed as a threat by any of these services can be useful for understanding the reputation of a site, and for identifying potential trends.', resources: [ { title: 'URLHaus', link: 'https://urlhaus-api.abuse.ch/'}, { title: 'PhishTank', link: 'https://www.phishtank.com/'}, ], - screenshot: '', + screenshot: 'https://i.ibb.co/hYgy621/Screenshot-from-2023-08-26-12-07-47.png', }, { - id: 'tls', - title: 'TLS Configuration', - description: '', - use: '', + id: 'tls-cipher-suites', + title: 'TLS Cipher Suites', + description: 'These are combinations of cryptographic algorithms used by the server to establish a secure connection. It includes the key exchange algorithm, bulk encryption algorithm, MAC algorithm, and PRF (pseudorandom function).', + use: 'This is important info to test for from a security perspective. Because a cipher suite is only as secure as the algorithms that it contains. If the version of encryption or authentication algorithm in a cipher suite have known vulnerabilities the cipher suite and TLS connection may then vulnerable to a downgrade or other attack', + resources: [ + { title: 'sslscan2 CLI', link: 'https://github.com/rbsec/sslscan' }, + { title: 'ssl-enum-ciphers (NPMAP script)', link: 'https://nmap.org/nsedoc/scripts/ssl-enum-ciphers.html' } + ], + screenshot: 'https://i.ibb.co/6ydtH5R/Screenshot-from-2023-08-26-12-09-58.png', + }, + { + id: 'tls-security-config', + title: 'TLS Security Config', + description: 'This uses guidelines from Mozilla\'s TLS Observatory to check the security of the TLS configuration. It checks for bad configurations, which may leave the site vulnerable to attack, as well as giving advice on how to fix. It will also give suggestions around outdated and modern TLS configs', + use: 'Understanding issues with a site\'s TLS configuration will help you address potential vulnerabilities, and ensure the site is using the latest and most secure TLS configuration.', resources: [], - screenshot: '', + screenshot: 'https://i.ibb.co/FmksZJt/Screenshot-from-2023-08-26-12-12-09.png', + }, + { + id: 'tls-client-support', + title: 'TLS Handshake Simulation', + description: 'This simulates how different clients (browsers, operating systems) would perform a TLS handshake with the server. It helps identify compatibility issues and insecure configurations.', + use: '', + resources: [ + { title: 'TLS Handshakes (via Cloudflare Learning)', link: 'https://www.cloudflare.com/learning/ssl/what-happens-in-a-tls-handshake/' }, + { title: 'SSL Test (via SSL Labs)', link: 'https://www.ssllabs.com/ssltest/' }, + ], + screenshot: 'https://i.ibb.co/F7qRZkh/Screenshot-from-2023-08-26-12-11-28.png', }, // { // id: '', @@ -499,29 +521,24 @@ const docs: Doc[] = [ // }, ]; +export const featureIntro = [ + 'When conducting an OSINT investigation on a given website or host, there are several key areas to look at. Each of these are documented below, along with links to the tools and techniques you can use to gather the relevant information.', + 'Web-Check can automate the process of gathering this data, but it will be up to you to interpret the results and draw conclusions.', +]; + export const about = [ `Web-Check is a powerful all-in-one tool for discovering information about a website/host. The core philosophy is simple: feed Web-Check a URL and let it gather, collate, and present a broad array of open data for you to delve into.`, `The report shines a spotlight onto potential attack vectors, existing security measures, -and the intricate web of connections within a site's architecture. +and the web of connections within a site's architecture. The results can also help optimizing server responses, configuring redirects, managing cookies, or fine-tuning DNS records for your site.`, `So, weather you're a developer, system administrator, security researcher, penetration tester or are just interested in discovering the underlying technologies of a given site - I'm sure you'll find this a useful addition to your toolbox.`, - -`It works using a series of lambda functions, each of which makes a crafted fetch -request to the host, processes the returned data, then responds with the results. -The web UI is just a simple React TypeScript app.`, - -`There's a managed instance (hosted on Netlify), which you can use for free -(until my lambda function credits run out!), or you can easily deploy your own -instance locally or remotely. -All the code is open source, so feel free to fork and modify to your liking. -For development and deployment instructions, as well as contributing guidelines, see the GitHub repo. -`]; +]; export const license = `The MIT License (MIT) Copyright (c) Alicia Sykes <alicia@omg.com> @@ -545,7 +562,8 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. `; export const supportUs = [ - "The hosted app is free to use without restriction. All the code is open source, so you're also free to deploy your own instance, or make any modifications.", + "Web-Check is free to use without restriction.", + "All the code is open source, so you're also free to deploy your own instance, as well as fork, modify and distribute the code in both private and commerical settings.", "Running web-check does cost me a small amount of money each month, so if you're finding the app useful, consider <a href='https://github.com/sponsors/Lissy93'>sponsoring me on GitHub</a> if you're able to. Even just $1 or $2/month would be a huge help in supporting the ongoing project running costs.", "Otherwise, there are other ways you can help out, like submitting or reviewing a pull request to the <a href='https://github.com/Lissy93/web-check'>GitHub repo</a>, upvoting us on <a href='https://www.producthunt.com/posts/web-check'>Product Hunt</a>, or by sharing with your network.", "But don't feel obliged to do anything, as this app (and all my other projects) will always remain 100% free and open source, and I will do my best to ensure the managed instances remain up and available for as long as possible :)", From 52cb0fd61877c7f943c478d5b97b2ae3f10b7a10 Mon Sep 17 00:00:00 2001 From: Alicia Sykes <alicia@omg.lol> Date: Sat, 26 Aug 2023 13:33:55 +0100 Subject: [PATCH 16/17] Adds link to ThreatJammer --- src/utils/docs.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/utils/docs.ts b/src/utils/docs.ts index 517ec0f..ec7ce62 100644 --- a/src/utils/docs.ts +++ b/src/utils/docs.ts @@ -467,7 +467,9 @@ const docs: Doc[] = [ title: 'Block Detection', description: 'Checks access to the URL using 10+ of the most popular privacy, malware and parental control blocking DNS servers.', use: '', - resources: [], + resources: [ + { title: 'ThreatJammer Lists', link: 'https://threatjammer.com/osint-lists'}, + ], screenshot: 'https://i.ibb.co/M5JSXbW/Screenshot-from-2023-08-26-12-12-43.png', }, { From a779057762b85d80fa8e737a404aa641e47b7438 Mon Sep 17 00:00:00 2001 From: Alicia Sykes <alicia@omg.lol> Date: Sat, 26 Aug 2023 13:56:22 +0100 Subject: [PATCH 17/17] Reorder docs --- src/utils/docs.ts | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/src/utils/docs.ts b/src/utils/docs.ts index ec7ce62..12c87a6 100644 --- a/src/utils/docs.ts +++ b/src/utils/docs.ts @@ -296,14 +296,6 @@ const docs: Doc[] = [ ], screenshot: 'https://i.ibb.co/k253fq4/Screenshot-from-2023-07-17-20-10-52.png', }, - { - id: 'screenshot', - title: 'Screenshot', - description: 'This check takes a screenshot of webpage that the requested URL / IP resolves to, and displays it.', - use: 'This may be useful to see what a given website looks like, free of the constraints of your browser, IP, or location.', - resources: [], - screenshot: 'https://i.ibb.co/2F0x8kP/Screenshot-from-2023-07-29-18-34-48.png', - }, { id: 'dns-server', title: 'DNS Server', @@ -513,14 +505,14 @@ const docs: Doc[] = [ ], screenshot: 'https://i.ibb.co/F7qRZkh/Screenshot-from-2023-08-26-12-11-28.png', }, - // { - // id: '', - // title: '', - // description: '', - // use: '', - // resources: [], - // screenshot: '', - // }, + { + id: 'screenshot', + title: 'Screenshot', + description: 'This check takes a screenshot of webpage that the requested URL / IP resolves to, and displays it.', + use: 'This may be useful to see what a given website looks like, free of the constraints of your browser, IP, or location.', + resources: [], + screenshot: 'https://i.ibb.co/2F0x8kP/Screenshot-from-2023-07-29-18-34-48.png', + }, ]; export const featureIntro = [