mirror of
https://github.com/Lissy93/web-check.git
synced 2025-08-09 21:07:33 +02:00
Merge branch 'master' into master
This commit is contained in:
@ -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<HTMLButtonElement>;
|
||||
@ -10,6 +12,7 @@ interface ButtonProps {
|
||||
fgColor?: string,
|
||||
styles?: string,
|
||||
title?: string,
|
||||
loadState?: LoadState,
|
||||
};
|
||||
|
||||
const StyledButton = styled.button<ButtonProps>`
|
||||
@ -19,6 +22,9 @@ const StyledButton = styled.button<ButtonProps>`
|
||||
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<ButtonProps>`
|
||||
${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 <SimpleLoader />
|
||||
if (props.loadState === 'success') return <span>✔</span>
|
||||
if (props.loadState === 'error') return <span>✗</span>
|
||||
return <span></span>;
|
||||
};
|
||||
|
||||
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 (
|
||||
<StyledButton
|
||||
onClick={onClick || (() => null) }
|
||||
@ -47,6 +74,7 @@ const Button = (props: ButtonProps): JSX.Element => {
|
||||
styles={styles}
|
||||
title={title?.toString()}
|
||||
>
|
||||
{ loadState && <Loader loadState={loadState} /> }
|
||||
{children}
|
||||
</StyledButton>
|
||||
);
|
||||
|
@ -30,7 +30,7 @@ export const Card = (props: CardProps): JSX.Element => {
|
||||
<ErrorBoundary title={heading}>
|
||||
<StyledCard styles={styles}>
|
||||
{ actionButtons && actionButtons }
|
||||
{ heading && <Heading as="h3" align="left" color={colors.primary}>{heading}</Heading> }
|
||||
{ heading && <Heading className="inner-heading" as="h3" align="left" color={colors.primary}>{heading}</Heading> }
|
||||
{children}
|
||||
</StyledCard>
|
||||
</ErrorBoundary>
|
||||
|
@ -10,12 +10,14 @@ interface HeadingProps {
|
||||
inline?: boolean;
|
||||
children: React.ReactNode;
|
||||
id?: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const StyledHeading = styled.h1<HeadingProps>`
|
||||
margin: 0.5rem 0;
|
||||
text-shadow: 2px 2px 0px ${colors.bgShadowColor};
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
font-size: ${TextSizes.medium};
|
||||
@ -26,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) {
|
||||
@ -47,10 +50,14 @@ const StyledHeading = styled.h1<HeadingProps>`
|
||||
${props => props.inline ? 'display: inline;' : '' }
|
||||
`;
|
||||
|
||||
const makeAnchor = (title: string): string => {
|
||||
return title.toLowerCase().replace(/[^\w\s]|_/g, "").replace(/\s+/g, "-");
|
||||
};
|
||||
|
||||
const Heading = (props: HeadingProps): JSX.Element => {
|
||||
const { children, as, size, align, color, inline, id } = props;
|
||||
const { children, as, size, align, color, inline, id, className } = props;
|
||||
return (
|
||||
<StyledHeading as={as} size={size} align={align} color={color} inline={inline} id={id}>
|
||||
<StyledHeading as={as} size={size} align={align} color={color} inline={inline} className={className} id={id || makeAnchor((children || '')?.toString())}>
|
||||
{children}
|
||||
</StyledHeading>
|
||||
);
|
||||
|
@ -57,13 +57,13 @@ const Modal: React.FC<ModalProps> = ({ children, isOpen, closeModal }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleEscPress = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
closeModal();
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
const handleEscPress = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
closeModal();
|
||||
}
|
||||
};
|
||||
|
||||
if (isOpen) {
|
||||
window.addEventListener('keydown', handleEscPress);
|
||||
}
|
||||
@ -71,7 +71,7 @@ const Modal: React.FC<ModalProps> = ({ children, isOpen, closeModal }) => {
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleEscPress);
|
||||
};
|
||||
}, [isOpen]);
|
||||
}, [isOpen, closeModal]);
|
||||
|
||||
if (!isOpen) {
|
||||
return null;
|
||||
|
@ -6,16 +6,19 @@ import Heading from 'components/Form/Heading';
|
||||
export interface RowProps {
|
||||
lbl: string,
|
||||
val: string,
|
||||
// key?: string,
|
||||
key?: string | number,
|
||||
children?: ReactNode,
|
||||
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) => {
|
||||
<span className="val" title={row.val} onClick={() => copyToClipboard(row.val)}>
|
||||
{formatValue(row.val)}
|
||||
</span>
|
||||
{ row.plaintext && <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>
|
||||
);
|
||||
};
|
||||
|
37
src/components/Results/Archives.tsx
Normal file
37
src/components/Results/Archives.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
import styled from 'styled-components';
|
||||
import colors from 'styles/colors';
|
||||
import { Card } from 'components/Form/Card';
|
||||
import Row from 'components/Form/Row';
|
||||
|
||||
const Note = styled.small`
|
||||
opacity: 0.5;
|
||||
display: block;
|
||||
margin-top: 0.5rem;
|
||||
a {
|
||||
color: ${colors.primary};
|
||||
}
|
||||
`;
|
||||
|
||||
const ArchivesCard = (props: { data: any, title: string, actionButtons: any }): JSX.Element => {
|
||||
const data = props.data;
|
||||
return (
|
||||
<Card heading={props.title} actionButtons={props.actionButtons}>
|
||||
<Row lbl="First Scan" val={data.firstScan} />
|
||||
<Row lbl="Last Scan" val={data.lastScan} />
|
||||
<Row lbl="Total Scans" val={data.totalScans} />
|
||||
<Row lbl="Change Count" val={data.changeCount} />
|
||||
<Row lbl="Avg Size" val={`${data.averagePageSize} bytes`} />
|
||||
{ data.scanFrequency?.scansPerDay > 1 ?
|
||||
<Row lbl="Avg Scans Per Day" val={data.scanFrequency.scansPerDay} /> :
|
||||
<Row lbl="Avg Days between Scans" val={data.scanFrequency.daysBetweenScans} />
|
||||
}
|
||||
|
||||
<Note>
|
||||
View historical versions of this page <a rel="noreferrer" target="_blank" href={`https://web.archive.org/web/*/${data.scanUrl}`}>here</a>,
|
||||
via the Internet Archive's Wayback Machine.
|
||||
</Note>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default ArchivesCard;
|
19
src/components/Results/BlockLists.tsx
Normal file
19
src/components/Results/BlockLists.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
|
||||
import { Card } from 'components/Form/Card';
|
||||
import Row from 'components/Form/Row';
|
||||
|
||||
const BlockListsCard = (props: {data: any, title: string, actionButtons: any }): JSX.Element => {
|
||||
const blockLists = props.data.blocklists;
|
||||
return (
|
||||
<Card heading={props.title} actionButtons={props.actionButtons}>
|
||||
{ blockLists.map((blocklist: any) => (
|
||||
<Row
|
||||
title={blocklist.serverIp}
|
||||
lbl={blocklist.server}
|
||||
val={blocklist.isBlocked ? '❌ Blocked' : '✅ Not Blocked'} />
|
||||
))}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default BlockListsCard;
|
@ -43,9 +43,8 @@ const getPathName = (link: string) => {
|
||||
};
|
||||
|
||||
const ContentLinksCard = (props: { data: any, title: string, actionButtons: any }): JSX.Element => {
|
||||
const { internal, external} = props.data;
|
||||
console.log('Internal Links', internal);
|
||||
console.log('External Links', external);
|
||||
const internal = props.data.internal || [];
|
||||
const external = props.data.external || [];
|
||||
return (
|
||||
<Card heading={props.title} actionButtons={props.actionButtons} styles={cardStyles}>
|
||||
<Heading as="h3" size="small" color={colors.primary}>Summary</Heading>
|
||||
@ -71,17 +70,6 @@ const ContentLinksCard = (props: { data: any, title: string, actionButtons: any
|
||||
))}
|
||||
</details>
|
||||
)}
|
||||
{/* {portData.openPorts.map((port: any) => (
|
||||
<Row key={port} lbl="" val="">
|
||||
<span>{port}</span>
|
||||
</Row>
|
||||
)
|
||||
)}
|
||||
<br />
|
||||
<small>
|
||||
Unable to establish connections to:<br />
|
||||
{portData.failedPorts.join(', ')}
|
||||
</small> */}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
@ -1,13 +1,31 @@
|
||||
import { Card } from 'components/Form/Card';
|
||||
import { ExpandableRow } from 'components/Form/Row';
|
||||
import { Cookie } from 'utils/result-processor';
|
||||
|
||||
export const parseHeaderCookies = (cookiesHeader: string[]): Cookie[] => {
|
||||
if (!cookiesHeader || !cookiesHeader.length) return [];
|
||||
const cookies = cookiesHeader.flatMap(cookieHeader => {
|
||||
return cookieHeader.split(/,(?=\s[A-Za-z0-9]+=)/).map(cookieString => {
|
||||
const [nameValuePair, ...attributePairs] = cookieString.split('; ').map(part => part.trim());
|
||||
const [name, value] = nameValuePair.split('=');
|
||||
const attributes: Record<string, string> = {};
|
||||
attributePairs.forEach(pair => {
|
||||
const [attributeName, attributeValue = ''] = pair.split('=');
|
||||
attributes[attributeName] = attributeValue;
|
||||
});
|
||||
return { name, value, attributes };
|
||||
});
|
||||
});
|
||||
return cookies;
|
||||
};
|
||||
|
||||
const CookiesCard = (props: { data: any, title: string, actionButtons: any}): JSX.Element => {
|
||||
const cookies = props.data.cookies;
|
||||
const headerCookies = parseHeaderCookies(props.data.headerCookies) || [];
|
||||
const clientCookies = props.data.clientCookies || [];
|
||||
return (
|
||||
<Card heading={props.title} actionButtons={props.actionButtons}>
|
||||
{ cookies.length === 0 && <p>No cookies found.</p> }
|
||||
{
|
||||
cookies.map((cookie: any, index: number) => {
|
||||
headerCookies.map((cookie: any, index: number) => {
|
||||
const attributes = Object.keys(cookie.attributes).map((key: string) => {
|
||||
return { lbl: key, val: cookie.attributes[key] }
|
||||
});
|
||||
@ -16,6 +34,14 @@ const CookiesCard = (props: { data: any, title: string, actionButtons: any}): JS
|
||||
)
|
||||
})
|
||||
}
|
||||
{
|
||||
clientCookies.map((cookie: any) => {
|
||||
const nameValPairs = Object.keys(cookie).map((key: string) => { return { lbl: key, val: cookie[key] }});
|
||||
return (
|
||||
<ExpandableRow key={`cookie-${cookie.name}`} lbl={cookie.name} val="" rowList={nameValPairs} />
|
||||
);
|
||||
})
|
||||
}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ span.val {
|
||||
`;
|
||||
|
||||
const DomainLookupCard = (props: { data: any, title: string, actionButtons: any }): JSX.Element => {
|
||||
const domain = props.data;
|
||||
const domain = props.data.internicData || {};
|
||||
return (
|
||||
<Card heading={props.title} actionButtons={props.actionButtons} styles={cardStyles}>
|
||||
{ domain.Domain_Name && <Row lbl="Registered Domain" val={domain.Domain_Name} /> }
|
||||
@ -25,13 +25,6 @@ const DomainLookupCard = (props: { data: any, title: string, actionButtons: any
|
||||
<span className="val"><a href={domain.Registrar_URL || '#'}>{domain.Registrar}</a></span>
|
||||
</Row> }
|
||||
{ domain.Registrar_IANA_ID && <Row lbl="Registrar IANA ID" val={domain.Registrar_IANA_ID} /> }
|
||||
|
||||
{/* <Row lbl="" val="">
|
||||
<span className="lbl">Is Up?</span>
|
||||
{ serverStatus.isUp ? <span className="val up">✅ Online</span> : <span className="val down">❌ Offline</span>}
|
||||
</Row>
|
||||
<Row lbl="Status Code" val={serverStatus.responseCode} />
|
||||
{ serverStatus.responseTime && <Row lbl="Response Time" val={`${Math.round(serverStatus.responseTime)}ms`} /> } */}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
24
src/components/Results/Firewall.tsx
Normal file
24
src/components/Results/Firewall.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import styled from 'styled-components';
|
||||
import { Card } from 'components/Form/Card';
|
||||
import Row from 'components/Form/Row';
|
||||
|
||||
const Note = styled.small`
|
||||
opacity: 0.5;
|
||||
display: block;
|
||||
margin-top: 0.5rem;
|
||||
`;
|
||||
|
||||
const FirewallCard = (props: { data: any, title: string, actionButtons: any }): JSX.Element => {
|
||||
const data = props.data;
|
||||
return (
|
||||
<Card heading={props.title} actionButtons={props.actionButtons}>
|
||||
<Row lbl="Firewall" val={data.hasWaf ? '✅ Yes' : '❌ No*' } />
|
||||
{ data.waf && <Row lbl="WAF" val={data.waf} /> }
|
||||
{ !data.hasWaf && (<Note>
|
||||
*The domain may be protected with a proprietary or custom WAF which we were unable to identify automatically
|
||||
</Note>) }
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default FirewallCard;
|
17
src/components/Results/HttpSecurity.tsx
Normal file
17
src/components/Results/HttpSecurity.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import { Card } from 'components/Form/Card';
|
||||
import Row from 'components/Form/Row';
|
||||
|
||||
const HttpSecurityCard = (props: { data: any, title: string, actionButtons: any }): JSX.Element => {
|
||||
const data = props.data;
|
||||
return (
|
||||
<Card heading={props.title} actionButtons={props.actionButtons}>
|
||||
<Row lbl="Content Security Policy" val={data.contentSecurityPolicy ? '✅ Yes' : '❌ No' } />
|
||||
<Row lbl="Strict Transport Policy" val={data.strictTransportPolicy ? '✅ Yes' : '❌ No' } />
|
||||
<Row lbl="X-Content-Type-Options" val={data.xContentTypeOptions ? '✅ Yes' : '❌ No' } />
|
||||
<Row lbl="X-Frame-Options" val={data.xFrameOptions ? '✅ Yes' : '❌ No' } />
|
||||
<Row lbl="X-XSS-Protection" val={data.xXSSProtection ? '✅ Yes' : '❌ No' } />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default HttpSecurityCard;
|
45
src/components/Results/MailConfig.tsx
Normal file
45
src/components/Results/MailConfig.tsx
Normal file
@ -0,0 +1,45 @@
|
||||
|
||||
import { Card } from 'components/Form/Card';
|
||||
import Row from 'components/Form/Row';
|
||||
import Heading from 'components/Form/Heading';
|
||||
import colors from 'styles/colors';
|
||||
|
||||
const cardStyles = ``;
|
||||
|
||||
const MailConfigCard = (props: {data: any, title: string, actionButtons: any }): JSX.Element => {
|
||||
const mailServer = props.data;
|
||||
const txtRecords = (mailServer.txtRecords || []).join('').toLowerCase() || '';
|
||||
return (
|
||||
<Card heading={props.title} actionButtons={props.actionButtons} styles={cardStyles}>
|
||||
<Heading as="h3" color={colors.primary} size="small">Mail Security Checklist</Heading>
|
||||
<Row lbl="SPF" val={txtRecords.includes('spf')} />
|
||||
<Row lbl="DKIM" val={txtRecords.includes('dkim')} />
|
||||
<Row lbl="DMARC" val={txtRecords.includes('dmarc')} />
|
||||
<Row lbl="BIMI" val={txtRecords.includes('bimi')} />
|
||||
|
||||
{ mailServer.mxRecords && <Heading as="h3" color={colors.primary} size="small">MX Records</Heading>}
|
||||
{ mailServer.mxRecords && mailServer.mxRecords.map((record: any, index: number) => (
|
||||
<Row lbl="" val="" key={index}>
|
||||
<span>{record.exchange}</span>
|
||||
<span>{record.priority ? `Priority: ${record.priority}` : ''}</span>
|
||||
</Row>
|
||||
))
|
||||
}
|
||||
{ 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} title={service.value} val="" key={index} />
|
||||
))
|
||||
}
|
||||
|
||||
{ mailServer.txtRecords && <Heading as="h3" color={colors.primary} size="small">Mail-related TXT Records</Heading>}
|
||||
{ mailServer.txtRecords && mailServer.txtRecords.map((record: any, index: number) => (
|
||||
<Row lbl="" val="" key={index}>
|
||||
<span>{record}</span>
|
||||
</Row>
|
||||
))
|
||||
}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default MailConfigCard;
|
77
src/components/Results/Rank.tsx
Normal file
77
src/components/Results/Rank.tsx
Normal file
@ -0,0 +1,77 @@
|
||||
|
||||
import { AreaChart, Area, Tooltip, CartesianGrid, ResponsiveContainer } from 'recharts';
|
||||
import colors from 'styles/colors';
|
||||
import { Card } from 'components/Form/Card';
|
||||
import Row from 'components/Form/Row';
|
||||
|
||||
const cardStyles = `
|
||||
span.val {
|
||||
&.up { color: ${colors.success}; }
|
||||
&.down { color: ${colors.danger}; }
|
||||
}
|
||||
.rank-average {
|
||||
text-align: center;
|
||||
font-size: 1.8rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
.chart-container {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const makeRankStats = (data: {date: string, rank: number }[]) => {
|
||||
const average = Math.round(data.reduce((acc, cur) => acc + cur.rank, 0) / data.length);
|
||||
const today = data[0].rank;
|
||||
const yesterday = data[1].rank;
|
||||
const percentageChange = ((today - yesterday) / yesterday) * 100;
|
||||
return {
|
||||
average,
|
||||
percentageChange
|
||||
};
|
||||
};
|
||||
|
||||
const makeChartData = (data: {date: string, rank: number }[]) => {
|
||||
return data.map((d) => {
|
||||
return {
|
||||
date: d.date,
|
||||
uv: d.rank
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
function Chart(chartData: { date: string; uv: number; }[], data: any) {
|
||||
return <ResponsiveContainer width="100%" height={100}>
|
||||
<AreaChart width={400} height={100} data={chartData}>
|
||||
<defs>
|
||||
<linearGradient id="colorUv" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="20%" stopColor="#0f1620" stopOpacity={0.8} />
|
||||
<stop offset="80%" stopColor="#0f1620" stopOpacity={0} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid strokeDasharray="4" strokeWidth={0.25} verticalPoints={[50, 100, 150, 200, 250, 300, 350]} horizontalPoints={[25, 50, 75]} />
|
||||
<Tooltip contentStyle={{ background: colors.background, color: colors.textColor, borderRadius: 4 }}
|
||||
labelFormatter={(value) => ['Date : ', data[value].date]} />
|
||||
<Area type="monotone" dataKey="uv" stroke="#9fef00" fillOpacity={1} name="Rank" fill={`${colors.backgroundDarker}a1`} />
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>;
|
||||
}
|
||||
|
||||
const RankCard = (props: { data: any, title: string, actionButtons: any }): JSX.Element => {
|
||||
const data = props.data.ranks || [];
|
||||
const { average, percentageChange } = makeRankStats(data);
|
||||
const chartData = makeChartData(data);
|
||||
return (
|
||||
<Card heading={props.title} actionButtons={props.actionButtons} styles={cardStyles}>
|
||||
<div className="rank-average">{data[0].rank.toLocaleString()}</div>
|
||||
<Row lbl="Change since Yesterday" val={`${percentageChange > 0 ? '+':''} ${percentageChange.toFixed(2)}%`} />
|
||||
<Row lbl="Historical Average Rank" val={average.toLocaleString()} />
|
||||
<div className="chart-container">
|
||||
{Chart(chartData, data)}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default RankCard;
|
||||
|
||||
|
@ -42,7 +42,7 @@ const SitemapCard = (props: {data: any, title: string, actionButtons: any }): JS
|
||||
<Card heading={props.title} actionButtons={props.actionButtons} styles={cardStyles}>
|
||||
{
|
||||
normalSiteMap && normalSiteMap.map((subpage: any, index: number) => {
|
||||
return (<ExpandableRow lbl={getPathFromUrl(subpage.loc[0])} val="" rowList={makeExpandableRowData(subpage)}></ExpandableRow>)
|
||||
return (<ExpandableRow lbl={getPathFromUrl(subpage.loc[0])} key={index} val="" rowList={makeExpandableRowData(subpage)}></ExpandableRow>)
|
||||
})
|
||||
}
|
||||
{ siteMapIndex && <p>
|
||||
@ -50,7 +50,7 @@ const SitemapCard = (props: {data: any, title: string, actionButtons: any }): JS
|
||||
</p>}
|
||||
{
|
||||
siteMapIndex && siteMapIndex.map((subpage: any, index: number) => {
|
||||
return (<Row lbl="" val=""><a href={subpage.loc[0]}>{getPathFromUrl(subpage.loc[0])}</a></Row>);
|
||||
return (<Row lbl="" val="" key={index}><a href={subpage.loc[0]}>{getPathFromUrl(subpage.loc[0])}</a></Row>);
|
||||
})
|
||||
}
|
||||
</Card>
|
||||
|
44
src/components/Results/SocialTags.tsx
Normal file
44
src/components/Results/SocialTags.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
|
||||
import { Card } from 'components/Form/Card';
|
||||
import Row from 'components/Form/Row';
|
||||
import colors from 'styles/colors';
|
||||
|
||||
const cardStyles = `
|
||||
.banner-image img {
|
||||
width: 100%;
|
||||
border-radius: 4px;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
.color-field {
|
||||
border-radius: 4px;
|
||||
&:hover {
|
||||
color: ${colors.primary};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const SocialTagsCard = (props: {data: any, title: string, actionButtons: any }): JSX.Element => {
|
||||
const tags = props.data;
|
||||
return (
|
||||
<Card heading={props.title} actionButtons={props.actionButtons} styles={cardStyles}>
|
||||
{ tags.title && <Row lbl="Title" val={tags.title} /> }
|
||||
{ tags.description && <Row lbl="Description" val={tags.description} /> }
|
||||
{ tags.keywords && <Row lbl="Keywords" val={tags.keywords} /> }
|
||||
{ tags.canonicalUrl && <Row lbl="Canonical URL" val={tags.canonicalUrl} /> }
|
||||
{ tags.themeColor && <Row lbl="" val="">
|
||||
<span className="lbl">Theme Color</span>
|
||||
<span className="val color-field" style={{background: tags.themeColor}}>{tags.themeColor}</span>
|
||||
</Row> }
|
||||
{ tags.twitterSite && <Row lbl="" val="">
|
||||
<span className="lbl">Twitter Site</span>
|
||||
<span className="val"><a href={`https://x.com/${tags.twitterSite}`}>{tags.twitterSite}</a></span>
|
||||
</Row> }
|
||||
{ tags.author && <Row lbl="Author" val={tags.author} />}
|
||||
{ tags.publisher && <Row lbl="Publisher" val={tags.publisher} />}
|
||||
{ tags.generator && <Row lbl="Generator" val={tags.generator} />}
|
||||
{ tags.ogImage && <div className="banner-image"><img src={tags.ogImage} alt="Banner" /></div> }
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default SocialTagsCard;
|
@ -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/drivers/webextension/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) => {
|
||||
|
88
src/components/Results/Threats.tsx
Normal file
88
src/components/Results/Threats.tsx
Normal file
@ -0,0 +1,88 @@
|
||||
|
||||
import styled from 'styled-components';
|
||||
import colors from 'styles/colors';
|
||||
import { Card } from 'components/Form/Card';
|
||||
import Row, { ExpandableRow } from 'components/Form/Row';
|
||||
|
||||
const Expandable = styled.details`
|
||||
margin-top: 0.5rem;
|
||||
cursor: pointer;
|
||||
summary::marker {
|
||||
color: ${colors.primary};
|
||||
}
|
||||
`;
|
||||
|
||||
const getExpandableTitle = (urlObj: any) => {
|
||||
let pathName = '';
|
||||
try {
|
||||
pathName = new URL(urlObj.url).pathname;
|
||||
} catch(e) {}
|
||||
return `${pathName} (${urlObj.id})`;
|
||||
}
|
||||
|
||||
const convertToDate = (dateString: string): string => {
|
||||
const [date, time] = dateString.split(' ');
|
||||
const [year, month, day] = date.split('-').map(Number);
|
||||
const [hour, minute, second] = time.split(':').map(Number);
|
||||
const dateObject = new Date(year, month - 1, day, hour, minute, second);
|
||||
if (isNaN(dateObject.getTime())) {
|
||||
return dateString;
|
||||
}
|
||||
return dateObject.toString();
|
||||
}
|
||||
|
||||
const MalwareCard = (props: {data: any, title: string, actionButtons: any }): JSX.Element => {
|
||||
const urlHaus = props.data.urlHaus || {};
|
||||
const phishTank = props.data.phishTank || {};
|
||||
const cloudmersive = props.data.cloudmersive || {};
|
||||
const safeBrowsing = props.data.safeBrowsing || {};
|
||||
return (
|
||||
<Card heading={props.title} actionButtons={props.actionButtons}>
|
||||
{ safeBrowsing && !safeBrowsing.error && (
|
||||
<Row lbl="Google Safe Browsing" val={safeBrowsing.unsafe ? '❌ Unsafe' : '✅ Safe'} />
|
||||
)}
|
||||
{ ((cloudmersive && !cloudmersive.error) || safeBrowsing?.details) && (
|
||||
<Row lbl="Threat Type" val={safeBrowsing?.details?.threatType || cloudmersive.WebsiteThreatType || 'None :)'} />
|
||||
)}
|
||||
{ phishTank && !phishTank.error && (
|
||||
<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="">
|
||||
<span className="lbl">Phish Info</span>
|
||||
<span className="val"><a href={phishTank.url0.phish_detail_page}>{phishTank.url0.phish_id}</a></span>
|
||||
</Row>
|
||||
)}
|
||||
{ urlHaus.query_status === 'no_results' && <Row lbl="Malware Status" val="✅ No Malwares Found" />}
|
||||
{ urlHaus.query_status === 'ok' && (
|
||||
<>
|
||||
<Row lbl="Status" val="❌ Malware Identified" />
|
||||
<Row lbl="First Seen" val={convertToDate(urlHaus.firstseen)} />
|
||||
<Row lbl="Bad URLs Count" val={urlHaus.url_count} />
|
||||
</>
|
||||
)}
|
||||
{urlHaus.urls && (
|
||||
<Expandable>
|
||||
<summary>Expand Results</summary>
|
||||
{ urlHaus.urls.map((urlResult: any, index: number) => {
|
||||
const rows = [
|
||||
{ lbl: 'ID', val: urlResult.id },
|
||||
{ lbl: 'Status', val: urlResult.url_status },
|
||||
{ lbl: 'Date Added', val: convertToDate(urlResult.date_added) },
|
||||
{ lbl: 'Threat Type', val: urlResult.threat },
|
||||
{ lbl: 'Reported By', val: urlResult.reporter },
|
||||
{ lbl: 'Takedown Time', val: urlResult.takedown_time_seconds },
|
||||
{ lbl: 'Larted', val: urlResult.larted },
|
||||
{ lbl: 'Tags', val: (urlResult.tags || []).join(', ') },
|
||||
{ lbl: 'Reference', val: urlResult.urlhaus_reference },
|
||||
{ lbl: 'File Path', val: urlResult.url },
|
||||
];
|
||||
return (<ExpandableRow lbl={getExpandableTitle(urlResult)} val="" rowList={rows} />)
|
||||
})}
|
||||
</Expandable>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default MalwareCard;
|
66
src/components/Results/TlsCipherSuites.tsx
Normal file
66
src/components/Results/TlsCipherSuites.tsx
Normal file
@ -0,0 +1,66 @@
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Card } from 'components/Form/Card';
|
||||
import Button from 'components/Form/Button';
|
||||
import { 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;
|
73
src/components/Results/TlsClientSupport.tsx
Normal file
73
src/components/Results/TlsClientSupport.tsx
Normal file
@ -0,0 +1,73 @@
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Card } from 'components/Form/Card';
|
||||
import Button from 'components/Form/Button';
|
||||
import { ExpandableRow } from 'components/Form/Row';
|
||||
|
||||
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;
|
120
src/components/Results/TlsIssueAnalysis.tsx
Normal file
120
src/components/Results/TlsIssueAnalysis.tsx
Normal file
@ -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;
|
@ -8,12 +8,13 @@ margin: 0;
|
||||
padding: 1rem;
|
||||
display: grid;
|
||||
gap: 0.5rem;
|
||||
grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr));
|
||||
grid-template-columns: repeat(auto-fit, minmax(19rem, 1fr));
|
||||
li a.resource-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: start;
|
||||
gap: 0.25rem;
|
||||
padding: 0.25rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
background: ${colors.background};
|
||||
border-radius: 8px;
|
||||
text-decoration: none;
|
||||
@ -37,27 +38,34 @@ li a.resource-wrap {
|
||||
}
|
||||
}
|
||||
img {
|
||||
width: 4rem;
|
||||
width: 2.5rem;
|
||||
border-radius: 4px;
|
||||
margin: 0.25rem 0.1rem 0.1rem 0.1rem;
|
||||
}
|
||||
p, a {
|
||||
margin: 0;
|
||||
}
|
||||
.resource-link {
|
||||
color: ${colors.primary};
|
||||
opacity: 0.75;
|
||||
font-size: 0.9rem;
|
||||
transition: all 0.2s ease-in-out;
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
.resource-title {
|
||||
font-weight: bold;
|
||||
}
|
||||
.resource-lower {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.resource-details {
|
||||
max-width: 20rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.1rem;
|
||||
p, a {
|
||||
margin: 0;
|
||||
}
|
||||
a.resource-link {
|
||||
color: ${colors.primary};
|
||||
opacity: 0.75;
|
||||
font-size: 0.9rem;
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
.resource-title {
|
||||
font-weight: bold;
|
||||
}
|
||||
.resource-description {
|
||||
color: ${colors.textColorSecondary};
|
||||
font-size: 0.9rem;
|
||||
@ -155,6 +163,13 @@ const resources = [
|
||||
description: 'Checks the performance, accessibility and SEO of a page on mobile + desktop.',
|
||||
searchLink: 'https://developers.google.com/speed/pagespeed/insights/?url={URL}',
|
||||
},
|
||||
{
|
||||
title: 'Built With',
|
||||
link: 'https://builtwith.com/',
|
||||
icon: 'https://i.ibb.co/5LXBDfD/Built-with.png',
|
||||
description: 'View the tech stack of a website',
|
||||
searchLink: 'https://builtwith.com/{URL}',
|
||||
},
|
||||
{
|
||||
title: 'DNS Dumpster',
|
||||
link: 'https://dnsdumpster.com/',
|
||||
@ -192,7 +207,7 @@ const resources = [
|
||||
];
|
||||
|
||||
const makeLink = (resource: any, scanUrl: string | undefined): string => {
|
||||
return (scanUrl && resource.searchLink) ? resource.searchLink.replaceAll('{URL}', scanUrl.replace('https://', '')) : '#';
|
||||
return (scanUrl && resource.searchLink) ? resource.searchLink.replaceAll('{URL}', scanUrl.replace('https://', '')) : resource.link;
|
||||
};
|
||||
|
||||
const AdditionalResources = (props: { url?: string }): JSX.Element => {
|
||||
@ -201,14 +216,18 @@ const AdditionalResources = (props: { url?: string }): JSX.Element => {
|
||||
{
|
||||
resources.map((resource, index) => {
|
||||
return (
|
||||
<li>
|
||||
<a className="resource-wrap" href={makeLink(resource, props.url)}>
|
||||
<img src={resource.icon} alt="" />
|
||||
<div className="resource-details">
|
||||
<li key={index}>
|
||||
<a className="resource-wrap" target="_blank" rel="noreferrer" href={makeLink(resource, props.url)}>
|
||||
<p className="resource-title">{resource.title}</p>
|
||||
<a className="resource-link" href={resource.link} target="_blank" rel="noreferrer">{new URL(resource.link).hostname}</a>
|
||||
<p className="resource-description">{resource.description}</p>
|
||||
</div>
|
||||
<span className="resource-link" onClick={()=> window.open(resource.link, '_blank')} title={`Open: ${resource.link}`}>
|
||||
{new URL(resource.link).hostname}
|
||||
</span>
|
||||
<div className="resource-lower">
|
||||
<img src={resource.icon} alt="" />
|
||||
<div className="resource-details">
|
||||
<p className="resource-description">{resource.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useMemo } from "react";
|
||||
|
||||
|
||||
const FancyBackground = (): JSX.Element => {
|
||||
@ -25,7 +25,7 @@ const FancyBackground = (): JSX.Element => {
|
||||
return result;
|
||||
};
|
||||
|
||||
const App: any = {};
|
||||
const App: any = useMemo(() => [], []);
|
||||
|
||||
App.setup = function () {
|
||||
|
||||
@ -335,7 +335,7 @@ const FancyBackground = (): JSX.Element => {
|
||||
requestAnimationFrame(frame);
|
||||
};
|
||||
frame();
|
||||
}, []);
|
||||
}, [App]);
|
||||
|
||||
|
||||
|
||||
|
@ -41,7 +41,7 @@ const Link = styled.a`
|
||||
`;
|
||||
|
||||
const Footer = (props: { isFixed?: boolean }): JSX.Element => {
|
||||
const licenseUrl = 'https://github.com/lissy93/web-check/blob/main/LICENSE';
|
||||
const licenseUrl = 'https://github.com/lissy93/web-check/blob/master/LICENSE';
|
||||
const authorUrl = 'https://aliciasykes.com';
|
||||
const githubUrl = 'https://github.com/lissy93/web-check';
|
||||
return (
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -167,6 +167,9 @@ p {
|
||||
}
|
||||
pre {
|
||||
color: ${colors.danger};
|
||||
&.info {
|
||||
color: ${colors.warning};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@ -183,30 +186,42 @@ export interface LoadingJob {
|
||||
const jobNames = [
|
||||
'get-ip',
|
||||
'location',
|
||||
'headers',
|
||||
'ssl',
|
||||
'domain',
|
||||
'dns',
|
||||
'dns-server',
|
||||
'tech-stack',
|
||||
'hosts',
|
||||
'quality',
|
||||
'tech-stack',
|
||||
'server-info',
|
||||
'cookies',
|
||||
// 'server-info',
|
||||
'redirects',
|
||||
'robots-txt',
|
||||
'headers',
|
||||
'dns',
|
||||
'hosts',
|
||||
'http-security',
|
||||
'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',
|
||||
'screenshot',
|
||||
'txt-records',
|
||||
'sitemap',
|
||||
'hsts',
|
||||
'security-txt',
|
||||
'linked-pages',
|
||||
// 'whois',
|
||||
'txt-records',
|
||||
'block-lists',
|
||||
'features',
|
||||
'sitemap',
|
||||
'carbon',
|
||||
'trace-route',
|
||||
] as const;
|
||||
|
||||
export const initialJobs = jobNames.map((job: string) => {
|
||||
@ -360,7 +375,7 @@ const ProgressLoader = (props: { loadStatus: LoadingJob[], showModal: (err: Reac
|
||||
}
|
||||
};
|
||||
|
||||
const showErrorModal = (name: string, state: LoadingState, timeTaken: number | undefined, error: string) => {
|
||||
const showErrorModal = (name: string, state: LoadingState, timeTaken: number | undefined, error: string, isInfo?: boolean) => {
|
||||
const errorContent = (
|
||||
<ErrorModalContent>
|
||||
<Heading as="h3">Error Details for {name}</Heading>
|
||||
@ -368,7 +383,8 @@ const ProgressLoader = (props: { loadStatus: LoadingJob[], showModal: (err: Reac
|
||||
The {name} job failed with an {state} state after {timeTaken} ms.
|
||||
The server responded with the following error:
|
||||
</p>
|
||||
<pre>{error}</pre>
|
||||
{ /* If isInfo == true, then add .info className to pre */}
|
||||
<pre className={isInfo ? 'info' : 'error'}>{error}</pre>
|
||||
</ErrorModalContent>
|
||||
);
|
||||
props.showModal(errorContent);
|
||||
@ -409,6 +425,7 @@ const ProgressLoader = (props: { loadStatus: LoadingJob[], showModal: (err: Reac
|
||||
<i>{(timeTaken && state !== 'loading') ? ` Took ${timeTaken} ms` : '' }</i>
|
||||
{ (retry && state !== 'success' && state !== 'loading') && <FailedJobActionButton onClick={retry}>↻ Retry</FailedJobActionButton> }
|
||||
{ (error && state === 'error') && <FailedJobActionButton onClick={() => showErrorModal(name, state, timeTaken, error)}>■ Show Error</FailedJobActionButton> }
|
||||
{ (error && state === 'skipped') && <FailedJobActionButton onClick={() => showErrorModal(name, state, timeTaken, error, true)}>■ Show Reason</FailedJobActionButton> }
|
||||
</li>
|
||||
);
|
||||
})
|
||||
|
@ -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;
|
||||
@ -38,21 +38,20 @@ const useMotherOfAllHooks = <ResultType = any>(params: UseIpAddressProps<ResultT
|
||||
const doTheFetch = () => {
|
||||
return fetchRequest()
|
||||
.then((res: any) => {
|
||||
if (!res) {
|
||||
if (!res) { // No response :(
|
||||
updateLoadingJobs(jobId, 'error', res.error || 'No response', reset);
|
||||
} else if (res.error) { // Response returned an error message
|
||||
updateLoadingJobs(jobId, 'error', res.error, reset);
|
||||
throw new Error('No response');
|
||||
} else if (res.skipped) { // Response returned a skipped message
|
||||
updateLoadingJobs(jobId, 'skipped', res.skipped, reset);
|
||||
} else { // Yay, everything went to plan :)
|
||||
setResult(res);
|
||||
updateLoadingJobs(jobId, 'success', '', undefined, res);
|
||||
}
|
||||
if (res.error) {
|
||||
updateLoadingJobs(jobId, 'error', res.error, reset);
|
||||
throw new Error(res.error);
|
||||
}
|
||||
// All went to plan, set results and mark as done
|
||||
setResult(res);
|
||||
updateLoadingJobs(jobId, 'success', '', undefined, res);
|
||||
})
|
||||
.catch((err) => {
|
||||
// Something fucked up, log the error
|
||||
updateLoadingJobs(jobId, 'error', err.error || err.message, reset);
|
||||
// Something fucked up
|
||||
updateLoadingJobs(jobId, 'error', err.error || err.message || 'Unknown error', reset);
|
||||
throw err;
|
||||
})
|
||||
}
|
||||
@ -69,6 +68,7 @@ const useMotherOfAllHooks = <ResultType = any>(params: UseIpAddressProps<ResultT
|
||||
pending: `Updating Data (${jobId})`,
|
||||
success: `Completed (${jobId})`,
|
||||
error: `Failed to update (${jobId})`,
|
||||
skipped: `Skipped job (${jobId}), as no valid results for host`,
|
||||
};
|
||||
// Initiate fetch, and show progress toast
|
||||
toast.promise(fetchyFetch, toastOptions).catch(() => {});
|
||||
|
@ -5,6 +5,10 @@
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: 'PTMono', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
|
@ -5,9 +5,9 @@ import Heading from 'components/Form/Heading';
|
||||
import Footer from 'components/misc/Footer';
|
||||
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 } from 'utils/docs';
|
||||
|
||||
import docs, { about, featureIntro, license, fairUse, supportUs } from 'utils/docs';
|
||||
|
||||
const AboutContainer = styled.div`
|
||||
width: 95vw;
|
||||
@ -16,6 +16,11 @@ margin: 2rem auto;
|
||||
padding-bottom: 1rem;
|
||||
header {
|
||||
margin 1rem 0;
|
||||
width: auto;
|
||||
}
|
||||
section {
|
||||
width: auto;
|
||||
.inner-heading { display: none; }
|
||||
}
|
||||
`;
|
||||
|
||||
@ -32,6 +37,9 @@ const Section = styled(StyledCard)`
|
||||
margin-bottom: 2rem;
|
||||
overflow: clip;
|
||||
max-height: 100%;
|
||||
section {
|
||||
clear: both;
|
||||
}
|
||||
h3 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
@ -71,13 +79,25 @@ const Section = styled(StyledCard)`
|
||||
}
|
||||
}
|
||||
}
|
||||
.screenshot {
|
||||
float: right;
|
||||
break-inside: avoid;
|
||||
max-width: 300px;
|
||||
max-height: 28rem;
|
||||
border-radius: 6px;
|
||||
.example-screenshot {
|
||||
float: right;
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
clear: both;
|
||||
max-width: 300px;
|
||||
img {
|
||||
float: right;
|
||||
break-inside: avoid;
|
||||
max-width: 300px;
|
||||
// max-height: 30rem;
|
||||
border-radius: 6px;
|
||||
clear: both;
|
||||
}
|
||||
figcaption {
|
||||
font-size: 0.8rem;
|
||||
text-align: center;
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@ -85,7 +105,6 @@ const makeAnchor = (title: string): string => {
|
||||
return title.toLowerCase().replace(/[^\w\s]|_/g, "").replace(/\s+/g, "-");
|
||||
};
|
||||
|
||||
|
||||
const About = (): JSX.Element => {
|
||||
return (
|
||||
<div>
|
||||
@ -101,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>
|
||||
@ -118,9 +147,13 @@ const About = (): JSX.Element => {
|
||||
</div>
|
||||
{docs.map((section, sectionIndex: number) => (
|
||||
<section key={section.title}>
|
||||
{ sectionIndex > 0 && <hr /> }
|
||||
<Heading as="h3" size="small" id={makeAnchor(section.title)} color={colors.primary}>{section.title}</Heading>
|
||||
{section.screenshot &&
|
||||
<img className="screenshot" src={section.screenshot} alt={`Example Screenshot ${section.title}`} />
|
||||
{section.screenshot &&
|
||||
<figure className="example-screenshot">
|
||||
<img className="screenshot" src={section.screenshot} alt={`Example Screenshot ${section.title}`} />
|
||||
<figcaption>Fig.{sectionIndex + 1} - Example of {section.title}</figcaption>
|
||||
</figure>
|
||||
}
|
||||
{section.description && <>
|
||||
<Heading as="h4" size="small">Description</Heading>
|
||||
@ -142,11 +175,24 @@ const About = (): JSX.Element => {
|
||||
))}
|
||||
</ul>
|
||||
</>}
|
||||
{ sectionIndex < docs.length - 1 && <hr /> }
|
||||
</section>
|
||||
))}
|
||||
</Section>
|
||||
|
||||
<Heading as="h2" size="medium" color={colors.primary}>API Documentation</Heading>
|
||||
<Section>
|
||||
{/* eslint-disable-next-line*/}
|
||||
<p>// Coming soon...</p>
|
||||
</Section>
|
||||
|
||||
<Heading as="h2" size="medium" color={colors.primary}>Additional Resources</Heading>
|
||||
<AdditionalResources />
|
||||
|
||||
<Heading as="h2" size="medium" color={colors.primary}>Support Us</Heading>
|
||||
<Section>
|
||||
{supportUs.map((para, index: number) => (<p dangerouslySetInnerHTML={{__html: para}} />))}
|
||||
</Section>
|
||||
|
||||
<Heading as="h2" size="medium" color={colors.primary}>Terms & Info</Heading>
|
||||
<Section>
|
||||
<Heading as="h3" size="small" color={colors.primary}>License</Heading>
|
||||
@ -166,6 +212,11 @@ const About = (): JSX.Element => {
|
||||
<Heading as="h3" size="small" color={colors.primary}>Privacy</Heading>
|
||||
<p>
|
||||
Analytics are used on the demo instance (via a self-hosted Plausible instance), this only records the URL you visited but no personal data.
|
||||
There's also some basic error logging (via a self-hosted GlitchTip instance), this is only used to help me fix bugs.
|
||||
<br />
|
||||
<br />
|
||||
Neither your IP address, browser/OS/hardware info, nor any other data will ever be collected or logged.
|
||||
(You may verify this yourself, either by inspecting the source code or the using developer tools)
|
||||
</p>
|
||||
<hr />
|
||||
<Heading as="h3" size="small" color={colors.primary}>Support</Heading>
|
||||
|
@ -67,6 +67,9 @@ const SiteFeaturesWrapper = styled(StyledCard)`
|
||||
width: calc(100% - 2rem);
|
||||
}
|
||||
}
|
||||
@media(max-width: 600px) {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
ul {
|
||||
-webkit-column-width: 150px;
|
||||
@ -179,8 +182,15 @@ const Home = (): JSX.Element => {
|
||||
</ul>
|
||||
</div>
|
||||
<div className="links">
|
||||
<a href="https://app.netlify.com/start/deploy?repository=https://github.com/lissy93/web-check"><Button>Deploy your own Instance</Button></a>
|
||||
<a href="https://github.com/lissy93/web-check"><Button>View on GitHub</Button></a>
|
||||
<a href="https://github.com/lissy93/web-check" title="Check out the source code and documentation on GitHub, and get support or contribute">
|
||||
<Button>View on GitHub</Button>
|
||||
</a>
|
||||
<a href="https://app.netlify.com/start/deploy?repository=https://github.com/lissy93/web-check" title="Deploy your own private or public instance of Web-Check to Netlify">
|
||||
<Button>Deploy your own</Button>
|
||||
</a>
|
||||
<a href="/about#api-documentation" title="View the API documentation, to use Web-Check programmatically">
|
||||
<Button>API Docs</Button>
|
||||
</a>
|
||||
</div>
|
||||
</SiteFeaturesWrapper>
|
||||
<Footer isFixed={true} />
|
||||
|
@ -20,6 +20,10 @@ const AboutContainer = styled.div`
|
||||
color: ${colors.primary};
|
||||
}
|
||||
.im-drink { font-size: 6rem; }
|
||||
header {
|
||||
width: auto;
|
||||
margin: 1rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const HeaderLinkContainer = styled.nav`
|
||||
|
@ -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'
|
||||
@ -46,14 +46,24 @@ import DnsServerCard from 'components/Results/DnsServer';
|
||||
import TechStackCard from 'components/Results/TechStack';
|
||||
import SecurityTxtCard from 'components/Results/SecurityTxt';
|
||||
import ContentLinksCard from 'components/Results/ContentLinks';
|
||||
import SocialTagsCard from 'components/Results/SocialTags';
|
||||
import MailConfigCard from 'components/Results/MailConfig';
|
||||
import HttpSecurityCard from 'components/Results/HttpSecurity';
|
||||
import FirewallCard from 'components/Results/Firewall';
|
||||
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';
|
||||
import useMotherHook from 'hooks/motherOfAllHooks';
|
||||
import {
|
||||
getLocation, ServerLocation,
|
||||
parseCookies, Cookie,
|
||||
parseRobotsTxt,
|
||||
Cookie,
|
||||
applyWhoIsResults, Whois,
|
||||
parseShodanResults, ShodanResults
|
||||
} from 'utils/result-processor';
|
||||
@ -79,6 +89,68 @@ 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, .toggle-filters {
|
||||
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, .toggle-filters {
|
||||
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;
|
||||
}
|
||||
.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 => {
|
||||
const startTime = new Date().getTime();
|
||||
|
||||
@ -88,13 +160,27 @@ 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[]>([]);
|
||||
|
||||
const updateLoadingJobs = useCallback((job: string, newState: LoadingState, error?: string, retry?: () => void, data?: any) => {
|
||||
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) => {
|
||||
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;
|
||||
@ -130,7 +216,8 @@ const Results = (): JSX.Element => {
|
||||
}
|
||||
return newJobs;
|
||||
});
|
||||
}, []);
|
||||
});
|
||||
}, [startTime]);
|
||||
|
||||
const parseJson = (response: Response): Promise<any> => {
|
||||
return new Promise((resolve) => {
|
||||
@ -145,6 +232,20 @@ const Results = (): JSX.Element => {
|
||||
});
|
||||
};
|
||||
|
||||
const urlTypeOnly = ['url'] as AddressType[]; // Many jobs only run with these address types
|
||||
|
||||
const api = process.env.REACT_APP_API_ENDPOINT || '/api'; // Where is the API hosted?
|
||||
|
||||
// Fetch and parse IP address for given URL
|
||||
const [ipAddress, setIpAddress] = useMotherHook({
|
||||
jobId: 'get-ip',
|
||||
updateLoadingJobs,
|
||||
addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly },
|
||||
fetchRequest: () => fetch(`${api}/get-ip?url=${address}`)
|
||||
.then(res => parseJson(res))
|
||||
.then(res => res.ip),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!addressType || addressType === 'empt') {
|
||||
setAddressType(determineAddressType(address || ''));
|
||||
@ -152,75 +253,7 @@ const Results = (): JSX.Element => {
|
||||
if (addressType === 'ipV4' && address) {
|
||||
setIpAddress(address);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const urlTypeOnly = ['url'] as AddressType[]; // Many jobs only run with these address types
|
||||
|
||||
const api = '/api';
|
||||
|
||||
// Fetch and parse IP address for given URL
|
||||
const [ipAddress, setIpAddress] = useMotherHook({
|
||||
jobId: 'get-ip',
|
||||
updateLoadingJobs,
|
||||
addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly },
|
||||
fetchRequest: () => fetch(`${api}/find-url-ip?url=${address}`)
|
||||
.then(res => parseJson(res))
|
||||
.then(res => res.ip),
|
||||
});
|
||||
|
||||
// Fetch and parse SSL certificate info
|
||||
const [sslResults, updateSslResults] = useMotherHook({
|
||||
jobId: 'ssl',
|
||||
updateLoadingJobs,
|
||||
addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly },
|
||||
fetchRequest: () => fetch(`${api}/ssl-check?url=${address}`).then((res) => parseJson(res)),
|
||||
});
|
||||
|
||||
// Fetch and parse cookies info
|
||||
const [cookieResults, updateCookieResults] = useMotherHook<{cookies: Cookie[]}>({
|
||||
jobId: 'cookies',
|
||||
updateLoadingJobs,
|
||||
addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly },
|
||||
fetchRequest: () => fetch(`${api}/get-cookies?url=${address}`)
|
||||
.then(res => parseJson(res))
|
||||
.then(res => parseCookies(res.cookies)),
|
||||
});
|
||||
|
||||
// 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}/read-robots-txt?url=${address}`)
|
||||
.then(res => res.text())
|
||||
.then(res => parseRobotsTxt(res)),
|
||||
});
|
||||
|
||||
// Fetch and parse headers
|
||||
const [headersResults, updateHeadersResults] = useMotherHook({
|
||||
jobId: 'headers',
|
||||
updateLoadingJobs,
|
||||
addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly },
|
||||
fetchRequest: () => fetch(`${api}/get-headers?url=${address}`).then(res => parseJson(res)),
|
||||
});
|
||||
|
||||
// Fetch and parse DNS records
|
||||
const [dnsResults, updateDnsResults] = useMotherHook({
|
||||
jobId: 'dns',
|
||||
updateLoadingJobs,
|
||||
addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly },
|
||||
fetchRequest: () => fetch(`${api}/get-dns?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}/lighthouse-report?url=${address}`)
|
||||
.then(res => parseJson(res))
|
||||
.then(res => res?.lighthouseResult || { error: 'No Data'}),
|
||||
});
|
||||
}, [address, addressType, setIpAddress]);
|
||||
|
||||
// Get IP address location info
|
||||
const [locationResults, updateLocationResults] = useMotherHook<ServerLocation>({
|
||||
@ -232,9 +265,43 @@ const Results = (): JSX.Element => {
|
||||
.then(res => getLocation(res)),
|
||||
});
|
||||
|
||||
// Fetch and parse SSL certificate info
|
||||
const [sslResults, updateSslResults] = useMotherHook({
|
||||
jobId: 'ssl',
|
||||
updateLoadingJobs,
|
||||
addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly },
|
||||
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',
|
||||
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}`)
|
||||
@ -242,12 +309,182 @@ const Results = (): JSX.Element => {
|
||||
.then(res => parseShodanResults(res)),
|
||||
});
|
||||
|
||||
// Fetch and parse cookies info
|
||||
const [cookieResults, updateCookieResults] = useMotherHook<{cookies: Cookie[]}>({
|
||||
jobId: 'cookies',
|
||||
updateLoadingJobs,
|
||||
addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly },
|
||||
fetchRequest: () => fetch(`${api}/cookies?url=${address}`)
|
||||
.then(res => parseJson(res)),
|
||||
});
|
||||
|
||||
// Fetch and parse headers
|
||||
const [headersResults, updateHeadersResults] = useMotherHook({
|
||||
jobId: 'headers',
|
||||
updateLoadingJobs,
|
||||
addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly },
|
||||
fetchRequest: () => fetch(`${api}/headers?url=${address}`).then(res => parseJson(res)),
|
||||
});
|
||||
|
||||
// Fetch and parse DNS records
|
||||
const [dnsResults, updateDnsResults] = useMotherHook({
|
||||
jobId: 'dns',
|
||||
updateLoadingJobs,
|
||||
addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly },
|
||||
fetchRequest: () => fetch(`${api}/dns?url=${address}`).then(res => parseJson(res)),
|
||||
});
|
||||
|
||||
// Get HTTP security
|
||||
const [httpSecurityResults, updateHttpSecurityResults] = useMotherHook({
|
||||
jobId: 'http-security',
|
||||
updateLoadingJobs,
|
||||
addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly },
|
||||
fetchRequest: () => fetch(`${api}/http-security?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)),
|
||||
});
|
||||
|
||||
// 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)),
|
||||
});
|
||||
|
||||
// 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
|
||||
const [portsResults, updatePortsResults] = useMotherHook({
|
||||
jobId: 'ports',
|
||||
updateLoadingJobs,
|
||||
addressInfo: { address: ipAddress, addressType: 'ipV4', expectedAddressTypes: ['ipV4', 'ipV6'] },
|
||||
fetchRequest: () => fetch(`${api}/check-ports?url=${ipAddress}`)
|
||||
fetchRequest: () => fetch(`${api}/ports?url=${ipAddress}`)
|
||||
.then(res => parseJson(res)),
|
||||
});
|
||||
|
||||
@ -266,55 +503,15 @@ const Results = (): JSX.Element => {
|
||||
jobId: 'txt-records',
|
||||
updateLoadingJobs,
|
||||
addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly },
|
||||
fetchRequest: () => fetch(`${api}/get-txt?url=${address}`).then(res => parseJson(res)),
|
||||
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}/follow-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}/server-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}/get-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}/check-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
|
||||
@ -325,20 +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)),
|
||||
fetchRequest: () => fetch(`${api}/carbon?url=${address}`).then(res => parseJson(res)),
|
||||
});
|
||||
|
||||
// Get site features from BuiltWith
|
||||
@ -346,7 +535,7 @@ const Results = (): JSX.Element => {
|
||||
jobId: 'features',
|
||||
updateLoadingJobs,
|
||||
addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly },
|
||||
fetchRequest: () => fetch(`${api}/site-features?url=${address}`)
|
||||
fetchRequest: () => fetch(`${api}/features?url=${address}`)
|
||||
.then(res => parseJson(res))
|
||||
.then(res => {
|
||||
if (res.Errors && res.Errors.length > 0) {
|
||||
@ -356,38 +545,6 @@ const Results = (): JSX.Element => {
|
||||
}),
|
||||
});
|
||||
|
||||
// Get DNSSEC info
|
||||
const [dnsSecResults, updateDnsSecResults] = useMotherHook({
|
||||
jobId: 'dnssec',
|
||||
updateLoadingJobs,
|
||||
addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly },
|
||||
fetchRequest: () => fetch(`${api}/dns-sec?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-lookup?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}/content-links?url=${address}`).then(res => parseJson(res)),
|
||||
});
|
||||
|
||||
/* Cancel remaining jobs after 10 second timeout */
|
||||
useEffect(() => {
|
||||
const checkJobs = () => {
|
||||
@ -401,7 +558,7 @@ const Results = (): JSX.Element => {
|
||||
return () => {
|
||||
clearTimeout(timeoutId);
|
||||
};
|
||||
}, [loadingJobs, updateLoadingJobs]); // dependencies for the effect
|
||||
}, [loadingJobs, updateLoadingJobs]);
|
||||
|
||||
const makeSiteName = (address: string): string => {
|
||||
try {
|
||||
@ -413,36 +570,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 Info', result: sslResults, Component: SslCertCard, refresh: updateSslResults },
|
||||
{ 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: 'tech-stack', title: 'Tech Stack', result: techStackResults, Component: TechStackCard, refresh: updateTechStackResults },
|
||||
{ id: 'quality', title: 'Quality Summary', result: lighthouseResults, Component: LighthouseCard, refresh: updateLighthouseResults },
|
||||
{ id: 'cookies', title: 'Cookies', result: cookieResults, Component: CookiesCard, refresh: updateCookieResults },
|
||||
{ id: 'trace-route', title: 'Trace Route', result: traceRouteResults, Component: TraceRouteCard, refresh: updateTraceRouteResults },
|
||||
{ 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: '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: 'screenshot', title: 'Screenshot', result: screenshotResult || lighthouseResults?.fullPageScreenshot?.screenshot, Component: ScreenshotCard, refresh: updateScreenshotResult },
|
||||
{ id: 'txt-records', title: 'TXT Records', result: txtRecordResults, Component: TxtRecordCard, refresh: updateTxtRecordResults },
|
||||
{ id: 'hsts', title: 'HSTS Check', result: hstsResults, Component: HstsCard, refresh: updateHstsResults },
|
||||
{ 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: '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 },
|
||||
|
||||
{
|
||||
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 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: '↻'},
|
||||
@ -467,7 +857,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>
|
||||
}
|
||||
@ -475,25 +865,60 @@ 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>{ showFilters ? <>
|
||||
<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">
|
||||
<span className="group-label">Search</span>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Filter Results"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
<span className="toggle-filters" onClick={() => setShowFilters(false)}>Hide</span>
|
||||
</div>
|
||||
</> : (
|
||||
<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>
|
||||
|
||||
<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) => (
|
||||
(result && !result.error) ? (
|
||||
resultCardData
|
||||
.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}`}
|
||||
data={{...result}}
|
||||
title={title}
|
||||
actionButtons={refresh ? MakeActionButtons(title, refresh, () => showInfo(id)) : undefined}
|
||||
actionButtons={refresh ? makeActionButtons(title, refresh, () => showInfo(id)) : undefined}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
) : <></>
|
||||
))
|
||||
) : null})
|
||||
}
|
||||
</Masonry>
|
||||
</ResultsContent>
|
||||
|
@ -7,13 +7,12 @@ export type AddressType = 'ipV4' | 'ipV6' | 'url' | 'err' | 'empt';
|
||||
|
||||
/* Checks if a given string looks like a URL */
|
||||
const isUrl = (value: string):boolean => {
|
||||
const urlPattern = new RegExp(
|
||||
'^(https?:\\/\\/)?' +
|
||||
'(?!([0-9]{1,3}\\.){3}[0-9]{1,3})' + // Exclude IP addresses
|
||||
'(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]*[a-zA-Z0-9])\\.)*' + // Domain name or a subdomain
|
||||
'([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\\-]*[A-Za-z0-9])$', // Second level domain
|
||||
'i' // Case-insensitive
|
||||
);
|
||||
var urlPattern = new RegExp('^(https?:\\/\\/)?'+ // validate protocol
|
||||
'((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|'+ // validate domain name
|
||||
'((\\d{1,3}\\.){3}\\d{1,3}))'+ // validate OR ip (v4) address
|
||||
'(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*'+ // validate port and path
|
||||
'(\\?[;&a-z\\d%_.~+=-]*)?'+ // validate query string
|
||||
'(\\#[-a-z\\d_]*)?$','i'); // validate fragment locator
|
||||
return urlPattern.test(value);
|
||||
};
|
||||
|
||||
|
@ -12,26 +12,26 @@ const docs: Doc[] = [
|
||||
id: "get-ip",
|
||||
title: "IP Info",
|
||||
description:
|
||||
"The IP Address task involves mapping the user provided URL to its corresponding IP address through a process known as Domain Name System (DNS) resolution. An IP address is a unique identifier given to every device on the Internet, and when paired with a domain name, it allows for accurate routing of online requests and responses.",
|
||||
use: "Identifying the IP address of a domain can be incredibly valuable for OSINT purposes. This information can aid in creating a detailed map of a target's network infrastructure, pinpointing the physical location of a server, identifying the hosting service, and even discovering other domains that are hosted on the same IP address. In cybersecurity, it's also useful for tracking the sources of attacks or malicious activities.",
|
||||
"An IP address (Internet Protocol address) is a numerical label assigned to each device connected to a network / the internet. The IP associated with a given domain can be found by querying the Domain Name System (DNS) for the domain's A (address) record.",
|
||||
use: "Finding the IP of a given server is the first step to conducting further investigations, as it allows us to probe the server for additional info. Including creating a detailed map of a target's network infrastructure, pinpointing the physical location of a server, identifying the hosting service, and even discovering other domains that are hosted on the same IP address.",
|
||||
resources: [
|
||||
"https://en.wikipedia.org/wiki/IP_address",
|
||||
"https://tools.ietf.org/html/rfc791",
|
||||
"https://www.cloudflare.com/learning/dns/what-is-dns/",
|
||||
"https://www.whois.com/whois-lookup",
|
||||
{ title: 'Understanding IP Addresses', link: 'https://www.digitalocean.com/community/tutorials/understanding-ip-addresses-subnets-and-cidr-notation-for-networking'},
|
||||
{ title: 'IP Addresses - Wiki', link: 'https://en.wikipedia.org/wiki/IP_address'},
|
||||
{ title: 'RFC-791 Internet Protocol', link: 'https://tools.ietf.org/html/rfc791'},
|
||||
{ title: 'whatismyipaddress.com', link: 'https://whatismyipaddress.com/'},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "ssl",
|
||||
title: "SSL Chain",
|
||||
description:
|
||||
"The SSL task involves checking if the site has a valid Secure Sockets Layer (SSL) certificate. SSL is a protocol for establishing authenticated and encrypted links between networked computers. It's commonly used for securing communications over the internet, such as web browsing sessions, email transmissions, and more. In this task, we reach out to the server and initiate a SSL handshake. If successful, we gather details about the SSL certificate presented by the server.",
|
||||
"SSL certificates are digital certificates that authenticate the identity of a website or server, enable secure encrypted communication (HTTPS), and establish trust between clients and servers. A valid SSL certificate is required for a website to be able to use the HTTPS protocol, and encrypt user + site data in transit. SSL certificates are issued by Certificate Authorities (CAs), which are trusted third parties that verify the identity and legitimacy of the certificate holder.",
|
||||
use: "SSL certificates not only provide the assurance that data transmission to and from the website is secure, but they also provide valuable OSINT data. Information from an SSL certificate can include the issuing authority, the domain name, its validity period, and sometimes even organization details. This can be useful for verifying the authenticity of a website, understanding its security setup, or even for discovering associated subdomains or other services.",
|
||||
resources: [
|
||||
"https://en.wikipedia.org/wiki/Transport_Layer_Security",
|
||||
"https://tools.ietf.org/html/rfc8446",
|
||||
"https://letsencrypt.org/docs/",
|
||||
"https://www.sslshopper.com/ssl-checker.html",
|
||||
{ title: 'TLS - Wiki', link: 'https://en.wikipedia.org/wiki/Transport_Layer_Security'},
|
||||
{ title: 'What is SSL (via Cloudflare learning)', link: 'https://www.cloudflare.com/learning/ssl/what-is-ssl/'},
|
||||
{ title: 'RFC-8446 - TLS', link: 'https://tools.ietf.org/html/rfc8446'},
|
||||
{ title: 'SSL Checker', link: 'https://www.sslshopper.com/ssl-checker.html'},
|
||||
],
|
||||
screenshot: 'https://i.ibb.co/kB7LsV1/wc-ssl.png',
|
||||
},
|
||||
@ -39,13 +39,13 @@ const docs: Doc[] = [
|
||||
id: "dns",
|
||||
title: "DNS Records",
|
||||
description:
|
||||
"The DNS Records task involves querying the Domain Name System (DNS) for records associated with the target domain. DNS is a system that translates human-readable domain names into IP addresses that computers use to communicate. Various types of DNS records exist, including A (address), MX (mail exchange), NS (name server), CNAME (canonical name), and TXT (text), among others.",
|
||||
"This task involves looking up the DNS records associated with a specific domain. DNS is a system that translates human-readable domain names into IP addresses that computers use to communicate. Various types of DNS records exist, including A (address), MX (mail exchange), NS (name server), CNAME (canonical name), and TXT (text), among others.",
|
||||
use: "Extracting DNS records can provide a wealth of information in an OSINT investigation. For example, A and AAAA records can disclose IP addresses associated with a domain, potentially revealing the location of servers. MX records can give clues about a domain's email provider. TXT records are often used for various administrative purposes and can sometimes inadvertently leak internal information. Understanding a domain's DNS setup can also be useful in understanding how its online infrastructure is built and managed.",
|
||||
resources: [
|
||||
"https://en.wikipedia.org/wiki/List_of_DNS_record_types",
|
||||
"https://tools.ietf.org/html/rfc1035",
|
||||
"https://mxtoolbox.com/DNSLookup.aspx",
|
||||
"https://www.dnswatch.info/",
|
||||
{ title: 'What are DNS records? (via Cloudflare learning)', link: 'https://www.cloudflare.com/learning/dns/dns-records/'},
|
||||
{ title: 'DNS Record Types', link: 'https://en.wikipedia.org/wiki/List_of_DNS_record_types'},
|
||||
{ title: 'RFC-1035 - DNS', link: 'https://tools.ietf.org/html/rfc1035'},
|
||||
{ title: 'DNS Lookup (via MxToolbox)', link: 'https://mxtoolbox.com/DNSLookup.aspx'},
|
||||
],
|
||||
screenshot: 'https://i.ibb.co/7Q1kMwM/wc-dns.png',
|
||||
},
|
||||
@ -54,12 +54,12 @@ const docs: Doc[] = [
|
||||
title: "Cookies",
|
||||
description:
|
||||
"The Cookies task involves examining the HTTP cookies set by the target website. Cookies are small pieces of data stored on the user's computer by the web browser while browsing a website. They hold a modest amount of data specific to a particular client and website, such as site preferences, the state of the user's session, or tracking information.",
|
||||
use: "Cookies provide a wealth of information in an OSINT investigation. They can disclose information about how the website tracks and interacts with its users. For instance, session cookies can reveal how user sessions are managed, and tracking cookies can hint at what kind of tracking or analytics frameworks are being used. Additionally, examining cookie policies and practices can offer insights into the site's security settings and compliance with privacy regulations.",
|
||||
use: "Cookies can disclose information about how the website tracks and interacts with its users. For instance, session cookies can reveal how user sessions are managed, and tracking cookies can hint at what kind of tracking or analytics frameworks are being used. Additionally, examining cookie policies and practices can offer insights into the site's security settings and compliance with privacy regulations.",
|
||||
resources: [
|
||||
"https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies",
|
||||
"https://www.cookiepro.com/knowledge/what-is-a-cookie/",
|
||||
"https://owasp.org/www-community/controls/SecureFlag",
|
||||
"https://tools.ietf.org/html/rfc6265",
|
||||
{ title: 'HTTP Cookie Docs (Mozilla)', link: 'https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies' },
|
||||
{ title: 'What are Cookies (via Cloudflare Learning)', link: 'https://www.cloudflare.com/learning/privacy/what-are-cookies/' },
|
||||
{ title: 'Testing for Cookie Attributes (OWASP)', link: 'https://owasp.org/www-project-web-security-testing-guide/v42/4-Web_Application_Security_Testing/06-Session_Management_Testing/02-Testing_for_Cookies_Attributes' },
|
||||
{ title: 'RFC-6265 - Coolies', link: 'https://tools.ietf.org/html/rfc6265' },
|
||||
],
|
||||
screenshot: 'https://i.ibb.co/TTQ6DtP/wc-cookies.png',
|
||||
},
|
||||
@ -67,13 +67,13 @@ const docs: Doc[] = [
|
||||
id: "robots-txt",
|
||||
title: "Crawl Rules",
|
||||
description:
|
||||
"The Crawl Rules task is focused on retrieving and interpreting the 'robots.txt' file from the target website. This text file is part of the Robots Exclusion Protocol (REP), a group of web standards that regulate how robots crawl the web, access and index content, and serve that content up to users. The file indicates which parts of the site the website owner doesn't want to be accessed by web crawler bots.",
|
||||
use: "The 'robots.txt' file can provide valuable information for an OSINT investigation. It often discloses the directories and pages that the site owner doesn't want to be indexed, potentially because they contain sensitive information. Moreover, it might reveal the existence of otherwise hidden or unlinked directories. Additionally, understanding crawl rules may offer insights into a website's SEO strategies.",
|
||||
"Robots.txt is a file found (usually) at the root of a domain, and is used to implement the Robots Exclusion Protocol (REP) to indicate which pages should be ignored by which crawlers and bots. It's good practice to avoid search engine crawlers from over-loading your site, but should not be used to keep pages out of search results (use the noindex meta tag or header instead).",
|
||||
use: "It's often useful to check the robots.txt file during an investigation, as it can sometimes disclose the directories and pages that the site owner doesn't want to be indexed, potentially because they contain sensitive information, or reveal the existence of otherwise hidden or unlinked directories. Additionally, understanding crawl rules may offer insights into a website's SEO strategies.",
|
||||
resources: [
|
||||
"https://developers.google.com/search/docs/advanced/robots/intro",
|
||||
"https://www.robotstxt.org/robotstxt.html",
|
||||
"https://moz.com/learn/seo/robotstxt",
|
||||
"https://en.wikipedia.org/wiki/Robots_exclusion_standard",
|
||||
{ title: 'Google Search Docs - Robots.txt', link: 'https://developers.google.com/search/docs/advanced/robots/intro' },
|
||||
{ title: 'Learn about robots.txt (via Moz.com)', link: 'https://moz.com/learn/seo/robotstxt' },
|
||||
{ title: 'RFC-9309 - Robots Exclusion Protocol', link: 'https://datatracker.ietf.org/doc/rfc9309/' },
|
||||
{ title: 'Robots.txt - wiki', link: 'https://en.wikipedia.org/wiki/Robots_exclusion_standard' },
|
||||
],
|
||||
screenshot: 'https://i.ibb.co/KwQCjPf/wc-robots.png',
|
||||
},
|
||||
@ -84,10 +84,10 @@ const docs: Doc[] = [
|
||||
"The Headers task involves extracting and interpreting the HTTP headers sent by the target website during the request-response cycle. HTTP headers are key-value pairs sent at the start of an HTTP response, or before the actual data. Headers contain important directives for how to handle the data being transferred, including cache policies, content types, encoding, server information, security policies, and more.",
|
||||
use: "Analyzing HTTP headers can provide significant insights in an OSINT investigation. Headers can reveal specific server configurations, chosen technologies, caching directives, and various security settings. This information can help to determine a website's underlying technology stack, server-side security measures, potential vulnerabilities, and general operational practices.",
|
||||
resources: [
|
||||
"https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers",
|
||||
"https://tools.ietf.org/html/rfc7231#section-3.2",
|
||||
"https://www.w3schools.com/tags/ref_httpheaders.asp",
|
||||
"https://owasp.org/www-project-secure-headers/",
|
||||
{ title: 'HTTP Headers - Docs', link: 'https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers' },
|
||||
{ title: 'RFC-7231 Section 7 - Headers', link: 'https://datatracker.ietf.org/doc/html/rfc7231#section-7' },
|
||||
{ title: 'List of header response fields', link: 'https://en.wikipedia.org/wiki/List_of_HTTP_header_fields' },
|
||||
{ title: 'OWASP Secure Headers Project', link: 'https://owasp.org/www-project-secure-headers/' },
|
||||
],
|
||||
screenshot: 'https://i.ibb.co/t3xcwP1/wc-headers.png',
|
||||
},
|
||||
@ -95,13 +95,15 @@ const docs: Doc[] = [
|
||||
id: "quality",
|
||||
title: "Quality Metrics",
|
||||
description:
|
||||
"The Headers task involves extracting and interpreting the HTTP headers sent by the target website during the request-response cycle. HTTP headers are key-value pairs sent at the start of an HTTP response, or before the actual data. Headers contain important directives for how to handle the data being transferred, including cache policies, content types, encoding, server information, security policies, and more.",
|
||||
use: "Analyzing HTTP headers can provide significant insights in an OSINT investigation. Headers can reveal specific server configurations, chosen technologies, caching directives, and various security settings. This information can help to determine a website's underlying technology stack, server-side security measures, potential vulnerabilities, and general operational practices.",
|
||||
"Using Lighthouse, the Quality Metrics task measures the performance, accessibility, best practices, and SEO of the target website. This returns a simple checklist of 100 core metrics, along with a score for each category, to gauge the overall quality of a given site.",
|
||||
use: "Useful for assessing a site's technical health, SEO issues, identify vulnerabilities, and ensure compliance with standards.",
|
||||
resources: [
|
||||
"https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers",
|
||||
"https://tools.ietf.org/html/rfc7231#section-3.2",
|
||||
"https://www.w3schools.com/tags/ref_httpheaders.asp",
|
||||
"https://owasp.org/www-project-secure-headers/",
|
||||
{ title: 'Lighthouse Docs', link: 'https://developer.chrome.com/docs/lighthouse/' },
|
||||
{ title: 'Google Page Speed Tools', link: 'https://developers.google.com/speed' },
|
||||
{ title: 'W3 Accessibility Tools', link: 'https://www.w3.org/WAI/test-evaluate/' },
|
||||
{ title: 'Google Search Console', link: 'https://search.google.com/search-console' },
|
||||
{ title: 'SEO Checker', link: 'https://www.seobility.net/en/seocheck/' },
|
||||
{ title: 'PWA Builder', link: 'https://www.pwabuilder.com/' },
|
||||
],
|
||||
screenshot: 'https://i.ibb.co/Kqg8rx7/wc-quality.png',
|
||||
},
|
||||
@ -109,13 +111,11 @@ const docs: Doc[] = [
|
||||
id: "location",
|
||||
title: "Server Location",
|
||||
description:
|
||||
"The Server Location task determines the physical location of a server hosting a website based on its IP address. The geolocation data typically includes the country, region, and often city where the server is located. The task also provides additional contextual information such as the official language, currency, and flag of the server's location country.",
|
||||
use: "In the realm of OSINT, server location information can be very valuable. It can give an indication of the possible jurisdiction that laws the data on the server falls under, which can be important in legal or investigative contexts. The server location can also hint at the target audience of a website and reveal inconsistencies that could suggest the use of hosting or proxy services to disguise the actual location.",
|
||||
"The Server Location task determines the physical location of the server hosting a given website based on its IP address. This is done by looking up the IP in a location database, which maps the IP to a lat + long of known data centers and ISPs. From the latitude and longitude, it's then possible to show additional contextual info, like a pin on the map, along with address, flag, time zone, currency, etc.",
|
||||
use: "Knowing the server location is a good first step in better understanding a website. For site owners this aids in optimizing content delivery, ensuring compliance with data residency requirements, and identifying potential latency issues that may impact user experience in specific geographical regions. And for security researcher, assess the risk posed by specific regions or jurisdictions regarding cyber threats and regulations.",
|
||||
resources: [
|
||||
"https://en.wikipedia.org/wiki/Geolocation_software",
|
||||
"https://www.iplocation.net/",
|
||||
"https://www.cloudflare.com/learning/cdn/glossary/geolocation/",
|
||||
"https://developers.google.com/maps/documentation/geolocation/intro",
|
||||
{ title: 'IP Locator', link: 'https://geobytes.com/iplocator/' },
|
||||
{ title: 'Internet Geolocation - Wiki', link: 'https://en.wikipedia.org/wiki/Internet_geolocation' },
|
||||
],
|
||||
screenshot: 'https://i.ibb.co/cXH2hfR/wc-location.png',
|
||||
},
|
||||
@ -123,13 +123,13 @@ const docs: Doc[] = [
|
||||
id: "hosts",
|
||||
title: "Associated Hosts",
|
||||
description:
|
||||
"This task involves identifying and listing all domains and subdomains (hostnames) that are associated with the website's primary domain. This process often involves DNS enumeration to discover any linked domains and hostnames.",
|
||||
use: "In OSINT investigations, understanding the full scope of a target's web presence is critical. Associated domains could lead to uncovering related projects, backup sites, development/test sites, or services linked to the main site. These can sometimes provide additional information or potential security vulnerabilities. A comprehensive list of associated domains and hostnames can also give an overview of the organization's structure and online footprint.",
|
||||
"This task involves identifying and listing all domains and subdomains (hostnames) that are associated with the website's primary domain. This process often involves DNS enumeration to discover any linked domains and hostnames, as well as looking at known DNS records.",
|
||||
use: "During an investigation, understanding the full scope of a target's web presence is critical. Associated domains could lead to uncovering related projects, backup sites, development/test sites, or services linked to the main site. These can sometimes provide additional information or potential security vulnerabilities. A comprehensive list of associated domains and hostnames can also give an overview of the organization's structure and online footprint.",
|
||||
resources: [
|
||||
"https://en.wikipedia.org/wiki/Domain_Name_System",
|
||||
"https://resources.infosecinstitute.com/topic/dns-enumeration-pentest/",
|
||||
"https://subdomainfinder.c99.nl/",
|
||||
"https://securitytrails.com/blog/top-dns-enumeration-tools",
|
||||
{ title: 'DNS Enumeration - Wiki', link: 'https://en.wikipedia.org/wiki/DNS_enumeration' },
|
||||
{ title: 'OWASP - Enumerate Applications on Webserver', link: 'https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/01-Information_Gathering/04-Enumerate_Applications_on_Webserver' },
|
||||
{ title: 'DNS Enumeration - DNS Dumpster', link: 'https://dnsdumpster.com/' },
|
||||
{ title: 'Subdomain Finder', link: 'https://subdomainfinder.c99.nl/' },
|
||||
],
|
||||
screenshot: 'https://i.ibb.co/25j1sT7/wc-hosts.png',
|
||||
},
|
||||
@ -138,12 +138,11 @@ const docs: Doc[] = [
|
||||
title: "Redirect Chain",
|
||||
description:
|
||||
"This task traces the sequence of HTTP redirects that occur from the original URL to the final destination URL. An HTTP redirect is a response with a status code that advises the client to go to another URL. Redirects can occur for several reasons, such as URL normalization (directing to the www version of the site), enforcing HTTPS, URL shorteners, or forwarding users to a new site location.",
|
||||
use: "Understanding the redirect chain can be crucial for several reasons. From a security perspective, long or complicated redirect chains can be a sign of potential security risks, such as unencrypted redirects in the chain. Additionally, redirects can impact website performance and SEO, as each redirect introduces additional round-trip-time (RTT). For OSINT, understanding the redirect chain can help identify relationships between different domains or reveal the use of certain technologies or hosting providers.",
|
||||
use: "Understanding the redirect chain can be useful for several reasons. From a security perspective, long or complicated redirect chains can be a sign of potential security risks, such as unencrypted redirects in the chain. Additionally, redirects can impact website performance and SEO, as each redirect introduces additional round-trip-time (RTT). For OSINT, understanding the redirect chain can help identify relationships between different domains or reveal the use of certain technologies or hosting providers.",
|
||||
resources: [
|
||||
"https://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections",
|
||||
"https://en.wikipedia.org/wiki/URL_redirection",
|
||||
"https://www.screamingfrog.co.uk/server-response-codes/",
|
||||
"https://ahrefs.com/blog/301-redirects/",
|
||||
{ title: 'HTTP Redirects - MDN', link: 'https://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections' },
|
||||
{ title: 'URL Redirection - Wiki', link: 'https://en.wikipedia.org/wiki/URL_redirection' },
|
||||
{ title: '301 Redirects explained', link: 'https://ahrefs.com/blog/301-redirects/' },
|
||||
],
|
||||
screenshot: 'https://i.ibb.co/hVVrmwh/wc-redirects.png',
|
||||
},
|
||||
@ -151,27 +150,22 @@ const docs: Doc[] = [
|
||||
id: "txt-records",
|
||||
title: "TXT Records",
|
||||
description:
|
||||
"TXT records are a type of Domain Name Service (DNS) record that provides text information to sources outside your domain. They can be used for a variety of purposes, such as verifying domain ownership, ensuring email security, and even preventing unauthorized changes to your website.",
|
||||
use: "In the context of OSINT, TXT records can be a valuable source of information. They may reveal details about the domain's email configuration, the use of specific services like Google Workspace or Microsoft 365, or security measures in place such as SPF and DKIM. Understanding these details can give an insight into the technologies used by the organization, their email security practices, and potential vulnerabilities.",
|
||||
"TXT records are a type of DNS record that provides text information to sources outside your domain. They can be used for a variety of purposes, such as verifying domain ownership, ensuring email security, and even preventing unauthorized changes to your website.",
|
||||
use: "The TXT records often reveal which external services and technologies are being used with a given domain. They may reveal details about the domain's email configuration, the use of specific services like Google Workspace or Microsoft 365, or security measures in place such as SPF and DKIM. Understanding these details can give an insight into the technologies used by the organization, their email security practices, and potential vulnerabilities.",
|
||||
resources: [
|
||||
"https://www.cloudflare.com/learning/dns/dns-records/dns-txt-record/",
|
||||
"https://en.wikipedia.org/wiki/TXT_record",
|
||||
"https://tools.ietf.org/html/rfc7208",
|
||||
"https://dmarc.org/wiki/FAQ",
|
||||
{ title: 'TXT Records (via Cloudflare Learning)', link: 'https://www.cloudflare.com/learning/dns/dns-records/dns-txt-record/' },
|
||||
{ title: 'TXT Records - Wiki', link: 'https://en.wikipedia.org/wiki/TXT_record' },
|
||||
{ title: 'RFC-1464 - TXT Records', link: 'https://datatracker.ietf.org/doc/html/rfc1464' },
|
||||
{ title: 'TXT Record Lookup (via MxToolbox)', link: 'https://mxtoolbox.com/TXTLookup.aspx' },
|
||||
],
|
||||
screenshot: 'https://i.ibb.co/wyt21QN/wc-txt-records.png',
|
||||
},
|
||||
{
|
||||
id: "status",
|
||||
title: "Server Status",
|
||||
description:
|
||||
"TXT records are a type of Domain Name Service (DNS) record that provides text information to sources outside your domain. They can be used for a variety of purposes, such as verifying domain ownership, ensuring email security, and even preventing unauthorized changes to your website.",
|
||||
use: "In the context of OSINT, TXT records can be a valuable source of information. They may reveal details about the domain's email configuration, the use of specific services like Google Workspace or Microsoft 365, or security measures in place such as SPF and DKIM. Understanding these details can give an insight into the technologies used by the organization, their email security practices, and potential vulnerabilities.",
|
||||
description: "Checks if a server is online and responding to requests.",
|
||||
use: "",
|
||||
resources: [
|
||||
"https://www.cloudflare.com/learning/dns/dns-records/dns-txt-record/",
|
||||
"https://en.wikipedia.org/wiki/TXT_record",
|
||||
"https://tools.ietf.org/html/rfc7208",
|
||||
"https://dmarc.org/wiki/FAQ",
|
||||
],
|
||||
screenshot: 'https://i.ibb.co/V9CNLBK/wc-status.png',
|
||||
},
|
||||
@ -180,12 +174,10 @@ const docs: Doc[] = [
|
||||
title: "Open Ports",
|
||||
description:
|
||||
"Open ports on a server are endpoints of communication which are available for establishing connections with clients. Each port corresponds to a specific service or protocol, such as HTTP (port 80), HTTPS (port 443), FTP (port 21), etc. The open ports on a server can be determined using techniques such as port scanning.",
|
||||
use: "In the context of OSINT, knowing which ports are open on a server can provide valuable information about the services running on that server. This information can be useful for understanding the potential vulnerabilities of the system, or for understanding the nature of the services the server is providing. For example, a server with port 22 open (SSH) might be used for remote administration, while a server with port 443 open is serving HTTPS traffic.",
|
||||
use: "Knowing which ports are open on a server can provide information about the services running on that server, useful for understanding the potential vulnerabilities of the system, or for understanding the nature of the services the server is providing.",
|
||||
resources: [
|
||||
"https://www.netwrix.com/port_scanning.html",
|
||||
"https://nmap.org/book/man-port-scanning-basics.html",
|
||||
"https://www.cloudflare.com/learning/ddos/glossary/open-port/",
|
||||
"https://en.wikipedia.org/wiki/List_of_TCP_and_UDP_port_numbers",
|
||||
{ title: 'List of TCP & UDP Port Numbers', link: 'https://en.wikipedia.org/wiki/List_of_TCP_and_UDP_port_numbers' },
|
||||
{ title: 'NMAP - Port Scanning Basics', link: 'https://nmap.org/book/man-port-scanning-basics.html' },
|
||||
],
|
||||
screenshot: 'https://i.ibb.co/F8D1hmf/wc-ports.png',
|
||||
},
|
||||
@ -210,12 +202,13 @@ const docs: Doc[] = [
|
||||
"This task calculates the estimated carbon footprint of a website. It's based on the amount of data being transferred and processed, and the energy usage of the servers that host and deliver the website. The larger the website and the more complex its features, the higher its carbon footprint is likely to be.",
|
||||
use: "From an OSINT perspective, understanding a website's carbon footprint doesn't directly provide insights into its internal workings or the organization behind it. However, it can still be valuable data in broader analyses, especially in contexts where environmental impact is a consideration. For example, it can be useful for activists, researchers, or ethical hackers who are interested in the sustainability of digital infrastructure, and who want to hold organizations accountable for their environmental impact.",
|
||||
resources: [
|
||||
"https://www.websitecarbon.com/",
|
||||
"https://www.thegreenwebfoundation.org/",
|
||||
"https://www.nature.com/articles/s41598-020-76164-y",
|
||||
"https://www.sciencedirect.com/science/article/pii/S0959652620307817",
|
||||
{ title: 'WebsiteCarbon - Carbon Calculator', link: 'https://www.websitecarbon.com/' },
|
||||
{ title: 'The Green Web Foundation', link: 'https://www.thegreenwebfoundation.org/' },
|
||||
{ title: 'The Eco Friendly Web Alliance', link: 'https://ecofriendlyweb.org/' },
|
||||
{ title: 'Reset.org', link: 'https://en.reset.org/' },
|
||||
{ title: 'Your website is killing the planet - via Wired', link: 'https://www.wired.co.uk/article/internet-carbon-footprint' },
|
||||
],
|
||||
screenshot: 'https://i.ibb.co/dmbFxjN/wc-carbon.png',
|
||||
screenshot: 'https://i.ibb.co/5v6fSyw/Screenshot-from-2023-07-29-19-07-50.png',
|
||||
},
|
||||
{
|
||||
id: "server-info",
|
||||
@ -261,7 +254,7 @@ const docs: Doc[] = [
|
||||
id: "dnssec",
|
||||
title: "DNS Security Extensions",
|
||||
description:
|
||||
"Without DNSSEC, it\"'s possible for MITM attackers to spoof records and lead users to phishing sites. This is because the DNS system includes no built-in methods to verify that the response to the request was not forged, or that any other part of the process wasn’t interrupted by an attacker. The DNS Security Extensions (DNSSEC) secures DNS lookups by signing your DNS records using public keys, so browsers can detect if the response has been tampered with. Another solution to this issue is DoH (DNS over HTTPS) and DoT (DNS over TLD).",
|
||||
"Without DNSSEC, it's possible for MITM attackers to spoof records and lead users to phishing sites. This is because the DNS system includes no built-in methods to verify that the response to the request was not forged, or that any other part of the process wasn’t interrupted by an attacker. The DNS Security Extensions (DNSSEC) secures DNS lookups by signing your DNS records using public keys, so browsers can detect if the response has been tampered with. Another solution to this issue is DoH (DNS over HTTPS) and DoT (DNS over TLD).",
|
||||
use: "DNSSEC information provides insight into an organization's level of cybersecurity maturity and potential vulnerabilities, particularly around DNS spoofing and cache poisoning. If no DNS secururity (DNSSEC, DoH, DoT, etc) is implemented, this may provide an entry point for an attacker.",
|
||||
resources: [
|
||||
"https://dnssec-analyzer.verisignlabs.com/",
|
||||
@ -288,7 +281,7 @@ const docs: Doc[] = [
|
||||
+'mechanism that helps protect websites against protocol downgrade attacks and '
|
||||
+ 'cookie hijacking. A website can be included in the HSTS preload list by '
|
||||
+ 'conforming to a set of requirements and then submitting itself to the list.',
|
||||
use: `There are several reasons why it\'s important for a site to be HSTS enabled:
|
||||
use: `There are several reasons why it's important for a site to be HSTS enabled:
|
||||
1. User bookmarks or manually types http://example.com and is subject to a man-in-the-middle attacker
|
||||
HSTS automatically redirects HTTP requests to HTTPS for the target domain
|
||||
2. Web application that is intended to be purely HTTPS inadvertently contains HTTP links or serves content over HTTP
|
||||
@ -303,32 +296,27 @@ 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: [],
|
||||
},
|
||||
{
|
||||
id: 'dns-server',
|
||||
title: 'DNS Server',
|
||||
description: 'This check determines the DNS server(s) that the requested URL / IP resolves to. Also fires off a rudimentary check to see if the DNS server supports DoH, and weather it\'s vulnerable to DNS cache poisoning.',
|
||||
use: '',
|
||||
resources: [],
|
||||
screenshot: 'https://i.ibb.co/tKpL8F9/Screenshot-from-2023-08-12-15-43-12.png',
|
||||
},
|
||||
{
|
||||
id: 'tech-stack',
|
||||
title: 'Tech Stack',
|
||||
description: 'Checks what technologies a site is built with.'
|
||||
description: 'Checks what technologies a site is built with. '
|
||||
+ 'This is done by fetching and parsing the site, then comparing it against a bit list of RegEx maintained by Wappalyzer to identify the unique fingerprints that different technologies leave.',
|
||||
use: 'Identifying a website\'s tech stack aids in evaluating its security by exposing potential vulnerabilities, '
|
||||
+ 'informs competitive analyses and development decisions, and can guide tailored marketing strategies. '
|
||||
+ 'Ethical application of this knowledge is crucial to avoid harmful activities like data theft or unauthorized intrusion.',
|
||||
resources: [
|
||||
'https://builtwith.com/',
|
||||
'https://github.com/wappalyzer/wappalyzer/tree/master/src/technologies'
|
||||
{ title: 'Wappalyzer fingerprints', link: 'https://github.com/wappalyzer/wappalyzer/tree/master/src/technologies'},
|
||||
{ title: 'BuiltWith - Check what tech a site is using', link: 'https://builtwith.com/'},
|
||||
],
|
||||
screenshot: 'https://i.ibb.co/bBQSQNz/Screenshot-from-2023-08-12-15-43-46.png',
|
||||
},
|
||||
{
|
||||
id: 'sitemap',
|
||||
@ -336,9 +324,9 @@ const docs: Doc[] = [
|
||||
description: 'This job finds and parses a site\'s listed sitemap. This file lists public sub-pages on the site, which the author wishes to be crawled by search engines. Sitemaps help with SEO, but are also useful for seeing all a sites public content at a glance.',
|
||||
use: 'Understand the structure of a site\'s public-facing content, and for site-owners, check that you\'re site\'s sitemap is accessible, parsable and contains everything you wish it to.',
|
||||
resources: [
|
||||
'https://developers.google.com/search/docs/crawling-indexing/sitemaps/overview',
|
||||
'https://www.sitemaps.org/protocol.html',
|
||||
'https://www.conductor.com/academy/xml-sitemap/',
|
||||
{ title: 'Learn about Sitemaps', link: 'https://developers.google.com/search/docs/crawling-indexing/sitemaps/overview'},
|
||||
{ title: 'Sitemap XML spec', link: 'https://www.sitemaps.org/protocol.html'},
|
||||
{ title: 'Sitemap tutorial', link: 'https://www.conductor.com/academy/xml-sitemap/'},
|
||||
],
|
||||
screenshot: 'https://i.ibb.co/GtrCQYq/Screenshot-from-2023-07-21-12-28-38.png',
|
||||
},
|
||||
@ -362,6 +350,174 @@ const docs: Doc[] = [
|
||||
],
|
||||
screenshot: 'https://i.ibb.co/tq1FT5r/Screenshot-from-2023-07-24-20-31-21.png',
|
||||
},
|
||||
{
|
||||
id: 'linked-pages',
|
||||
title: 'Linked Pages',
|
||||
description: 'Displays all internal and external links found on a site, identified by the href attributes attached to anchor elements.',
|
||||
use: "For site owners, this is useful for diagnosing SEO issues, improving the site structure, understanding how content is inter-connected. External links can show partnerships, dependencies, and potential reputation risks. " +
|
||||
"From a security standpoint, the outbound links can help identify any potential malicious or compromised sites the website is unknowingly linking to. Analyzing internal links can aid in understanding the site's structure and potentially uncover hidden or vulnerable pages which are not intended to be public. " +
|
||||
"And for an OSINT investigator, it can aid in building a comprehensive understanding of the target, uncovering related entities, resources, or even potential hidden parts of the site.",
|
||||
resources: [
|
||||
{ title: 'W3C Link Checker', link: 'https://validator.w3.org/checklink'},
|
||||
],
|
||||
screenshot: 'https://i.ibb.co/LtK14XR/Screenshot-from-2023-07-29-11-16-44.png',
|
||||
},
|
||||
{
|
||||
id: 'social-tags',
|
||||
title: 'Social Tags',
|
||||
description: 'Websites can include certain meta tags, that tell search engines and social media platforms what info to display. This usually includes a title, description, thumbnail, keywords, author, social accounts, etc.',
|
||||
use: 'Adding this data to your site will boost SEO, and as an OSINT researcher it can be useful to understand how a given web app describes itself',
|
||||
resources: [
|
||||
{ title: 'SocialSharePreview.com', link: 'https://socialsharepreview.com/'},
|
||||
{ title: 'The guide to social meta tags', link: 'https://css-tricks.com/essential-meta-tags-social-media/'},
|
||||
{ title: 'Web.dev metadata tags', link: 'https://web.dev/learn/html/metadata/'},
|
||||
{ title: 'Open Graph Protocol', link: 'https://ogp.me/'},
|
||||
{ title: 'Twitter Cards', link: 'https://developer.twitter.com/en/docs/twitter-for-websites/cards/overview/abouts-cards'},
|
||||
{ title: 'Facebook Open Graph', link: 'https://developers.facebook.com/docs/sharing/webmasters'},
|
||||
],
|
||||
screenshot: 'https://i.ibb.co/4srTT1w/Screenshot-from-2023-07-29-11-15-27.png',
|
||||
},
|
||||
{
|
||||
id: 'mail-config',
|
||||
title: 'Email Configuration',
|
||||
description: "DMARC (Domain-based Message Authentication, Reporting & Conformance): DMARC is an email authentication protocol that works with SPF and DKIM to prevent email spoofing and phishing. It allows domain owners to specify how to handle unauthenticated mail via a published policy in DNS, and provides a way for receiving mail servers to send feedback about emails' compliance to the sender. " +
|
||||
"BIMI (Brand Indicators for Message Identification): BIMI is an emerging email standard that enables organizations to display a logo in their customers' email clients automatically. BIMI ties the logo to the domain's DMARC record, providing another level of visual assurance to recipients that the email is legitimate. " +
|
||||
"DKIM (DomainKeys Identified Mail): DKIM is an email security standard designed to make sure that messages were not altered in transit between the sending and recipient servers. It uses digital signatures linked to the domain of the sender to verify the sender and ensure message integrity. " +
|
||||
"SPF (Sender Policy Framework): SPF is an email authentication method designed to prevent email spoofing. It specifies which mail servers are authorized to send email on behalf of a domain by creating a DNS record. This helps protect against spam by providing a way for receiving mail servers to check that incoming mail from a domain comes from a host authorized by that domain's administrators.",
|
||||
use: "This information is helpful for researchers as it helps assess a domain's email security posture, uncover potential vulnerabilities, and verify the legitimacy of emails for phishing detection. These details can also provide insight into the hosting environment, potential service providers, and the configuration patterns of a target organization, assisting in investigative efforts.",
|
||||
resources: [
|
||||
{ title: 'Intro to DMARC, DKIM, and SPF (via Cloudflare)', link: 'https://www.cloudflare.com/learning/email-security/dmarc-dkim-spf/' },
|
||||
{ title: 'EasyDMARC Domain Scanner', link: 'https://easydmarc.com/tools/domain-scanner' },
|
||||
{ title: 'MX Toolbox', link: 'https://mxtoolbox.com/' },
|
||||
{ title: 'RFC-7208 - SPF', link: 'https://datatracker.ietf.org/doc/html/rfc7208' },
|
||||
{ title: 'RFC-6376 - DKIM', link: 'https://datatracker.ietf.org/doc/html/rfc6376' },
|
||||
{ title: 'RFC-7489 - DMARC', link: 'https://datatracker.ietf.org/doc/html/rfc7489' },
|
||||
{ title: 'BIMI Group', link: 'https://bimigroup.org/' },
|
||||
],
|
||||
screenshot: 'https://i.ibb.co/yqhwx5G/Screenshot-from-2023-07-29-18-22-20.png',
|
||||
},
|
||||
{
|
||||
id: 'firewall',
|
||||
title: 'Firewall Detection',
|
||||
description: 'A WAF or web application firewall helps protect web applications by filtering and monitoring HTTP traffic between a web application and the Internet. It typically protects web applications from attacks such as cross-site forgery, cross-site-scripting (XSS), file inclusion, and SQL injection, among others.',
|
||||
use: 'It\'s useful to understand if a site is using a WAF, and which firewall software / service it is using, as this provides an insight into the sites protection against several attack vectors, but also may reveal vulnerabilities in the firewall itself.',
|
||||
resources: [
|
||||
{ title: 'What is a WAF (via Cloudflare Learning)', link: 'https://www.cloudflare.com/learning/ddos/glossary/web-application-firewall-waf/' },
|
||||
{ title: 'OWASP - Web Application Firewalls', link: 'https://owasp.org/www-community/Web_Application_Firewall' },
|
||||
{ title: 'Web Application Firewall Best Practices', link: 'https://owasp.org/www-pdf-archive/Best_Practices_Guide_WAF_v104.en.pdf' },
|
||||
{ title: 'WAF - Wiki', link: 'https://en.wikipedia.org/wiki/Web_application_firewall' },
|
||||
],
|
||||
screenshot: 'https://i.ibb.co/MfcxQt2/Screenshot-from-2023-08-12-15-40-52.png',
|
||||
},
|
||||
{
|
||||
id: 'http-security',
|
||||
title: 'HTTP Security Features',
|
||||
description: 'Correctly configured security HTTP headers adds a layer of protection against common attacks to your site. The main headers to be aware of are: '
|
||||
+ 'HTTP Strict Transport Security (HSTS): Enforces the use of HTTPS, mitigating man-in-the-middle attacks and protocol downgrade attempts. '
|
||||
+ 'Content Security Policy (CSP): Constrains web page resources to prevent cross-site scripting and data injection attacks. '
|
||||
+ 'X-Content-Type-Options: Prevents browsers from MIME-sniffing a response away from the declared content type, curbing MIME-type confusion attacks. '
|
||||
+ 'X-Frame-Options: Protects users from clickjacking attacks by controlling whether a browser should render the page in a <frame>, <iframe>, <embed>, or <object>. ',
|
||||
use: 'Reviewing security headers is important, as it offers insights into a site\'s defensive posture and potential vulnerabilities, enabling proactive mitigation and ensuring compliance with security best practices.',
|
||||
resources: [
|
||||
{ title: 'OWASP Secure Headers Project', link: 'https://owasp.org/www-project-secure-headers/'},
|
||||
{ title: 'HTTP Header Cheatsheet', link: 'https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html' },
|
||||
{ title: 'content-security-policy.com', link: 'https://content-security-policy.com/' },
|
||||
{ title: 'resourcepolicy.fyi', link: 'https://resourcepolicy.fyi/' },
|
||||
{ title: 'HTTP Security Headers', link: 'https://securityheaders.com/' },
|
||||
{ title: 'Mozilla Observatory', link: 'https://observatory.mozilla.org/' },
|
||||
{ title: 'CSP Docs', link: 'https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP' },
|
||||
{ title: 'HSTS Docs', link: 'https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security' },
|
||||
{ title: 'X-Content-Type-Options Docs', link: 'https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options' },
|
||||
{ title: 'X-Frame-Options Docs', link: 'https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options' },
|
||||
{ title: 'X-XSS-Protection Docs', link: 'https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-XSS-Protection' },
|
||||
],
|
||||
screenshot: 'https://i.ibb.co/LP05HMV/Screenshot-from-2023-08-12-15-40-28.png',
|
||||
},
|
||||
{
|
||||
id: 'archives',
|
||||
title: 'Archive History',
|
||||
description: 'Fetches full history of archives from the Wayback machine',
|
||||
use: 'This is useful for understanding the history of a site, and how it has changed over time. It can also be useful for finding old versions of a site, or for finding content that has been removed.',
|
||||
resources: [
|
||||
{ title: 'Wayback Machine', link: 'https://archive.org/web/'},
|
||||
],
|
||||
screenshot: 'https://i.ibb.co/nB9szT1/Screenshot-from-2023-08-14-22-31-16.png',
|
||||
},
|
||||
{
|
||||
id: 'rank',
|
||||
title: 'Global Ranking',
|
||||
description: 'This check shows the global rank of the requested site. This is only accurate for websites which are in the top 100 million list. We\'re using data from the Tranco project (see below), which collates the top sites on the web from Umbrella, Majestic, Quantcast, the Chrome User Experience Report and Cloudflare Radar.',
|
||||
use: 'Knowing a websites overall global rank can be useful for understanding the scale of the site, and for comparing it to other sites. It can also be useful for understanding the relative popularity of a site, and for identifying potential trends.',
|
||||
resources: [
|
||||
{ title: 'Tranco List', link: 'https://tranco-list.eu/' },
|
||||
{ title: 'Tranco Research Paper', link: 'https://tranco-list.eu/assets/tranco-ndss19.pdf'},
|
||||
],
|
||||
screenshot: 'https://i.ibb.co/nkbczgb/Screenshot-from-2023-08-14-22-02-40.png',
|
||||
},
|
||||
{
|
||||
id: 'block-lists',
|
||||
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: [
|
||||
{ title: 'ThreatJammer Lists', link: 'https://threatjammer.com/osint-lists'},
|
||||
],
|
||||
screenshot: 'https://i.ibb.co/M5JSXbW/Screenshot-from-2023-08-26-12-12-43.png',
|
||||
},
|
||||
{
|
||||
id: 'threats',
|
||||
title: 'Malware & Phishing Detection',
|
||||
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: 'https://i.ibb.co/hYgy621/Screenshot-from-2023-08-26-12-07-47.png',
|
||||
},
|
||||
{
|
||||
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: '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: '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 = [
|
||||
'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 = [
|
||||
@ -369,24 +525,14 @@ export const about = [
|
||||
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>
|
||||
@ -409,10 +555,19 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
`;
|
||||
|
||||
export const supportUs = [
|
||||
"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 :)",
|
||||
];
|
||||
|
||||
export const fairUse = [
|
||||
'Please use this tool responsibly. Do not use it for hosts you do not have permission to scan. Do not use it to attack or disrupt services.',
|
||||
'Requests are rate-limited to prevent abuse. If you need to make more bandwidth, please deploy your own instance.',
|
||||
'The hosted instance is only for demo use, as excessive use will quickly deplete my lambda function credits, making it unavailable for others and/or costing me money.',
|
||||
'Please use this tool responsibly. Do not use it for hosts you do not have permission to scan. Do not use it as part of a scheme to attack or disrupt services.',
|
||||
'Requests may be rate-limited to prevent abuse. If you need to make more bandwidth, please deploy your own instance.',
|
||||
'There is no guarantee of uptime or availability. If you need to make sure the service is available, please deploy your own instance.',
|
||||
'Please use fairly, as excessive use will quickly deplete the lambda function credits, making the service unavailable for others (and/or empty my bank account!).',
|
||||
];
|
||||
|
||||
export default docs;
|
||||
|
@ -140,25 +140,6 @@ export type Cookie = {
|
||||
attributes: Record<string, string>;
|
||||
};
|
||||
|
||||
export const parseCookies = (cookiesHeader: string[]): {cookies: Cookie[]} => {
|
||||
if (!cookiesHeader || !cookiesHeader.length) return {cookies: []};
|
||||
|
||||
const cookies = cookiesHeader.flatMap(cookieHeader => {
|
||||
return cookieHeader.split(/,(?=\s[A-Za-z0-9]+=)/).map(cookieString => {
|
||||
const [nameValuePair, ...attributePairs] = cookieString.split('; ').map(part => part.trim());
|
||||
const [name, value] = nameValuePair.split('=');
|
||||
const attributes: Record<string, string> = {};
|
||||
attributePairs.forEach(pair => {
|
||||
const [attributeName, attributeValue = ''] = pair.split('=');
|
||||
attributes[attributeName] = attributeValue;
|
||||
});
|
||||
return { name, value, attributes };
|
||||
});
|
||||
});
|
||||
|
||||
return { cookies };
|
||||
};
|
||||
|
||||
export const parseRobotsTxt = (content: string): { robots: RowProps[] } => {
|
||||
const lines = content.split('\n');
|
||||
const rules: RowProps[] = [];
|
||||
|
Reference in New Issue
Block a user