Migrate to Astro base

This commit is contained in:
Alicia Sykes
2024-05-05 17:39:47 +01:00
parent 7234e11e87
commit 45bf452f17
91 changed files with 432 additions and 431 deletions

View File

@@ -1,7 +0,0 @@
interface Props {
message: string,
};
const Demo = ({ message }: Props): JSX.Element => <div>{message}</div>;
export default Demo;

View File

@@ -1,83 +0,0 @@
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>;
size?: InputSize,
bgColor?: string,
fgColor?: string,
styles?: string,
title?: string,
loadState?: LoadState,
};
const StyledButton = styled.button<ButtonProps>`
cursor: pointer;
border: none;
border-radius: 0.25rem;
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};
}
&:active {
box-shadow: -3px -3px 0px ${colors.fgShadowColor};
}
${props => applySize(props.size)};
${(props) => props.bgColor ?
`background: ${props.bgColor};` : `background: ${colors.primary};`
}
${(props) => props.fgColor ?
`color: ${props.fgColor};` : `color: ${colors.background};`
}
${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, loadState } = props;
return (
<StyledButton
onClick={onClick || (() => null) }
size={size}
bgColor={bgColor}
fgColor={fgColor}
styles={styles}
title={title?.toString()}
>
{ loadState && <Loader loadState={loadState} /> }
{children}
</StyledButton>
);
};
export default Button;

View File

@@ -1,40 +0,0 @@
import styled from 'styled-components';
import ErrorBoundary from 'components/misc/ErrorBoundary';
import Heading from 'components/Form/Heading';
import colors from 'styles/colors';
import { ReactNode } from 'react';
export const StyledCard = styled.section<{ styles?: string}>`
background: ${colors.backgroundLighter};
box-shadow: 4px 4px 0px ${colors.bgShadowColor};
border-radius: 8px;
padding: 1rem;
position: relative;
margin 0.5rem;
max-height: 64rem;
overflow: auto;
${props => props.styles}
`;
interface CardProps {
children: React.ReactNode;
heading?: string,
styles?: string;
actionButtons?: ReactNode | undefined;
};
export const Card = (props: CardProps): JSX.Element => {
const { children, heading, styles, actionButtons } = props;
return (
<ErrorBoundary title={heading}>
<StyledCard styles={styles}>
{ actionButtons && actionButtons }
{ heading && <Heading className="inner-heading" as="h3" align="left" color={colors.primary}>{heading}</Heading> }
{children}
</StyledCard>
</ErrorBoundary>
);
}
export default StyledCard;

View File

@@ -1,66 +0,0 @@
import styled from 'styled-components';
import colors from 'styles/colors';
import { TextSizes } from 'styles/typography';
interface HeadingProps {
as?: 'h1' | 'h2' | 'h3' | 'h4' | 'p';
align?: 'left' | 'center' | 'right';
color?: string;
size?: 'xSmall' | 'small' | 'medium' | 'large' | 'xLarge';
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};
img { // Some titles have an icon
width: 2.5rem;
border-radius: 4px;
}
a { // If a title is a link, keep title styles
color: inherit;
text-decoration: none;
display: flex;
}
${props => {
switch (props.size) {
case 'xSmall': return `font-size: ${TextSizes.xSmall};`;
case 'small': return `font-size: ${TextSizes.small};`;
case 'medium': return `font-size: ${TextSizes.large};`;
case 'large': return `font-size: ${TextSizes.xLarge};`;
case 'xLarge': return `font-size: ${TextSizes.xLarge};`;
}
}};
${props => {
switch (props.align) {
case 'left': return 'text-align: left;';
case 'right': return 'text-align: right;';
case 'center': return 'text-align: center; justify-content: center;';
}
}};
${props => props.color ? `color: ${props.color};` : '' }
${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, className } = props;
return (
<StyledHeading as={as} size={size} align={align} color={color} inline={inline} className={className} id={id || makeAnchor((children || '')?.toString())}>
{children}
</StyledHeading>
);
}
export default Heading;

View File

@@ -1,70 +0,0 @@
import { InputHTMLAttributes } from 'react';
import styled from 'styled-components';
import colors from 'styles/colors';
import { InputSize, applySize } from 'styles/dimensions';
type Orientation = 'horizontal' | 'vertical';
interface Props {
id: string,
value: string,
label?: string,
placeholder?: string,
disabled?: boolean,
size?: InputSize,
orientation?: Orientation;
handleChange: (nweVal: React.ChangeEvent<HTMLInputElement>) => void,
};
type SupportedElements = HTMLInputElement | HTMLLabelElement | HTMLDivElement;
interface StyledInputTypes extends InputHTMLAttributes<SupportedElements> {
inputSize?: InputSize;
orientation?: Orientation;
};
const InputContainer = styled.div<StyledInputTypes>`
display: flex;
${props => props.orientation === 'vertical' ? 'flex-direction: column;' : ''};
`;
const StyledInput = styled.input<StyledInputTypes>`
background: ${colors.background};
color: ${colors.textColor};
border: none;
border-radius: 0.25rem;
font-family: PTMono;
box-shadow: 3px 3px 0px ${colors.backgroundDarker};
&:focus {
outline: 1px solid ${colors.primary}
}
${props => applySize(props.inputSize)};
`;
const StyledLabel = styled.label<StyledInputTypes>`
color: ${colors.textColor};
${props => applySize(props.inputSize)};
padding: 0;
font-size: 1.6rem;
`;
const Input = (inputProps: Props): JSX.Element => {
const { id, value, label, placeholder, disabled, size, orientation, handleChange } = inputProps;
return (
<InputContainer orientation={orientation}>
{ label && <StyledLabel htmlFor={id} inputSize={size}>{ label }</StyledLabel> }
<StyledInput
id={id}
value={value}
placeholder={placeholder}
disabled={disabled}
onChange={handleChange}
inputSize={size}
/>
</InputContainer>
);
};
export default Input;

View File

@@ -1,91 +0,0 @@
import React from 'react';
import ReactDOM from 'react-dom';
import styled from 'styled-components';
import colors from 'styles/colors';
import Button from 'components/Form/Button';
interface ModalProps {
children: React.ReactNode;
isOpen: boolean;
closeModal: () => void;
}
const Overlay = styled.div`
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
display: flex;
justify-content: center;
align-items: center;
background-color: rgba(0, 0, 0, 0.5);
animation: fadeIn 0.5s;
@keyframes fadeIn {
0% {opacity: 0;}
100% {opacity: 1;}
}
`;
const ModalWindow = styled.div`
width: 80%;
max-width: 500px;
background: ${colors.backgroundLighter};
padding: 2rem;
border-radius: 12px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
position: relative;
animation: appear 0.5s;
color: ${colors.textColor};
box-shadow: 4px 4px 0px ${colors.bgShadowColor};
max-height: 80%;
overflow-y: auto;
@keyframes appear {
0% {opacity: 0; transform: scale(0.9);}
100% {opacity: 1; transform: scale(1);}
}
pre {
white-space: break-spaces;
}
`;
const Modal: React.FC<ModalProps> = ({ children, isOpen, closeModal }) => {
const handleOverlayClick = (e: React.MouseEvent<HTMLElement>) => {
if (e.target === e.currentTarget) {
closeModal();
}
};
React.useEffect(() => {
const handleEscPress = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
closeModal();
}
};
if (isOpen) {
window.addEventListener('keydown', handleEscPress);
}
return () => {
window.removeEventListener('keydown', handleEscPress);
};
}, [isOpen, closeModal]);
if (!isOpen) {
return null;
}
return ReactDOM.createPortal(
<Overlay onClick={handleOverlayClick}>
<ModalWindow>
{children}
<Button onClick={closeModal} styles="width: fit-content;float: right;">Close</Button>
</ModalWindow>
</Overlay>,
document.body,
);
};
export default Modal;

View File

@@ -1,31 +0,0 @@
import styled from 'styled-components';
import { StyledCard } from 'components/Form/Card';
import Heading from 'components/Form/Heading';
import colors from 'styles/colors';
import { ReactNode } from 'react';
const Header = styled(StyledCard)`
margin: 1rem auto;
display: flex;
flex-wrap: wrap;
align-items: baseline;
justify-content: space-between;
padding: 0.5rem 1rem;
align-items: center;
width: 95vw;
`;
const Nav = (props: { children?: ReactNode}) => {
return (
<Header as="header">
<Heading color={colors.primary} size="large">
<img width="64" src="/web-check.png" alt="Web Check Icon" />
<a href="/">Web Check</a>
</Heading>
{props.children && props.children}
</Header>
);
};
export default Nav;

View File

@@ -1,216 +0,0 @@
import { ReactNode } from 'react';
import styled from 'styled-components';
import colors from 'styles/colors';
import Heading from 'components/Form/Heading';
export interface RowProps {
lbl: string,
val: 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; }
span.val {
max-width: 16rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
a {
color: ${colors.primary};
}
}
`;
const StyledExpandableRow = styled(StyledRow).attrs({
as: "summary"
})``;
export const Details = styled.details`
transition: all 0.2s ease-in-out;
summary {
padding-left: 1rem;
cursor: pointer;
}
summary:before {
content: "►";
position: absolute;
margin-left: -1rem;
color: ${colors.primary};
cursor: pointer;
}
&[open] summary:before {
content: "▼";
}
`;
const SubRowList = styled.ul`
margin: 0;
padding: 0.25rem;
background: ${colors.primaryTransparent};
`;
const SubRow = styled(StyledRow).attrs({
as: "li"
})`
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 => {
return date >= new Date('1995-01-01') && date <= new Date('2030-12-31');
};
// Check if input is a timestamp
if (typeof date === 'number') {
const timestampDate = new Date(date);
return !isNaN(timestampDate.getTime()) && isInRange(timestampDate);
}
// Check if input is a date string
if (typeof date === 'string') {
const dateStringDate = new Date(date);
return !isNaN(dateStringDate.getTime()) && isInRange(dateStringDate);
}
// Check if input is a Date object
if (date instanceof Date) {
return !isNaN(date.getTime()) && isInRange(date);
}
return false;
};
const formatDate = (dateString: string): string => {
return new Intl.DateTimeFormat('en-GB', {
day: 'numeric', month: 'long', year: 'numeric'
}).format(new Date(dateString));
}
const formatValue = (value: any): string => {
if (isValidDate(new Date(value))) return formatDate(value);
if (typeof value === 'boolean') return value ? '✅' : '❌';
return value;
};
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 (
<Details open={open}>
<StyledExpandableRow key={`${lbl}-${val}`}>
<span className="lbl" title={title?.toString()}>{lbl}</span>
<span className="val" title={val?.toString()}>{val.toString()}</span>
</StyledExpandableRow>
{ rowList &&
<SubRowList>
{ rowList?.map((row: RowProps, index: number) => {
return (
<SubRow key={`${row.lbl}-${index}`}>
<span className="lbl" title={row.title?.toString()}>{row.lbl}</span>
<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>
)
})}
</SubRowList>
}
</Details>
);
};
export const ListRow = (props: { list: string[], title: string }) => {
const { list, title } = props;
return (
<>
<Heading as="h4" size="small" align="left" color={colors.primary}>{title}</Heading>
{ list.map((entry: string, index: number) => {
return (
<Row lbl="" val="" key={`${entry}-${title.toLocaleLowerCase()}-${index}`}>
<span>{ entry }</span>
</Row>
)}
)}
</>
);
}
const Row = (props: RowProps) => {
const { lbl, val, title, plaintext, listResults, children } = props;
if (children) return <StyledRow key={`${lbl}-${val}`}>{children}</StyledRow>;
return (
<StyledRow key={`${lbl}-${val}`}>
{ lbl && <span className="lbl" title={title?.toString()}>{lbl}</span> }
<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>
);
};
export default Row;

View File

@@ -1,37 +0,0 @@
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;

View File

@@ -1,21 +0,0 @@
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, blockIndex: number) => (
<Row
title={blocklist.serverIp}
lbl={blocklist.server}
val={blocklist.isBlocked ? '❌ Blocked' : '✅ Not Blocked'}
key={`blocklist-${blockIndex}-${blocklist.serverIp}`}
/>
))}
</Card>
);
}
export default BlockListsCard;

View File

@@ -1,58 +0,0 @@
import styled from 'styled-components';
import { TechnologyGroup, Technology } from 'utils/result-processor';
import colors from 'styles/colors';
import Card from 'components/Form/Card';
import Heading from 'components/Form/Heading';
const Outer = styled(Card)`
grid-row: span 2
`;
const Row = styled.div`
display: flex;
justify-content: space-between;
padding: 0.25rem;
&:not(:last-child) { border-bottom: 1px solid ${colors.primary}; }
span.lbl { font-weight: bold; }
span.val {
max-width: 200px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
`;
const ListRow = (props: { list: Technology[], title: string }) => {
const { list, title } = props;
return (
<>
<Heading as="h3" align="left" color={colors.primary}>{title}</Heading>
{ list.map((entry: Technology, index: number) => {
return (
<Row key={`${title.toLocaleLowerCase()}-${index}`}><span>{ entry.Name }</span></Row>
)}
)}
</>
);
}
const BuiltWithCard = (props: { data: TechnologyGroup[]}): JSX.Element => {
// const { created, updated, expires, nameservers } = whois;
return (
<Outer>
<Heading as="h3" align="left" color={colors.primary}>Technologies</Heading>
{ props.data.map((group: TechnologyGroup) => {
return (
<ListRow key={group.tag} title={group.tag} list={group.technologies} />
);
})}
{/* { created && <DataRow lbl="Created" val={formatDate(created)} /> }
{ updated && <DataRow lbl="Updated" val={formatDate(updated)} /> }
{ expires && <DataRow lbl="Expires" val={formatDate(expires)} /> }
{ nameservers && <ListRow title="Name Servers" list={nameservers} /> } */}
</Outer>
);
}
export default BuiltWithCard;

View File

@@ -1,49 +0,0 @@
import { useEffect, useState } from 'react';
import styled from 'styled-components';
import { Card } from 'components/Form/Card';
import Row from 'components/Form/Row';
import colors from 'styles/colors';
const LearnMoreInfo = styled.p`
font-size: 0.8rem;
margin-top: 0.5rem;
opacity: 0.75;
a { color: ${colors.primary}; }
`;
const CarbonCard = (props: { data: any, title: string, actionButtons: any }): JSX.Element => {
const carbons = props.data.statistics;
const initialUrl = props.data.scanUrl;
const [carbonData, setCarbonData] = useState<{c?: number, p?: number}>({});
useEffect(() => {
const fetchCarbonData = async () => {
try {
const response = await fetch(`https://api.websitecarbon.com/b?url=${encodeURIComponent(initialUrl)}`);
const data = await response.json();
setCarbonData(data);
} catch (error) {
console.error('Error fetching carbon data:', error);
}
};
fetchCarbonData();
}, [initialUrl]);
return (
<Card heading={props.title} actionButtons={props.actionButtons}>
{ (!carbons?.adjustedBytes && !carbonData.c) && <p>Unable to calculate carbon footprint for host</p>}
{ carbons?.adjustedBytes > 0 && <>
<Row lbl="HTML Initial Size" val={`${carbons.adjustedBytes} bytes`} />
<Row lbl="CO2 for Initial Load" val={`${(carbons.co2.grid.grams * 1000).toPrecision(4)} grams`} />
<Row lbl="Energy Usage for Load" val={`${(carbons.energy * 1000).toPrecision(4)} KWg`} />
</>}
{carbonData.c && <Row lbl="CO2 Emitted" val={`${carbonData.c} grams`} />}
{carbonData.p && <Row lbl="Better than average site by" val={`${carbonData.p}%`} />}
<br />
<LearnMoreInfo>Learn more at <a href="https://www.websitecarbon.com/">websitecarbon.com</a></LearnMoreInfo>
</Card>
);
}
export default CarbonCard;

View File

@@ -1,77 +0,0 @@
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 = `
small { margin-top: 1rem; opacity: 0.5; }
a {
color: ${colors.textColor};
}
details {
// display: inline;
display: flex;
transition: all 0.2s ease-in-out;
h3 {
display: inline;
}
summary {
padding: 0;
margin: 1rem 0 0 0;
cursor: pointer;
}
summary:before {
content: "►";
position: absolute;
margin-left: -1rem;
color: ${colors.primary};
cursor: pointer;
}
&[open] summary:before {
content: "▼";
}
}
`;
const getPathName = (link: string) => {
try {
const url = new URL(link);
return url.pathname;
} catch(e) {
return link;
}
};
const ContentLinksCard = (props: { data: any, title: string, actionButtons: any }): JSX.Element => {
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>
<Row lbl="Internal Link Count" val={internal.length} />
<Row lbl="External Link Count" val={external.length} />
{ internal && internal.length > 0 && (
<details>
<summary><Heading as="h3" size="small" color={colors.primary}>Internal Links</Heading></summary>
{internal.map((link: string) => (
<Row key={link} lbl="" val="">
<a href={link} target="_blank" rel="noreferrer">{getPathName(link)}</a>
</Row>
))}
</details>
)}
{ external && external.length > 0 && (
<details>
<summary><Heading as="h3" size="small" color={colors.primary}>External Links</Heading></summary>
{external.map((link: string) => (
<Row key={link} lbl="" val="">
<a href={link} target="_blank" rel="noreferrer">{link}</a>
</Row>
))}
</details>
)}
</Card>
);
}
export default ContentLinksCard;

View File

@@ -1,49 +0,0 @@
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 headerCookies = parseHeaderCookies(props.data.headerCookies) || [];
const clientCookies = props.data.clientCookies || [];
return (
<Card heading={props.title} actionButtons={props.actionButtons}>
{
headerCookies.map((cookie: any, index: number) => {
const attributes = Object.keys(cookie.attributes).map((key: string) => {
return { lbl: key, val: cookie.attributes[key] }
});
return (
<ExpandableRow key={`cookie-${index}`} lbl={cookie.name} val={cookie.value} rowList={attributes} />
)
})
}
{
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>
);
}
export default CookiesCard;

View File

@@ -1,30 +0,0 @@
import { Card } from 'components/Form/Card';
import Row, { ListRow } from 'components/Form/Row';
const styles = `
grid-row: span 2;
.content {
max-height: 50rem;
overflow-x: hidden;
overflow-y: auto;
}
`;
const DnsRecordsCard = (props: { data: any, title: string, actionButtons: any }): JSX.Element => {
const dnsRecords = props.data;
return (
<Card heading={props.title} actionButtons={props.actionButtons} styles={styles}>
<div className="content">
{ dnsRecords.A && <Row lbl="A" val={dnsRecords.A.address} /> }
{ dnsRecords.AAAA?.length > 0 && <ListRow title="AAAA" list={dnsRecords.AAAA} /> }
{ dnsRecords.MX?.length > 0 && <ListRow title="MX" list={dnsRecords.MX} /> }
{ dnsRecords.CNAME?.length > 0 && <ListRow title="CNAME" list={dnsRecords.CNAME} /> }
{ dnsRecords.NS?.length > 0 && <ListRow title="NS" list={dnsRecords.NS} /> }
{ dnsRecords.PTR?.length > 0 && <ListRow title="PTR" list={dnsRecords.PTR} /> }
{ dnsRecords.SOA?.length > 0 && <ListRow title="SOA" list={dnsRecords.SOA} /> }
</div>
</Card>
);
}
export default DnsRecordsCard;

View File

@@ -1,210 +0,0 @@
import { Card } from 'components/Form/Card';
import Row, { ExpandableRow, RowProps } from 'components/Form/Row';
import Heading from 'components/Form/Heading';
import colors from 'styles/colors';
const parseDNSKeyData = (data: string) => {
const dnsKey = data.split(' ');
const flags = parseInt(dnsKey[0]);
const protocol = parseInt(dnsKey[1]);
const algorithm = parseInt(dnsKey[2]);
let flagMeaning = '';
let protocolMeaning = '';
let algorithmMeaning = '';
// Flags
if (flags === 256) {
flagMeaning = 'Zone Signing Key (ZSK)';
} else if (flags === 257) {
flagMeaning = 'Key Signing Key (KSK)';
} else {
flagMeaning = 'Unknown';
}
// Protocol
protocolMeaning = protocol === 3 ? 'DNSSEC' : 'Unknown';
// Algorithm
switch (algorithm) {
case 5:
algorithmMeaning = 'RSA/SHA-1';
break;
case 7:
algorithmMeaning = 'RSASHA1-NSEC3-SHA1';
break;
case 8:
algorithmMeaning = 'RSA/SHA-256';
break;
case 10:
algorithmMeaning = 'RSA/SHA-512';
break;
case 13:
algorithmMeaning = 'ECDSA Curve P-256 with SHA-256';
break;
case 14:
algorithmMeaning = 'ECDSA Curve P-384 with SHA-384';
break;
case 15:
algorithmMeaning = 'Ed25519';
break;
case 16:
algorithmMeaning = 'Ed448';
break;
default:
algorithmMeaning = 'Unknown';
break;
}
return {
flags: flagMeaning,
protocol: protocolMeaning,
algorithm: algorithmMeaning,
publicKey: dnsKey[3]
};
}
const getRecordTypeName = (typeCode: number): string => {
switch(typeCode) {
case 1: return 'A';
case 2: return 'NS';
case 5: return 'CNAME';
case 6: return 'SOA';
case 12: return 'PTR';
case 13: return 'HINFO';
case 15: return 'MX';
case 16: return 'TXT';
case 28: return 'AAAA';
case 33: return 'SRV';
case 35: return 'NAPTR';
case 39: return 'DNAME';
case 41: return 'OPT';
case 43: return 'DS';
case 46: return 'RRSIG';
case 47: return 'NSEC';
case 48: return 'DNSKEY';
case 50: return 'NSEC3';
case 51: return 'NSEC3PARAM';
case 52: return 'TLSA';
case 53: return 'SMIMEA';
case 55: return 'HIP';
case 56: return 'NINFO';
case 57: return 'RKEY';
case 58: return 'TALINK';
case 59: return 'CDS';
case 60: return 'CDNSKEY';
case 61: return 'OPENPGPKEY';
case 62: return 'CSYNC';
case 63: return 'ZONEMD';
default: return 'Unknown';
}
}
const parseDSData = (dsData: string) => {
const parts = dsData.split(' ');
const keyTag = parts[0];
const algorithm = getAlgorithmName(parseInt(parts[1], 10));
const digestType = getDigestTypeName(parseInt(parts[2], 10));
const digest = parts[3];
return {
keyTag,
algorithm,
digestType,
digest
};
}
const getAlgorithmName = (code: number) => {
switch(code) {
case 1: return 'RSA/MD5';
case 2: return 'Diffie-Hellman';
case 3: return 'DSA/SHA1';
case 5: return 'RSA/SHA1';
case 6: return 'DSA/NSEC3/SHA1';
case 7: return 'RSASHA1/NSEC3/SHA1';
case 8: return 'RSA/SHA256';
case 10: return 'RSA/SHA512';
case 12: return 'ECC/GOST';
case 13: return 'ECDSA/CurveP256/SHA256';
case 14: return 'ECDSA/CurveP384/SHA384';
case 15: return 'Ed25519';
case 16: return 'Ed448';
default: return 'Unknown';
}
}
const getDigestTypeName = (code: number) => {
switch(code) {
case 1: return 'SHA1';
case 2: return 'SHA256';
case 3: return 'GOST R 34.11-94';
case 4: return 'SHA384';
default: return 'Unknown';
}
}
const makeResponseList = (response: any): RowProps[] => {
const result = [] as RowProps[];
if (!response) return result;
if (typeof response.RD === 'boolean') result.push({ lbl: 'Recursion Desired (RD)', val: response.RD });
if (typeof response.RA === 'boolean') result.push({ lbl: 'Recursion Available (RA)', val: response.RA });
if (typeof response.TC === 'boolean') result.push({ lbl: 'TrunCation (TC)', val: response.TC });
if (typeof response.AD === 'boolean') result.push({ lbl: 'Authentic Data (AD)', val: response.AD });
if (typeof response.CD === 'boolean') result.push({ lbl: 'Checking Disabled (CD)', val: response.CD });
return result;
};
const makeAnswerList = (recordData: any): RowProps[] => {
return [
{ lbl: 'Domain', val: recordData.name },
{ lbl: 'Type', val: `${getRecordTypeName(recordData.type)} (${recordData.type})` },
{ lbl: 'TTL', val: recordData.TTL },
{ lbl: 'Algorithm', val: recordData.algorithm },
{ lbl: 'Flags', val: recordData.flags },
{ lbl: 'Protocol', val: recordData.protocol },
{ lbl: 'Public Key', val: recordData.publicKey },
{ lbl: 'Key Tag', val: recordData.keyTag },
{ lbl: 'Digest Type', val: recordData.digestType },
{ lbl: 'Digest', val: recordData.digest },
].filter((rowData) => rowData.val && rowData.val !== 'Unknown');
};
const DnsSecCard = (props: { data: any, title: string, actionButtons: any }): JSX.Element => {
const dnsSec = props.data;
return (
<Card heading={props.title} actionButtons={props.actionButtons}>
{
['DNSKEY', 'DS', 'RRSIG'].map((key: string, index: number) => {
const record = dnsSec[key];
return (<div key={`${key}-${index}`}>
<Heading as="h3" size="small" color={colors.primary}>{key}</Heading>
{(record.isFound && record.answer) && (<>
<Row lbl={`${key} - Present?`} val="✅ Yes" />
{
record.answer.map((answer: any, index: number) => {
const keyData = parseDNSKeyData(answer.data);
const dsData = parseDSData(answer.data);
const label = (keyData.flags && keyData.flags !== 'Unknown') ? keyData.flags : key;
return (
<ExpandableRow lbl={`Record #${index+1}`} val={label} rowList={makeAnswerList({ ...answer, ...keyData, ...dsData })} open />
);
})
}
</>)}
{(!record.isFound && record.response) && (
<ExpandableRow lbl={`${key} - Present?`} val={record.isFound ? '✅ Yes' : '❌ No'} rowList={makeResponseList(record.response)} />
)}
</div>)
})
}
</Card>
);
}
export default DnsSecCard;

View File

@@ -1,37 +0,0 @@
import { Card } from 'components/Form/Card';
import Heading from 'components/Form/Heading';
import Row from 'components/Form/Row';
import colors from 'styles/colors';
const cardStyles = `
small {
margin-top: 1rem;
opacity: 0.5;
display: block;
a { color: ${colors.primary}; }
}
`;
const DnsServerCard = (props: {data: any, title: string, actionButtons: any }): JSX.Element => {
const dnsSecurity = props.data;
return (
<Card heading={props.title} actionButtons={props.actionButtons} styles={cardStyles}>
{dnsSecurity.dns.map((dns: any, index: number) => {
return (<div key={`dns-${index}`}>
{ dnsSecurity.dns.length > 1 && <Heading as="h4" size="small" color={colors.primary}>DNS Server #{index+1}</Heading> }
<Row lbl="IP Address" val={dns.address} key={`ip-${index}`} />
{ dns.hostname && <Row lbl="Hostname" val={dns.hostname} key={`host-${index}`} /> }
<Row lbl="DoH Support" val={dns.dohDirectSupports ? '✅ Yes*' : '❌ No*'} key={`doh-${index}`} />
</div>);
})}
{dnsSecurity.dns.length > 0 && (<small>
* DoH Support is determined by the DNS server's response to a DoH query.
Sometimes this gives false negatives, and it's also possible that the DNS server supports DoH but does not respond to DoH queries.
If the DNS server does not support DoH, it may still be possible to use DoH by using a DoH proxy.
</small>)}
</Card>
);
}
export default DnsServerCard;

View File

@@ -1,32 +0,0 @@
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}; }
}
`;
const DomainLookupCard = (props: { data: any, title: string, actionButtons: any }): JSX.Element => {
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} /> }
{ domain.Creation_Date && <Row lbl="Creation Date" val={domain.Creation_Date} /> }
{ domain.Updated_Date && <Row lbl="Updated Date" val={domain.Updated_Date} /> }
{ domain.Registry_Expiry_Date && <Row lbl="Registry Expiry Date" val={domain.Registry_Expiry_Date} /> }
{ domain.Registry_Domain_ID && <Row lbl="Registry Domain ID" val={domain.Registry_Domain_ID} /> }
{ domain.Registrar_WHOIS_Server && <Row lbl="Registrar WHOIS Server" val={domain.Registrar_WHOIS_Server} /> }
{ domain.Registrar && <Row lbl="" val="">
<span className="lbl">Registrar</span>
<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} /> }
</Card>
);
}
export default DomainLookupCard;

View File

@@ -1,24 +0,0 @@
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;

View File

@@ -1,20 +0,0 @@
import { Card } from 'components/Form/Card';
import Row from 'components/Form/Row';
import { ReactNode } from 'react';
const HeadersCard = (props: { data: any, title: string, actionButtons: ReactNode }): JSX.Element => {
const headers = props.data;
return (
<Card heading={props.title} styles="grid-row: span 2;" actionButtons={props.actionButtons}>
{
Object.keys(headers).map((header: string, index: number) => {
return (
<Row key={`header-${index}`} lbl={header} val={headers[header]} />
)
})
}
</Card>
);
}
export default HeadersCard;

View File

@@ -1,49 +0,0 @@
import styled from 'styled-components';
import { HostNames } from 'utils/result-processor';
import colors from 'styles/colors';
import { Card } from 'components/Form/Card';
import Heading from 'components/Form/Heading';
const Row = styled.div`
display: flex;
justify-content: space-between;
padding: 0.25rem;
&:not(:last-child) { border-bottom: 1px solid ${colors.primary}; }
span:first-child { font-weight: bold; }
`;
const HostListSection = (props: { list: string[], title: string }) => {
const { list, title } = props;
return (
<>
<Heading as="h4" size="small" align="left" color={colors.primary}>{title}</Heading>
{ list.map((entry: string, index: number) => {
return (
<Row key={`${title.toLocaleLowerCase()}-${index}`}><span>{ entry }</span></Row>
)}
)}
</>
);
}
const cardStyles = `
max-height: 50rem;
overflow: auto;
`;
const HostNamesCard = (props: { data: HostNames, title: string, actionButtons: any }): JSX.Element => {
const hosts = props.data;
return (
<Card heading={props.title} actionButtons={props.actionButtons} styles={cardStyles}>
{ hosts.domains.length > 0 &&
<HostListSection list={hosts.domains} title="Domains" />
}
{ hosts.hostnames.length > 0 &&
<HostListSection list={hosts.hostnames} title="Hosts" />
}
</Card>
);
}
export default HostNamesCard;

View File

@@ -1,42 +0,0 @@
import { Card } from 'components/Form/Card';
import Row, { RowProps } from 'components/Form/Row';
const cardStyles = '';
const parseHeader = (headerString: string): RowProps[] => {
return headerString.split(';').map((part) => {
const trimmedPart = part.trim();
const equalsIndex = trimmedPart.indexOf('=');
if (equalsIndex >= 0) {
return {
lbl: trimmedPart.substring(0, equalsIndex).trim(),
val: trimmedPart.substring(equalsIndex + 1).trim(),
};
} else {
return { lbl: trimmedPart, val: 'true' };
}
});
};
const HstsCard = (props: {data: any, title: string, actionButtons: any }): JSX.Element => {
const hstsResults = props.data;
const hstsHeaders = hstsResults?.hstsHeader ? parseHeader(hstsResults.hstsHeader) : [];
return (
<Card heading={props.title} actionButtons={props.actionButtons} styles={cardStyles}>
{typeof hstsResults.compatible === 'boolean' && (
<Row lbl="HSTS Enabled?" val={hstsResults.compatible ? '✅ Yes' : '❌ No'} />
)}
{hstsHeaders.length > 0 && hstsHeaders.map((header: RowProps, index: number) => {
return (
<Row lbl={header.lbl} val={header.val} key={`hsts-${index}`} />
);
})
}
{hstsResults.message && (<p>{hstsResults.message}</p>)}
</Card>
);
}
export default HstsCard;

View File

@@ -1,17 +0,0 @@
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;

View File

@@ -1,52 +0,0 @@
import { Card } from 'components/Form/Card';
import { ExpandableRow } from 'components/Form/Row';
const processScore = (percentile: number) => {
return `${Math.round(percentile * 100)}%`;
}
interface Audit {
id: string,
score?: number | string,
scoreDisplayMode?: string,
title?: string,
description?: string,
displayValue?: string,
};
const makeValue = (audit: Audit) => {
let score = audit.score;
if (audit.displayValue) {
score = audit.displayValue;
} else if (audit.scoreDisplayMode) {
score = audit.score === 1 ? '✅ Pass' : '❌ Fail';
}
return score;
};
const LighthouseCard = (props: { data: any, title: string, actionButtons: any }): JSX.Element => {
const lighthouse = props.data;
const categories = lighthouse?.categories || {};
const audits = lighthouse?.audits || [];
return (
<Card heading={props.title} actionButtons={props.actionButtons}>
{ Object.keys(categories).map((title: string, index: number) => {
const scoreIds = categories[title].auditRefs.map((ref: { id: string }) => ref.id);
const scoreList = scoreIds.map((id: string) => {
return { lbl: audits[id].title, val: makeValue(audits[id]), title: audits[id].description, key: id }
})
return (
<ExpandableRow
key={`lighthouse-${index}`}
lbl={title}
val={processScore(categories[title].score)}
rowList={scoreList}
/>
);
}) }
</Card>
);
}
export default LighthouseCard;

View File

@@ -1,45 +0,0 @@
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;

View File

@@ -1,27 +0,0 @@
import { Card } from 'components/Form/Card';
import Row from 'components/Form/Row';
const cardStyles = `
small { margin-top: 1rem; opacity: 0.5; }
`;
const OpenPortsCard = (props: { data: any, title: string, actionButtons: any }): JSX.Element => {
const portData = props.data;
return (
<Card heading={props.title} actionButtons={props.actionButtons} styles={cardStyles}>
{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>
);
}
export default OpenPortsCard;

View File

@@ -1,77 +0,0 @@
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;

View File

@@ -1,42 +0,0 @@
import colors from 'styles/colors';
import { Card } from 'components/Form/Card';
import Row from 'components/Form/Row';
const cardStyles = `
div {
justify-content: flex-start;
align-items: baseline;
}
.arrow-thing {
color: ${colors.primary};
font-size: 1.8rem;
font-weight: bold;
margin-right: 0.5rem;
}
.redirect-count {
color: ${colors.textColorSecondary};
margin: 0;
}
`;
const RedirectsCard = (props: { data: any, title: string, actionButtons: any }): JSX.Element => {
const redirects = props.data;
return (
<Card heading={props.title} actionButtons={props.actionButtons} styles={cardStyles}>
{ !redirects?.redirects.length && <Row lbl="" val="No redirects" />}
<p className="redirect-count">
Followed {redirects.redirects.length}{' '}
redirect{redirects.redirects.length === 1 ? '' : 's'} when contacting host
</p>
{redirects.redirects.map((redirect: any, index: number) => {
return (
<Row lbl="" val="" key={index}>
<span className="arrow-thing"></span> {redirect}
</Row>
);
})}
</Card>
);
}
export default RedirectsCard;

View File

@@ -1,33 +0,0 @@
import { Card } from 'components/Form/Card';
import Row, { RowProps } from 'components/Form/Row';
const cardStyles = `
grid-row: span 2;
.content {
max-height: 50rem;
overflow-y: auto;
}
`;
const RobotsTxtCard = ( props: { data: { robots: RowProps[]}, title: string, actionButtons: any}): JSX.Element => {
const robots = props.data;
return (
<Card heading={props.title} actionButtons={props.actionButtons} styles={cardStyles}>
<div className="content">
{
robots.robots.length === 0 && <p>No crawl rules found.</p>
}
{
robots.robots.map((row: RowProps, index: number) => {
return (
<Row key={`${row.lbl}-${index}`} lbl={row.lbl} val={row.val} />
)
})
}
</div>
</Card>
);
}
export default RobotsTxtCard;

View File

@@ -1,24 +0,0 @@
import { Card } from 'components/Form/Card';
const cardStyles = `
overflow: auto;
max-height: 50rem;
grid-row: span 2;
img {
border-radius: 6px;
width: 100%;
margin 0.5rem 0;;
}
`;
const ScreenshotCard = (props: { data: { image?: string, data?: string, }, title: string, actionButtons: any }): JSX.Element => {
const screenshot = props.data;
return (
<Card heading={props.title} actionButtons={props.actionButtons} styles={cardStyles}>
{ screenshot.image && <img src={`data:image/png;base64,${screenshot.image}`} alt="Full page screenshot" /> }
{ (!screenshot.image && screenshot.data) && <img src={screenshot.data} alt="Full page screenshot" /> }
</Card>
);
}
export default ScreenshotCard;

View File

@@ -1,67 +0,0 @@
import { Card } from 'components/Form/Card';
import Row, { Details } from 'components/Form/Row';
import colors from 'styles/colors';
const cardStyles = `
small {
margin-top: 1rem;
opacity: 0.5;
display: block;
a { color: ${colors.primary}; }
}
summary {
padding: 0.5rem 0 0 0.5rem !important;
cursor: pointer;
font-weight: bold;
}
pre {
background: ${colors.background};
padding: 0.5rem 0.25rem;
border-radius: 4px;
overflow: auto;
}
`;
const getPagePath = (url: string): string => {
try {
return new URL(url).pathname;
} catch (error) {
return url;
}
}
const SecurityTxtCard = (props: {data: any, title: string, actionButtons: any }): JSX.Element => {
const securityTxt = props.data;
return (
<Card heading={props.title} actionButtons={props.actionButtons} styles={cardStyles}>
<Row lbl="Present" val={securityTxt.isPresent ? '✅ Yes' : '❌ No'} />
{ securityTxt.isPresent && (
<>
<Row lbl="File Location" val={securityTxt.foundIn} />
<Row lbl="PGP Signed" val={securityTxt.isPgpSigned ? '✅ Yes' : '❌ No'} />
{securityTxt.fields && Object.keys(securityTxt.fields).map((field: string, index: number) => {
if (securityTxt.fields[field].includes('http')) return (
<Row lbl="" val="" key={`policy-url-row-${index}`}>
<span className="lbl">{field}</span>
<span className="val"><a href={securityTxt.fields[field]}>{getPagePath(securityTxt.fields[field])}</a></span>
</Row>
);
return (
<Row lbl={field} val={securityTxt.fields[field]} key={`policy-row-${index}`} />
);
})}
<Details>
<summary>View Full Policy</summary>
<pre>{securityTxt.content}</pre>
</Details>
</>
)}
{!securityTxt.isPresent && (<small>
Having a security.txt ensures security researchers know how and where to safely report vulnerabilities.
</small>)}
</Card>
);
}
export default SecurityTxtCard;

View File

@@ -1,22 +0,0 @@
import { ServerInfo } from 'utils/result-processor';
import { Card } from 'components/Form/Card';
import Row from 'components/Form/Row';
const ServerInfoCard = (props: { data: ServerInfo, title: string, actionButtons: any }): JSX.Element => {
const info = props.data;
const { org, asn, isp, os, ports, ip, loc, type } = info;
return (
<Card heading={props.title} actionButtons={props.actionButtons}>
{ org && <Row lbl="Organization" val={org} /> }
{ (isp && isp !== org) && <Row lbl="Service Provider" val={isp} /> }
{ os && <Row lbl="Operating System" val={os} /> }
{ asn && <Row lbl="ASN Code" val={asn} /> }
{ ports && <Row lbl="Ports" val={ports} /> }
{ ip && <Row lbl="IP" val={ip} /> }
{ type && <Row lbl="Type" val={type} /> }
{ loc && <Row lbl="Location" val={loc} /> }
</Card>
);
}
export default ServerInfoCard;

View File

@@ -1,58 +0,0 @@
import styled from 'styled-components';
import { ServerLocation } from 'utils/result-processor';
import { Card } from 'components/Form/Card';
import LocationMap from 'components/misc/LocationMap';
import Flag from 'components/misc/Flag';
import { TextSizes } from 'styles/typography';
import Row, { StyledRow } from 'components/Form/Row';
const cardStyles = '';
const SmallText = styled.span`
opacity: 0.5;
font-size: ${TextSizes.xSmall};
text-align: right;
display: block;
`;
const MapRow = styled(StyledRow)`
padding-top: 1rem;
flex-direction: column;
`;
const CountryValue = styled.span`
display: flex;
gap: 0.5rem;
`;
const ServerLocationCard = (props: { data: ServerLocation, title: string, actionButtons: any }): JSX.Element => {
const location = props.data;
const {
city, region, country,
postCode, countryCode, coords,
isp, timezone, languages, currency, currencyCode,
} = location;
return (
<Card heading={props.title} actionButtons={props.actionButtons} styles={cardStyles}>
<Row lbl="City" val={`${postCode}, ${city}, ${region}`} />
<Row lbl="" val="">
<b>Country</b>
<CountryValue>
{country}
{ countryCode && <Flag countryCode={countryCode} width={28} /> }
</CountryValue>
</Row>
<Row lbl="Timezone" val={timezone} />
<Row lbl="Languages" val={languages} />
<Row lbl="Currency" val={`${currency} (${currencyCode})`} />
<MapRow>
<LocationMap lat={coords.latitude} lon={coords.longitude} label={`Server (${isp})`} />
<SmallText>Latitude: {coords.latitude}, Longitude: {coords.longitude} </SmallText>
</MapRow>
</Card>
);
}
export default ServerLocationCard;

View File

@@ -1,27 +0,0 @@
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}; }
}
`;
const ServerStatusCard = (props: { data: any, title: string, actionButtons: any }): JSX.Element => {
const serverStatus = props.data;
return (
<Card heading={props.title.toString()} actionButtons={props.actionButtons} styles={cardStyles}>
<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>
);
}
export default ServerStatusCard;

View File

@@ -1,65 +0,0 @@
import { Card } from 'components/Form/Card';
import colors from 'styles/colors';
import Row from 'components/Form/Row';
import Heading from 'components/Form/Heading';
const styles = `
.content {
max-height: 50rem;
overflow-y: auto;
}
.scan-date {
font-size: 0.8rem;
margin-top: 0.5rem;
opacity: 0.75;
}
`;
const formatDate = (timestamp: number): string => {
if (isNaN(timestamp) || timestamp <= 0) return 'No Date';
const date = new Date(timestamp * 1000);
if (isNaN(date.getTime())) return 'Unknown';
const formatter = new Intl.DateTimeFormat('en-GB', {
day: 'numeric',
month: 'long',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
hour12: true
});
return formatter.format(date);
}
const SiteFeaturesCard = (props: { data: any, title: string, actionButtons: any }): JSX.Element => {
const features = props.data;
return (
<Card heading={props.title} actionButtons={props.actionButtons} styles={styles}>
<div className="content">
{ (features?.groups || []).filter((group: any) => group.categories.length > 0).map((group: any, index: number) => (
<div key={`${group.name}-${index}`}>
<Heading as="h4" size="small" color={colors.primary}>{group.name}</Heading>
{ group.categories.map((category: any, subIndex: number) => (
// <Row lbl={category.name} val={category.live} />
<Row lbl="" val="" key={`${category.name}-${subIndex}`}>
<span className="lbl">{category.name}</span>
<span className="val">{category.live} Live {category.dead ? `(${category.dead} dead)` : ''}</span>
</Row>
))
}
</div>
))
}
</div>
<p className="scan-date">Last scanned on {formatDate(features.last)}</p>
</Card>
);
}
export default SiteFeaturesCard;

View File

@@ -1,60 +0,0 @@
import { Card } from 'components/Form/Card';
import Row, { ExpandableRow } from 'components/Form/Row';
import colors from 'styles/colors';
const cardStyles = `
max-height: 50rem;
overflow-y: auto;
a {
color: ${colors.primary};
}
small {
margin-top: 1rem;
opacity: 0.5;
display: block;
a { color: ${colors.primary}; }
}
`;
const SitemapCard = (props: {data: any, title: string, actionButtons: any }): JSX.Element => {
const normalSiteMap = props.data.url || props.data.urlset?.url || null;
const siteMapIndex = props.data.sitemapindex?.sitemap || null;
const makeExpandableRowData = (site: any) => {
const results = [];
if (site.lastmod) { results.push({lbl: 'Last Modified', val: site.lastmod[0]}); }
if (site.changefreq) { results.push({lbl: 'Change Frequency', val: site.changefreq[0]}); }
if (site.priority) { results.push({lbl: 'Priority', val: site.priority[0]}); }
return results;
};
const getPathFromUrl = (url: string) => {
try {
const urlObj = new URL(url);
return urlObj.pathname;
} catch (e) {
return url;
}
};
return (
<Card heading={props.title} actionButtons={props.actionButtons} styles={cardStyles}>
{
normalSiteMap && normalSiteMap.map((subpage: any, index: number) => {
return (<ExpandableRow lbl={getPathFromUrl(subpage.loc[0])} key={index} val="" rowList={makeExpandableRowData(subpage)}></ExpandableRow>)
})
}
{ siteMapIndex && <p>
This site returns a sitemap index, which is a list of sitemaps.
</p>}
{
siteMapIndex && siteMapIndex.map((subpage: any, index: number) => {
return (<Row lbl="" val="" key={index}><a href={subpage.loc[0]}>{getPathFromUrl(subpage.loc[0])}</a></Row>);
})
}
</Card>
);
}
export default SitemapCard;

View File

@@ -1,44 +0,0 @@
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;

View File

@@ -1,99 +0,0 @@
import styled from 'styled-components';
import colors from 'styles/colors';
import { Card } from 'components/Form/Card';
import Heading from 'components/Form/Heading';
const Row = styled.div`
display: flex;
justify-content: space-between;
padding: 0.25rem;
&:not(:last-child) { border-bottom: 1px solid ${colors.primary}; }
span.lbl { font-weight: bold; }
span.val {
max-width: 200px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
`;
const formatDate = (dateString: string): string => {
const date = new Date(dateString);
const formatter = new Intl.DateTimeFormat('en-GB', {
day: 'numeric',
month: 'long',
year: 'numeric'
});
return formatter.format(date);
}
const DataRow = (props: { lbl: string, val: string }) => {
const { lbl, val } = props;
return (
<Row>
<span className="lbl">{lbl}</span>
<span className="val" title={val}>{val}</span>
</Row>
);
};
function getExtendedKeyUsage(oids: string[]) {
const oidMap: { [key: string]: string } = {
"1.3.6.1.5.5.7.3.1": "TLS Web Server Authentication",
"1.3.6.1.5.5.7.3.2": "TLS Web Client Authentication",
"1.3.6.1.5.5.7.3.3": "Code Signing",
"1.3.6.1.5.5.7.3.4": "Email Protection (SMIME)",
"1.3.6.1.5.5.7.3.8": "Time Stamping",
"1.3.6.1.5.5.7.3.9": "OCSP Signing",
"1.3.6.1.5.5.7.3.5": "IPSec End System",
"1.3.6.1.5.5.7.3.6": "IPSec Tunnel",
"1.3.6.1.5.5.7.3.7": "IPSec User",
"1.3.6.1.5.5.8.2.2": "IKE Intermediate",
"2.16.840.1.113730.4.1": "Netscape Server Gated Crypto",
"1.3.6.1.4.1.311.10.3.3": "Microsoft Server Gated Crypto",
"1.3.6.1.4.1.311.10.3.4": "Microsoft Encrypted File System",
"1.3.6.1.4.1.311.20.2.2": "Microsoft Smartcard Logon",
"1.3.6.1.4.1.311.10.3.12": "Microsoft Document Signing",
"0.9.2342.19200300.100.1.3": "Email Address (in Subject Alternative Name)",
};
const results = oids.map(oid => oidMap[oid] || oid);
return results.filter((item, index) => results.indexOf(item) === index);
}
const ListRow = (props: { list: string[], title: string }) => {
const { list, title } = props;
return (
<>
<Heading as="h3" size="small" align="left" color={colors.primary}>{title}</Heading>
{ list.map((entry: string, index: number) => {
return (
<Row key={`${title.toLocaleLowerCase()}-${index}`}><span>{ entry }</span></Row>
)}
)}
</>
);
}
const SslCertCard = (props: { data: any, title: string, actionButtons: any }): JSX.Element => {
const sslCert = props.data;
const { subject, issuer, fingerprint, serialNumber, asn1Curve, nistCurve, valid_to, valid_from, ext_key_usage } = sslCert;
return (
<Card heading={props.title} actionButtons={props.actionButtons}>
{ subject && <DataRow lbl="Subject" val={subject?.CN} /> }
{ issuer?.O && <DataRow lbl="Issuer" val={issuer.O} /> }
{ asn1Curve && <DataRow lbl="ASN1 Curve" val={asn1Curve} /> }
{ nistCurve && <DataRow lbl="NIST Curve" val={nistCurve} /> }
{ valid_to && <DataRow lbl="Expires" val={formatDate(valid_to)} /> }
{ valid_from && <DataRow lbl="Renewed" val={formatDate(valid_from)} /> }
{ serialNumber && <DataRow lbl="Serial Num" val={serialNumber} /> }
{ fingerprint && <DataRow lbl="Fingerprint" val={fingerprint} /> }
{ ext_key_usage && <ListRow title="Extended Key Usage" list={getExtendedKeyUsage(ext_key_usage)} /> }
</Card>
);
}
export default SslCertCard;

View File

@@ -1,114 +0,0 @@
import styled from 'styled-components';
import { Card } from 'components/Form/Card';
import Heading from 'components/Form/Heading';
import colors from 'styles/colors';
const cardStyles = `
grid-row: span 2;
small {
margin-top: 1rem;
opacity: 0.5;
display: block;
a { color: ${colors.primary}; }
}
`;
const TechStackRow = styled.div`
transition: all 0.2s ease-in-out;
.r1 {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
flex-wrap: wrap;
}
h4 {
margin: 0.5rem 0 0 0;
}
.r2 {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
}
.tech-version {
opacity: 0.5;
}
.tech-confidence, .tech-categories {
font-size: 0.8rem;
opacity: 0.5;
}
.tech-confidence {
display: none;
}
.tech-description, .tech-website {
font-size: 0.8rem;
margin: 0.25rem 0;
font-style: italic;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
&.tech-website {
-webkit-line-clamp: 1;
}
a {
color: ${colors.primary};
opacity: 0.75;
&:hover { opacity: 1; }
}
}
.tech-icon {
min-width: 2.5rem;
border-radius: 4px;
margin: 0.5rem 0;
}
&:not(:last-child) {
border-bottom: 1px solid ${colors.primary};
}
&:hover {
.tech-confidence {
display: block;
}
.tech-categories {
display: none;
}
}
`;
const TechStackCard = (props: {data: any, title: string, actionButtons: any }): JSX.Element => {
const technologies = props.data.technologies;
const iconsCdn = 'https://www.wappalyzer.com/images/icons/';
return (
<Card heading={props.title} actionButtons={props.actionButtons} styles={cardStyles}>
{technologies.map((tech: any, index: number) => {
return (
<TechStackRow>
<div className="r1">
<Heading as="h4" size="small">
{tech.name}
<span className="tech-version">{tech.version? `(v${tech.version})` : ''}</span>
</Heading>
<span className="tech-confidence" title={`${tech.confidence}% certain`}>Certainty: {tech.confidence}%</span>
<span className="tech-categories">
{tech.categories.map((cat: any, i: number) => `${cat.name}${i < tech.categories.length - 1 ? ', ' : ''}`)}
</span>
</div>
<div className="r2">
<img className="tech-icon" width="10" src={`${iconsCdn}${tech.icon}`} alt={tech.name} />
<div>
<p className="tech-description">{tech.description}</p>
<p className="tech-website">Learn more at: <a href={tech.website}>{tech.website}</a></p>
</div>
</div>
</TechStackRow>
);
})}
</Card>
);
}
export default TechStackCard;

View File

@@ -1,88 +0,0 @@
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;

View File

@@ -1,70 +0,0 @@
import { useState, useEffect } 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);
useEffect(() => { // Update cipher suites when data changes
setCipherSuites(makeCipherSuites(props.data));
}, [props.data]);
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 key={`tls-${index}`} 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;

View File

@@ -1,84 +0,0 @@
import { useState, useEffect } 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);
useEffect(() => {
setClientSupport(makeClientSupport(props.data));
}, [props.data]);
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, index: number) => {
return (
<ExpandableRow
key={`tls-client-${index}`}
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;

View File

@@ -1,131 +0,0 @@
import { useState, useEffect } 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);
useEffect(() => {
setTlsRowWata(makeExpandableData(props.data));
setTlsResults(makeResults(props.data));
}, [props.data]);
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}
key={`tls-issues-${index}`}
/>
);
})}
<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;

View File

@@ -1,64 +0,0 @@
import styled from 'styled-components';
import colors from 'styles/colors';
import { Card } from 'components/Form/Card';
const RouteRow = styled.div`
text-align: center;
width: fit-content;
margin: 0 auto;
.ipName {
font-size: 1rem;
}
`;
const RouteTimings = styled.div`
p {
margin: 0 auto;
}
.arrow {
font-size: 2.5rem;
color: ${colors.primary};
margin-top: -1rem;
}
.times {
font-size: 0.85rem;
color: ${colors.textColorSecondary};
}
.completed {
text-align: center;
font-weight: bold;
}
`;
const cardStyles = ``;
const TraceRouteCard = (props: { data: any, title: string, actionButtons: any }): JSX.Element => {
const traceRouteResponse = props.data;
const routes = traceRouteResponse.result;
return (
<Card heading={props.title} actionButtons={props.actionButtons} styles={cardStyles}>
{routes.filter((x: any) => x).map((route: any, index: number) => (
<RouteRow key={index}>
<span className="ipName">{Object.keys(route)[0]}</span>
<RouteTimings>
{route[Object.keys(route)[0]].map((time: any, packetIndex: number) => (
<p className="times" key={`timing-${packetIndex}-${time}`}>
{ route[Object.keys(route)[0]].length > 1 && (<>Packet #{packetIndex + 1}:</>) }
Took {time} ms
</p>
))}
<p className="arrow"></p>
</RouteTimings>
</RouteRow>
)
)}
<RouteTimings>
<p className="completed">
Round trip completed in {traceRouteResponse.timeTaken} ms
</p>
</RouteTimings>
</Card>
);
}
export default TraceRouteCard;

View File

@@ -1,25 +0,0 @@
import { Card } from 'components/Form/Card';
import Row from 'components/Form/Row';
const cardStyles = `
grid-column: span 2;
span.val { max-width: 32rem !important; }
span { overflow: hidden; }
`;
const TxtRecordCard = (props: {data: any, title: string, actionButtons: any }): JSX.Element => {
const records = props.data;
return (
<Card heading={props.title} actionButtons={props.actionButtons} styles={cardStyles}>
{ !records && <Row lbl="" val="No TXT Records" />}
{Object.keys(records).map((recordName: any, index: number) => {
return (
<Row lbl={recordName} val={records[recordName]} key={`${recordName}-${index}`} />
);
})}
</Card>
);
}
export default TxtRecordCard;

View File

@@ -1,69 +0,0 @@
import styled from 'styled-components';
import { Whois } from 'utils/result-processor';
import colors from 'styles/colors';
import { Card } from 'components/Form/Card';
import Heading from 'components/Form/Heading';
const Row = styled.div`
display: flex;
justify-content: space-between;
padding: 0.25rem;
&:not(:last-child) { border-bottom: 1px solid ${colors.primary}; }
span.lbl { font-weight: bold; }
span.val {
max-width: 200px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
`;
const formatDate = (dateString: string): string => {
const date = new Date(dateString);
const formatter = new Intl.DateTimeFormat('en-GB', {
day: 'numeric',
month: 'long',
year: 'numeric'
});
return formatter.format(date);
}
const DataRow = (props: { lbl: string, val: string }) => {
const { lbl, val } = props;
return (
<Row>
<span className="lbl">{lbl}</span>
<span className="val" title={val}>{val}</span>
</Row>
);
};
const ListRow = (props: { list: string[], title: string }) => {
const { list, title } = props;
return (
<>
<Heading as="h3" size="small" align="left" color={colors.primary}>{title}</Heading>
{ list.map((entry: string, index: number) => {
return (
<Row key={`${title.toLocaleLowerCase()}-${index}`}><span>{ entry }</span></Row>
)}
)}
</>
);
}
const WhoIsCard = (props: { data: Whois, title: string, actionButtons: any }): JSX.Element => {
const whois = props.data;
const { created, updated, expires, nameservers } = whois;
return (
<Card heading={props.title} actionButtons={props.actionButtons}>
{ created && <DataRow lbl="Created" val={formatDate(created)} /> }
{ updated && <DataRow lbl="Updated" val={formatDate(updated)} /> }
{ expires && <DataRow lbl="Expires" val={formatDate(expires)} /> }
{ nameservers && <ListRow title="Name Servers" list={nameservers} /> }
</Card>
);
}
export default WhoIsCard;

View File

@@ -1,59 +0,0 @@
import styled from 'styled-components';
import Button from 'components/Form/Button';
import colors from 'styles/colors';
const ActionButtonContainer = styled.div`
position: absolute;
top: 0.25rem;
right: 0.25rem;
opacity: 0.75;
display: flex;
gap: 0.125rem;
align-items: baseline;
`;
interface Action {
label: string;
icon: string;
onClick: () => void;
};
const actionButtonStyles = `
padding: 0 0.25rem;
font-size: 1.25rem;
text-align: center;
width: 1.5rem;
height: 1.5rem;
color: ${colors.textColor};
background: none;
box-shadow: none;
transition: all 0.2s ease-in-out;
margin: 0;
display: flex;
align-items: center;
&:hover {
color: ${colors.primary};
background: ${colors.backgroundDarker};
box-shadow: none;
}
`;
const ActionButtons = (props: { actions: any }): JSX.Element => {
const actions = props.actions;
if (!actions) return (<></>);
return (
<ActionButtonContainer>
{ actions.map((action: Action, index: number) =>
<Button
key={`action-${index}`}
styles={actionButtonStyles}
onClick={action.onClick}
title={action.label}>
{action.icon}
</Button>
)}
</ActionButtonContainer>
);
};
export default ActionButtons;

View File

@@ -1,252 +0,0 @@
import styled from 'styled-components';
import colors from 'styles/colors';
import { Card } from 'components/Form/Card';
const ResourceListOuter = styled.ul`
list-style: none;
margin: 0;
padding: 1rem;
display: grid;
gap: 0.5rem;
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 0.5rem;
background: ${colors.background};
border-radius: 8px;
text-decoration: none;
color: ${colors.textColor};
height: 100%;
transition: all 0.2s ease-in-out;
cursor: pointer;
border: none;
border-radius: 0.25rem;
font-family: PTMono;
box-sizing: border-box;
width: -moz-available;
box-shadow: 3px 3px 0px ${colors.backgroundDarker};
&:hover {
box-shadow: 5px 5px 0px ${colors.backgroundDarker};
a { opacity: 1; }
}
&:active {
box-shadow: -3px -3px 0px ${colors.fgShadowColor};
}
}
img {
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;
.resource-description {
color: ${colors.textColorSecondary};
font-size: 0.9rem;
}
}
`;
const Note = styled.small`
margin-top: 1rem;
opacity: 0.5;
display: block;
a { color: ${colors.primary}; }
`;
const CardStyles = `
margin: 0 auto 1rem auto;
width: 95vw;
position: relative;
transition: all 0.2s ease-in-out;
`;
const resources = [
{
title: 'SSL Labs Test',
link: 'https://ssllabs.com/ssltest/analyze.html',
icon: 'https://i.ibb.co/6bVL8JK/Qualys-ssl-labs.png',
description: 'Analyzes the SSL configuration of a server and grades it',
},
{
title: 'Virus Total',
link: 'https://virustotal.com',
icon: 'https://i.ibb.co/dWFz0RC/Virustotal.png',
description: 'Checks a URL against multiple antivirus engines',
searchLink: 'https://www.virustotal.com/gui/domain/{URL}',
},
{
title: 'Shodan',
link: 'https://shodan.io/',
icon: 'https://i.ibb.co/SBZ8WG4/shodan.png',
description: 'Search engine for Internet-connected devices',
searchLink: 'https://www.shodan.io/search/report?query={URL}',
},
{
title: 'Archive',
link: 'https://archive.org/',
icon: 'https://i.ibb.co/nfKMvCm/Archive-org.png',
description: 'View previous versions of a site via the Internet Archive',
searchLink: 'https://web.archive.org/web/*/{URL}',
},
{
title: 'URLScan',
link: 'https://urlscan.io/',
icon: 'https://i.ibb.co/cYXt8SH/Url-scan.png',
description: 'Scans a URL and provides information about the page',
},
{
title: 'Sucuri SiteCheck',
link: 'https://sitecheck.sucuri.net/',
icon: 'https://i.ibb.co/K5pTP1K/Sucuri-site-check.png',
description: 'Checks a URL against blacklists and known threats',
searchLink: 'https://www.ssllabs.com/ssltest/analyze.html?d={URL}',
},
{
title: 'Domain Tools',
link: 'https://whois.domaintools.com/',
icon: 'https://i.ibb.co/zJfCKjM/Domain-tools.png',
description: 'Run a WhoIs lookup on a domain',
searchLink: 'https://whois.domaintools.com/{URL}',
},
{
title: 'NS Lookup',
link: 'https://nslookup.io/',
icon: 'https://i.ibb.co/BLSWvBv/Ns-lookup.png',
description: 'View DNS records for a domain',
searchLink: 'https://www.nslookup.io/domains/{URL}/dns-records/',
},
{
title: 'DNS Checker',
link: 'https://dnschecker.org/',
icon: 'https://i.ibb.co/gyKtgZ1/Dns-checker.webp',
description: 'Check global DNS propagation across multiple servers',
searchLink: 'https://dnschecker.org/#A/{URL}',
},
{
title: 'Censys',
link: 'https://search.censys.io/',
icon: 'https://i.ibb.co/j3ZtXzM/censys.png',
description: 'Lookup hosts associated with a domain',
searchLink: 'https://search.censys.io/search?resource=hosts&q={URL}',
},
{
title: 'Page Speed Insights',
link: 'https://developers.google.com/speed/pagespeed/insights/',
icon: 'https://i.ibb.co/k68t9bb/Page-speed-insights.png',
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/',
icon: 'https://i.ibb.co/DtQ2QXP/Trash-can-regular.png',
description: 'DNS recon tool, to map out a domain from it\'s DNS records',
searchLink: '',
},
{
title: 'BGP Tools',
link: 'https://bgp.tools/',
icon: 'https://i.ibb.co/zhcSnmh/Bgp-tools.png',
description: 'View realtime BGP data for any ASN, Prefix or DNS',
},
{
title: 'Similar Web',
link: 'https://similarweb.com/',
icon: 'https://i.ibb.co/9YX8x3c/Similar-web.png',
description: 'View approx traffic and engagement stats for a website',
searchLink: 'https://similarweb.com/website/{URL}',
},
{
title: 'Blacklist Checker',
link: 'https://blacklistchecker.com/',
icon: 'https://i.ibb.co/7ygCyz3/black-list-checker.png',
description: 'Check if a domain, IP or email is present on the top blacklists',
searchLink: 'https://blacklistchecker.com/check?input={URL}',
},
{
title: 'Cloudflare Radar',
link: 'https://radar.cloudflare.com/',
icon: 'https://i.ibb.co/DGZXRgh/Cloudflare.png',
description: 'View traffic source locations for a domain through Cloudflare',
searchLink: 'https://radar.cloudflare.com/domains/domain/{URL}',
},
{
title: 'Mozilla Observatory',
link: 'https://observatory.mozilla.org/',
icon: 'https://i.ibb.co/hBWh9cj/logo-mozm-5e95c457fdd1.png',
description: 'Assesses website security posture by analyzing various security headers and practices',
searchLink: 'https://observatory.mozilla.org/analyze/{URL}',
},
];
const makeLink = (resource: any, scanUrl: string | undefined): string => {
return (scanUrl && resource.searchLink) ? resource.searchLink.replaceAll('{URL}', scanUrl.replace('https://', '')) : resource.link;
};
const AdditionalResources = (props: { url?: string }): JSX.Element => {
return (<Card heading="External Tools for Further Research" styles={CardStyles}>
<ResourceListOuter>
{
resources.map((resource, index) => {
return (
<li key={index}>
<a className="resource-wrap" target="_blank" rel="noreferrer" href={makeLink(resource, props.url)}>
<p className="resource-title">{resource.title}</p>
<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>
);
})
}
</ResourceListOuter>
<Note>
These tools are not affiliated with Web-Check. Please use them at your own risk.<br />
At the time of listing, all of the above were available and free to use
- if this changes, please report it via GitHub (<a href="https://github.com/lissy93/web-check">lissy93/web-check</a>).
</Note>
</Card>);
}
export default AdditionalResources;

View File

@@ -1,56 +0,0 @@
import styled from 'styled-components';
import docs, { type Doc } from 'utils/docs';
import colors from 'styles/colors';
import Heading from 'components/Form/Heading';
const JobDocsContainer = styled.div`
p.doc-desc, p.doc-uses, ul {
margin: 0.25rem auto 1.5rem auto;
}
ul {
padding: 0 0.5rem 0 1rem;
}
ul li a {
color: ${colors.primary};
}
summary { color: ${colors.primary};}
h4 {
border-top: 1px solid ${colors.primary};
color: ${colors.primary};
opacity: 0.75;
padding: 0.5rem 0;
}
`;
const DocContent = (id: string) => {
const doc = docs.filter((doc: Doc) => doc.id === id)[0] || null;
return (
doc? (<JobDocsContainer>
<Heading as="h3" size="medium" color={colors.primary}>{doc.title}</Heading>
<Heading as="h4" size="small">About</Heading>
<p className="doc-desc">{doc.description}</p>
<Heading as="h4" size="small">Use Cases</Heading>
<p className="doc-uses">{doc.use}</p>
<Heading as="h4" size="small">Links</Heading>
<ul>
{doc.resources.map((resource: string | { title: string, link: string } , index: number) => (
typeof resource === 'string' ? (
<li id={`link-${index}`}><a target="_blank" rel="noreferrer" href={resource}>{resource}</a></li>
) : (
<li id={`link-${index}`}><a target="_blank" rel="noreferrer" href={resource.link}>{resource.title}</a></li>
)
))}
</ul>
<details>
<summary><Heading as="h4" size="small">Example</Heading></summary>
<img width="300" src={doc.screenshot} alt="Screenshot" />
</details>
</JobDocsContainer>)
: (
<JobDocsContainer>
<p>No Docs provided for this widget yet</p>
</JobDocsContainer>
));
};
export default DocContent;

View File

@@ -1,63 +0,0 @@
import React, { Component, ErrorInfo, ReactNode } from "react";
import styled from 'styled-components';
import Card from 'components/Form/Card';
import Heading from 'components/Form/Heading';
import colors from 'styles/colors';
interface Props {
children: ReactNode;
title?: string;
key?: string;
}
interface State {
hasError: boolean;
errorMessage: string | null;
}
const ErrorText = styled.p`
color: ${colors.danger};
`;
class ErrorBoundary extends Component<Props, State> {
public state: State = {
hasError: false,
errorMessage: null
};
// Catch errors in any components below and re-render with error message
public static getDerivedStateFromError(error: Error): State {
return { hasError: true, errorMessage: error.message };
}
public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error("Uncaught error:", error, errorInfo);
}
public render() {
if (this.state.hasError) {
return (
<Card>
{ this.props.title && <Heading color={colors.primary}>{this.props.title}</Heading> }
<ErrorText>This component errored unexpectedly</ErrorText>
<p>
Usually this happens if the result from the server was not what was expected.
Check the logs for more info. If you continue to experience this issue, please raise a ticket on the repository.
</p>
{
this.state.errorMessage &&
<details>
<summary>Error Details</summary>
<div>{this.state.errorMessage}</div>
</details>
}
</Card>
);
}
return this.props.children;
}
}
export default ErrorBoundary;

View File

@@ -1,352 +0,0 @@
import { useEffect, useMemo } from "react";
const FancyBackground = (): JSX.Element => {
const makeAbsolute = (elem: HTMLElement) => {
elem.style.position = 'absolute';
elem.style.top = '0';
elem.style.left = '0';
};
const maxBy = (array: any) => {
const chaos = 30;
const iteratee = (e: any) => e.field + chaos * Math.random();
let result;
if (array == null) { return result; }
let computed;
for (const value of array) {
const current = iteratee(value);
if (current != null && (computed === undefined ? current : current > computed)) {
computed = current;
result = value;
}
}
return result;
};
const App: any = useMemo(() => [], []);
App.setup = function () {
this.lifespan = 1000;
this.popPerBirth = 1;
this.maxPop = 300;
this.birthFreq = 2;
this.bgColor = '#141d2b';
var canvas = document.createElement('canvas');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
canvas.style.opacity = '0.5';
makeAbsolute(canvas);
this.canvas = canvas;
const container = document.getElementById('fancy-background');
if (container) {
container.style.color = this.bgColor;
makeAbsolute(container);
container.appendChild(canvas);
}
this.ctx = this.canvas.getContext('2d');
this.width = this.canvas.width;
this.height = this.canvas.height;
this.dataToImageRatio = 1;
this.ctx.imageSmoothingEnabled = false;
this.ctx.webkitImageSmoothingEnabled = false;
this.ctx.msImageSmoothingEnabled = false;
this.xC = this.width / 2;
this.yC = this.height / 2;
this.stepCount = 0;
this.particles = [];
// Build grid
this.gridSize = 8; // Motion coords
this.gridSteps = Math.floor(1000 / this.gridSize);
this.grid = [];
var i = 0;
for (var xx = -500; xx < 500; xx += this.gridSize) {
for (var yy = -500; yy < 500; yy += this.gridSize) {
// Radial field, triangular function of r with max around r0
var r = Math.sqrt(xx * xx + yy * yy),
r0 = 100,
field;
if (r < r0) field = (255 / r0) * r;
else if (r > r0) field = 255 - Math.min(255, (r - r0) / 2);
this.grid.push({
x: xx,
y: yy,
busyAge: 0,
spotIndex: i,
isEdge:
xx === -500
? 'left'
: xx === -500 + this.gridSize * (this.gridSteps - 1)
? 'right'
: yy === -500
? 'top'
: yy === -500 + this.gridSize * (this.gridSteps - 1)
? 'bottom'
: false,
field: field,
});
i++;
}
}
this.gridMaxIndex = i;
// Counters for UI
this.drawnInLastFrame = 0;
this.deathCount = 0;
this.initDraw();
};
App.evolve = function () {
var time1 = performance.now();
this.stepCount++;
// Increment all grid ages
this.grid.forEach(function (e: any) {
if (e.busyAge > 0) e.busyAge++;
});
if (
this.stepCount % this.birthFreq === 0 &&
this.particles.length + this.popPerBirth < this.maxPop
) {
this.birth();
}
App.move();
App.draw();
var time2 = performance.now();
// Update UI
const elemDead = document.getElementsByClassName('dead');
if (elemDead && elemDead.length > 0) elemDead[0].textContent = this.deathCount;
const elemAlive = document.getElementsByClassName('alive');
if (elemAlive && elemAlive.length > 0) elemAlive[0].textContent = this.particles.length;
const elemFPS = document.getElementsByClassName('fps');
if (elemFPS && elemFPS.length > 0) elemFPS[0].textContent = Math.round(1000 / (time2 - time1)).toString();
const elemDrawn = document.getElementsByClassName('drawn');
if (elemDrawn && elemDrawn.length > 0) elemDrawn[0].textContent = this.drawnInLastFrame;
};
App.birth = function () {
var x, y;
var gridSpotIndex = Math.floor(Math.random() * this.gridMaxIndex);
var gridSpot = this.grid[gridSpotIndex];
x = gridSpot.x;
y = gridSpot.y;
var particle = {
hue: 200, // + Math.floor(50*Math.random()),
sat: 95, //30 + Math.floor(70*Math.random()),
lum: 20 + Math.floor(40 * Math.random()),
x: x,
y: y,
xLast: x,
yLast: y,
xSpeed: 0,
ySpeed: 0,
age: 0,
ageSinceStuck: 0,
attractor: {
oldIndex: gridSpotIndex,
gridSpotIndex: gridSpotIndex, // Pop at random position on grid
},
name: 'seed-' + Math.ceil(10000000 * Math.random()),
};
this.particles.push(particle);
};
App.kill = function (particleName: any) {
const newArray = this.particles.filter(
(seed: any) => seed.name !== particleName
);
this.particles = [...newArray];
};
App.move = function () {
for (var i = 0; i < this.particles.length; i++) {
// Get particle
var p = this.particles[i];
// Save last position
p.xLast = p.x;
p.yLast = p.y;
// Attractor and corresponding grid spot
var index = p.attractor.gridSpotIndex,
gridSpot = this.grid[index];
// Maybe move attractor and with certain constraints
if (Math.random() < 0.5) {
// Move attractor
if (!gridSpot.isEdge) {
// Change particle's attractor grid spot and local move function's grid spot
var topIndex = index - 1,
bottomIndex = index + 1,
leftIndex = index - this.gridSteps,
rightIndex = index + this.gridSteps,
topSpot = this.grid[topIndex],
bottomSpot = this.grid[bottomIndex],
leftSpot = this.grid[leftIndex],
rightSpot = this.grid[rightIndex];
var maxFieldSpot = maxBy(
[topSpot, bottomSpot, leftSpot, rightSpot]
);
var potentialNewGridSpot = maxFieldSpot;
if (
potentialNewGridSpot.busyAge === 0 ||
potentialNewGridSpot.busyAge > 15
) {
p.ageSinceStuck = 0;
p.attractor.oldIndex = index;
p.attractor.gridSpotIndex = potentialNewGridSpot.spotIndex;
gridSpot = potentialNewGridSpot;
gridSpot.busyAge = 1;
} else p.ageSinceStuck++;
} else p.ageSinceStuck++;
if (p.ageSinceStuck === 10) this.kill(p.name);
}
// Spring attractor to center with viscosity
const k = 8, visc = 0.4;
var dx = p.x - gridSpot.x,
dy = p.y - gridSpot.y,
dist = Math.sqrt(dx * dx + dy * dy);
// Spring
var xAcc = -k * dx,
yAcc = -k * dy;
p.xSpeed += xAcc;
p.ySpeed += yAcc;
// Calm the f*ck down
p.xSpeed *= visc;
p.ySpeed *= visc;
// Store stuff in particle brain
p.speed = Math.sqrt(p.xSpeed * p.xSpeed + p.ySpeed * p.ySpeed);
p.dist = dist;
// Update position
p.x += 0.1 * p.xSpeed;
p.y += 0.1 * p.ySpeed;
// Get older
p.age++;
// Kill if too old
if (p.age > this.lifespan) {
this.kill(p.name);
this.deathCount++;
}
}
};
App.initDraw = function () {
this.ctx.beginPath();
this.ctx.rect(0, 0, this.width, this.height);
this.ctx.fillStyle = this.bgColor;
this.ctx.fill();
this.ctx.closePath();
};
App.draw = function () {
this.drawnInLastFrame = 0;
if (!this.particles.length) return false;
this.ctx.beginPath();
this.ctx.rect(0, 0, this.width, this.height);
this.ctx.fillStyle = this.bgColor;
this.ctx.fill();
this.ctx.closePath();
for (var i = 0; i < this.particles.length; i++) {
var p = this.particles[i];
var last = this.dataXYtoCanvasXY(p.xLast, p.yLast),
now = this.dataXYtoCanvasXY(p.x, p.y);
var attracSpot = this.grid[p.attractor.gridSpotIndex],
attracXY = this.dataXYtoCanvasXY(attracSpot.x, attracSpot.y);
var oldAttracSpot = this.grid[p.attractor.oldIndex],
oldAttracXY = this.dataXYtoCanvasXY(oldAttracSpot.x, oldAttracSpot.y);
this.ctx.beginPath();
this.ctx.strokeStyle = '#9fef00';
this.ctx.fillStyle = '#9fef00';
// Particle trail
this.ctx.moveTo(last.x, last.y);
this.ctx.lineTo(now.x, now.y);
this.ctx.lineWidth = 1.5 * this.dataToImageRatio;
this.ctx.stroke();
this.ctx.closePath();
// Attractor positions
this.ctx.beginPath();
this.ctx.lineWidth = 1.5 * this.dataToImageRatio;
this.ctx.moveTo(oldAttracXY.x, oldAttracXY.y);
this.ctx.lineTo(attracXY.x, attracXY.y);
this.ctx.arc(
attracXY.x,
attracXY.y,
1.5 * this.dataToImageRatio,
0,
2 * Math.PI,
false
);
this.ctx.strokeStyle = '#9fef00';
this.ctx.fillStyle = '#9fef00';
this.ctx.stroke();
this.ctx.fill();
this.ctx.closePath();
// UI counter
this.drawnInLastFrame++;
}
};
App.dataXYtoCanvasXY = function (x: number, y: number) {
var zoom = 1.6;
var xx = this.xC + x * zoom * this.dataToImageRatio,
yy = this.yC + y * zoom * this.dataToImageRatio;
return { x: xx, y: yy };
};
useEffect(() => {
App.setup();
App.draw();
var frame = function () {
App.evolve();
requestAnimationFrame(frame);
};
frame();
}, [App]);
return (
<div className='ui' id='fancy-background'>
<p><span className='dead'>0</span></p>
<p><span className='alive'>0</span></p>
<p><span className='drawn'>0</span></p>
<p><span className='fps'>0</span></p>
</div>
);
}
export default FancyBackground;

View File

@@ -1,20 +0,0 @@
interface Props {
countryCode: string,
width: number,
};
const Flag = ({ countryCode, width }: Props): JSX.Element => {
const getFlagUrl = (code: string, w: number = 64) => {
const protocol = 'https';
const cdn = 'flagcdn.com';
const dimensions = `${width}x${width * 0.75}`;
const country = countryCode.toLowerCase();
const ext = 'png';
return `${protocol}://${cdn}/${dimensions}/${country}.${ext}`;
};
return (<img src={getFlagUrl(countryCode, width)} alt={countryCode} />);
}
export default Flag;

View File

@@ -1,61 +0,0 @@
import styled from 'styled-components';
import colors from 'styles/colors';
const StyledFooter = styled.footer`
bottom: 0;
width: 100%;
text-align: center;
padding: 0.5rem 0;
background: ${colors.backgroundDarker};
display: flex;
justify-content: space-around;
align-items: center;
align-content: center;
flex-wrap: wrap;
opacity: 0.75;
transition: all 0.2s ease-in-out;
@media (min-width: 1024px) {
justify-content: space-between;
}
&:hover {
opacity: 1;
}
span {
margin: 0 0.5rem;
text-align: center;
}
`;
const Link = styled.a`
color: ${colors.primary};
font-weight: bold;
border-radius: 4px;
padding: 0.1rem;
transition: all 0.2s ease-in-out;
&:hover {
background: ${colors.primary};
color: ${colors.backgroundDarker};
text-decoration: none;
}
`;
const Footer = (props: { isFixed?: boolean }): JSX.Element => {
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 (
<StyledFooter style={props.isFixed ? {position: 'fixed'} : {}}>
<span>
View source at <Link href={githubUrl}>github.com/lissy93/web-check</Link>
</span>
<span>
<Link href="/about">Web-Check</Link> is
licensed under <Link href={licenseUrl}>MIT</Link> -
© <Link href={authorUrl}>Alicia Sykes</Link> 2023
</span>
</StyledFooter>
);
}
export default Footer;

View File

@@ -1,103 +0,0 @@
import styled from 'styled-components';
import { StyledCard } from 'components/Form/Card';
import Heading from 'components/Form/Heading';
import colors from 'styles/colors';
const LoaderContainer = styled(StyledCard)`
margin: 0 auto 1rem auto;
width: 95vw;
position: relative;
transition: all 0.2s ease-in-out;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
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;
}
&.finished {
height: 0;
overflow: hidden;
opacity: 0;
margin: 0;
padding: 0;
svg { width: 0; }
h4 { font-size: 0; }
}
&.hide {
display: none;
}
`;
const StyledSvg = styled.svg`
width: 200px;
margin: 0 auto;
path {
fill: ${colors.primary};
&:nth-child(2) { opacity: 0.8; }
&:nth-child(3) { opacity: 0.5; }
}
`;
const Loader = (props: { show: boolean }): JSX.Element => {
return (
<LoaderContainer className={props.show ? '' : 'finished'}>
<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
c-8.4-21.3,2-45.4,23.3-53.8L31.6,3.5z">
<animateTransform
attributeName="transform"
attributeType="XML"
type="rotate"
dur="2s"
from="0 50 50"
to="360 50 50"
repeatCount="indefinite" />
</path>
<path fill="#fff" d="M42.3,39.6c5.7-4.3,13.9-3.1,18.1,2.7c4.3,5.7,3.1,13.9-2.7,18.1l4.1,5.5c8.8-6.5,10.6-19,4.1-27.7
c-6.5-8.8-19-10.6-27.7-4.1L42.3,39.6z">
<animateTransform
attributeName="transform"
attributeType="XML"
type="rotate"
dur="1s"
from="0 50 50"
to="-360 50 50"
repeatCount="indefinite" />
</path>
<path fill="#fff" d="M82,35.7C74.1,18,53.4,10.1,35.7,18S10.1,46.6,18,64.3l7.6-3.4c-6-13.5,0-29.3,13.5-35.3s29.3,0,35.3,13.5
L82,35.7z">
<animateTransform
attributeName="transform"
attributeType="XML"
type="rotate"
dur="2s"
from="0 50 50"
to="360 50 50"
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>
);
}
export default Loader;

View File

@@ -1,59 +0,0 @@
import {
ComposableMap,
Geographies,
Geography,
Annotation,
} from 'react-simple-maps';
import colors from 'styles/colors';
import MapFeatures from 'assets/data/map-features.json';
interface Props {
lat: number,
lon: number,
label?: string,
};
const MapChart = (location: Props) => {
const { lat, lon, label } = location;
return (
<ComposableMap
projection="geoAzimuthalEqualArea"
projectionConfig={{
rotate: [0, 0, 0],
center: [lon + 5, lat - 25],
scale: 200
}}
>
<Geographies
geography={MapFeatures}
fill={colors.backgroundDarker}
stroke={colors.primary}
strokeWidth={0.5}
>
{({ geographies }: any) =>
geographies.map((geo: any) => (
<Geography key={geo.rsmKey} geography={geo} />
))
}
</Geographies>
<Annotation
subject={[lon, lat]}
dx={-80}
dy={-80}
connectorProps={{
stroke: colors.textColor,
strokeWidth: 3,
strokeLinecap: "round"
}}
>
<text x="-8" textAnchor="end" fill={colors.textColor} fontSize={25}>
{label || "Server"}
</text>
</Annotation>
</ComposableMap>
);
};
export default MapChart;

View File

@@ -1,468 +0,0 @@
import styled from 'styled-components';
import colors from 'styles/colors';
import Card from 'components/Form/Card';
import Heading from 'components/Form/Heading';
import { useState, useEffect, ReactNode } from 'react';
const LoadCard = styled(Card)`
margin: 0 auto 1rem auto;
width: 95vw;
position: relative;
transition: all 0.2s ease-in-out;
&.hidden {
height: 0;
overflow: hidden;
margin: 0;
padding: 0;
}
`;
const ProgressBarContainer = styled.div`
width: 100%;
height: 0.5rem;
background: ${colors.bgShadowColor};
border-radius: 4px;
overflow: hidden;
`;
const ProgressBarSegment = styled.div<{ color: string, color2: string, width: number }>`
height: 1rem;
display: inline-block;
width: ${props => props.width}%;
background: ${props => props.color};
background: ${props => props.color2 ?
`repeating-linear-gradient( 315deg, ${props.color}, ${props.color} 3px, ${props.color2} 3px, ${props.color2} 6px )`
: props.color};
transition: width 0.5s ease-in-out;
`;
const Details = styled.details`
transition: all 0.2s ease-in-out;
summary {
margin: 0.5rem 0;
font-weight: bold;
cursor: pointer;
}
summary:before {
content: "►";
position: absolute;
margin-left: -1rem;
color: ${colors.primary};
cursor: pointer;
}
&[open] summary:before {
content: "▼";
}
ul {
list-style: none;
padding: 0.25rem;
border-radius: 4px;
width: fit-content;
li b {
cursor: pointer;
}
i {
color: ${colors.textColorSecondary};
}
}
p.error {
margin: 0.5rem 0;
opacity: 0.75;
color: ${colors.danger};
}
`;
const StatusInfoWrapper = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
.run-status {
color: ${colors.textColorSecondary};
margin: 0;
}
`;
const AboutPageLink = styled.a`
color: ${colors.primary};
`;
const SummaryContainer = styled.div`
margin: 0.5rem 0;
b {
margin: 0;
font-weight: bold;
}
p {
margin: 0;
opacity: 0.75;
}
&.error-info {
color: ${colors.danger};
}
&.success-info {
color: ${colors.success};
}
&.loading-info {
color: ${colors.info};
}
.skipped {
margin-left: 0.75rem;
color: ${colors.warning};
}
.success {
margin-left: 0.75rem;
color: ${colors.success};
}
`;
const ReShowContainer = styled.div`
position: relative;
&.hidden {
height: 0;
overflow: hidden;
margin: 0;
padding: 0;
}
button { background: none;}
`;
const DismissButton = styled.button`
width: fit-content;
position: absolute;
right: 1rem;
bottom: 1rem;
background: ${colors.background};
color: ${colors.textColorSecondary};
border: none;
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-family: PTMono;
cursor: pointer;
&:hover {
color: ${colors.primary};
}
`;
const FailedJobActionButton = styled.button`
margin: 0.1rem 0.1rem 0.1rem 0.5rem;
background: ${colors.background};
color: ${colors.textColorSecondary};
border: none;
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-family: PTMono;
cursor: pointer;
border: 1px solid ${colors.textColorSecondary};
transition: all 0.2s ease-in-out;
&:hover {
color: ${colors.primary};
border: 1px solid ${colors.primary};
}
`;
const ErrorModalContent = styled.div`
p {
margin: 0;
}
pre {
color: ${colors.danger};
&.info {
color: ${colors.warning};
}
}
`;
export type LoadingState = 'success' | 'loading' | 'skipped' | 'error' | 'timed-out';
export interface LoadingJob {
name: string,
state: LoadingState,
error?: string,
timeTaken?: number,
retry?: () => void,
}
const jobNames = [
'get-ip',
'location',
'ssl',
'domain',
'quality',
'tech-stack',
'server-info',
'cookies',
'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',
// 'whois',
'txt-records',
'block-lists',
'features',
'sitemap',
'carbon',
] as const;
interface JobListItemProps {
job: LoadingJob;
showJobDocs: (name: string) => void;
showErrorModal: (name: string, state: LoadingState, timeTaken: number | undefined, error: string, isInfo?: boolean) => void;
barColors: Record<LoadingState, [string, string]>;
}
const getStatusEmoji = (state: LoadingState): string => {
switch (state) {
case 'success':
return '✅';
case 'loading':
return '🔄';
case 'error':
return '❌';
case 'timed-out':
return '⏸️';
case 'skipped':
return '⏭️';
default:
return '❓';
}
};
const JobListItem: React.FC<JobListItemProps> = ({ job, showJobDocs, showErrorModal, barColors }) => {
const { name, state, timeTaken, retry, error } = job;
const actionButton = retry && state !== 'success' && state !== 'loading' ?
<FailedJobActionButton onClick={retry}> Retry</FailedJobActionButton> : null;
const showModalButton = error && ['error', 'timed-out', 'skipped'].includes(state) &&
<FailedJobActionButton onClick={() => showErrorModal(name, state, timeTaken, error, state === 'skipped')}>
{state === 'timed-out' ? '■ Show Timeout Reason' : '■ Show Error'}
</FailedJobActionButton>;
return (
<li key={name}>
<b onClick={() => showJobDocs(name)}>{getStatusEmoji(state)} {name}</b>
<span style={{color: barColors[state][0]}}> ({state})</span>.
<i>{timeTaken && state !== 'loading' ? ` Took ${timeTaken} ms` : ''}</i>
{actionButton}
{showModalButton}
</li>
);
};
export const initialJobs = jobNames.map((job: string) => {
return {
name: job,
state: 'loading' as LoadingState,
retry: () => {}
}
});
export const calculateLoadingStatePercentages = (loadingJobs: LoadingJob[]): Record<LoadingState | string, number> => {
const totalJobs = loadingJobs.length;
// Initialize count object
const stateCount: Record<LoadingState, number> = {
'success': 0,
'loading': 0,
'timed-out': 0,
'error': 0,
'skipped': 0,
};
// Count the number of each state
loadingJobs.forEach((job) => {
stateCount[job.state] += 1;
});
// Convert counts to percentages
const statePercentage: Record<LoadingState, number> = {
'success': (stateCount['success'] / totalJobs) * 100,
'loading': (stateCount['loading'] / totalJobs) * 100,
'timed-out': (stateCount['timed-out'] / totalJobs) * 100,
'error': (stateCount['error'] / totalJobs) * 100,
'skipped': (stateCount['skipped'] / totalJobs) * 100,
};
return statePercentage;
};
const MillisecondCounter = (props: {isDone: boolean}) => {
const { isDone } = props;
const [milliseconds, setMilliseconds] = useState<number>(0);
useEffect(() => {
let timer: NodeJS.Timeout;
// Start the timer as soon as the component mounts
if (!isDone) {
timer = setInterval(() => {
setMilliseconds(milliseconds => milliseconds + 100);
}, 100);
}
// Clean up the interval on unmount
return () => {
clearInterval(timer);
};
}, [isDone]); // If the isDone prop changes, the effect will re-run
return <span>{milliseconds} ms</span>;
};
const RunningText = (props: { state: LoadingJob[], count: number }): JSX.Element => {
const loadingTasksCount = jobNames.length - props.state.filter((val: LoadingJob) => val.state === 'loading').length;
const isDone = loadingTasksCount >= jobNames.length;
return (
<p className="run-status">
{ isDone ? 'Finished in ' : `Running ${loadingTasksCount} of ${jobNames.length} jobs - ` }
<MillisecondCounter isDone={isDone} />
</p>
);
};
const SummaryText = (props: { state: LoadingJob[], count: number }): JSX.Element => {
const totalJobs = jobNames.length;
let failedTasksCount = props.state.filter((val: LoadingJob) => val.state === 'error').length;
let loadingTasksCount = props.state.filter((val: LoadingJob) => val.state === 'loading').length;
let skippedTasksCount = props.state.filter((val: LoadingJob) => val.state === 'skipped').length;
let successTasksCount = props.state.filter((val: LoadingJob) => val.state === 'success').length;
const jobz = (jobCount: number) => `${jobCount} ${jobCount === 1 ? 'job' : 'jobs'}`;
const skippedInfo = skippedTasksCount > 0 ? (<span className="skipped">{jobz(skippedTasksCount)} skipped </span>) : null;
const successInfo = successTasksCount > 0 ? (<span className="success">{jobz(successTasksCount)} successful </span>) : null;
const failedInfo = failedTasksCount > 0 ? (<span className="error">{jobz(failedTasksCount)} failed </span>) : null;
if (loadingTasksCount > 0) {
return (
<SummaryContainer className="loading-info">
<b>Loading {totalJobs - loadingTasksCount} / {totalJobs} Jobs</b>
{skippedInfo}
</SummaryContainer>
);
}
if (failedTasksCount === 0) {
return (
<SummaryContainer className="success-info">
<b>{successTasksCount} Jobs Completed Successfully</b>
{skippedInfo}
</SummaryContainer>
);
}
return (
<SummaryContainer className="error-info">
{successInfo}
{skippedInfo}
{failedInfo}
</SummaryContainer>
);
};
const ProgressLoader = (props: { loadStatus: LoadingJob[], showModal: (err: ReactNode) => void, showJobDocs: (job: string) => void }): JSX.Element => {
const [ hideLoader, setHideLoader ] = useState<boolean>(false);
const loadStatus = props.loadStatus;
const percentages = calculateLoadingStatePercentages(loadStatus);
const loadingTasksCount = jobNames.length - loadStatus.filter((val: LoadingJob) => val.state === 'loading').length;
const isDone = loadingTasksCount >= jobNames.length;
const makeBarColor = (colorCode: string): [string, string] => {
const amount = 10;
const darkerColorCode = '#' + colorCode.replace(/^#/, '').replace(
/../g,
colorCode => ('0' + Math.min(255, Math.max(0, parseInt(colorCode, 16) - amount)).toString(16)).slice(-2),
);
return [colorCode, darkerColorCode];
};
const barColors: Record<LoadingState | string, [string, string]> = {
'success': isDone ? makeBarColor(colors.primary) : makeBarColor(colors.success),
'loading': makeBarColor(colors.info),
'error': makeBarColor(colors.danger),
'timed-out': makeBarColor(colors.warning),
'skipped': makeBarColor(colors.neutral),
};
const showErrorModal = (name: string, state: LoadingState, timeTaken: number | undefined, error: string, isInfo?: boolean) => {
const errorContent = (
<ErrorModalContent>
<Heading as="h3">Error Details for {name}</Heading>
<p>
The {name} job failed with an {state} state after {timeTaken} ms.
The server responded with the following error:
</p>
{ /* If isInfo == true, then add .info className to pre */}
<pre className={isInfo ? 'info' : 'error'}>{error}</pre>
</ErrorModalContent>
);
props.showModal(errorContent);
};
return (
<>
<ReShowContainer className={!hideLoader ? 'hidden' : ''}>
<DismissButton onClick={() => setHideLoader(false)}>Show Load State</DismissButton>
</ReShowContainer>
<LoadCard className={hideLoader ? 'hidden' : ''}>
<ProgressBarContainer>
{Object.keys(percentages).map((state: string | LoadingState) =>
<ProgressBarSegment
color={barColors[state][0]}
color2={barColors[state][1]}
title={`${state} (${Math.round(percentages[state])}%)`}
width={percentages[state]}
key={`progress-bar-${state}`}
/>
)}
</ProgressBarContainer>
<StatusInfoWrapper>
<SummaryText state={loadStatus} count={loadStatus.length} />
<RunningText state={loadStatus} count={loadStatus.length} />
</StatusInfoWrapper>
<Details>
<summary>Show Details</summary>
<ul>
{loadStatus.map((job: LoadingJob) => (
<JobListItem key={job.name} job={job} showJobDocs={props.showJobDocs} showErrorModal={showErrorModal} barColors={barColors} />
))}
</ul>
{ loadStatus.filter((val: LoadingJob) => val.state === 'error').length > 0 &&
<p className="error">
<b>Check the browser console for logs and more info</b><br />
It's normal for some jobs to fail, either because the host doesn't return the required info,
or restrictions in the lambda function, or hitting an API limit.
</p>}
<AboutPageLink href="/about" target="_blank" rel="noreferer" >Learn More about Web-Check</AboutPageLink>
</Details>
<DismissButton onClick={() => setHideLoader(true)}>Dismiss</DismissButton>
</LoadCard>
</>
);
}
export default ProgressLoader;

View File

@@ -1,49 +0,0 @@
import styled from 'styled-components';
import colors from 'styles/colors';
import { StyledCard } from 'components/Form/Card';
const StyledSelfScanMsg = styled(StyledCard)`
margin: 0px auto 1rem;
width: 95vw;
a { color: ${colors.primary}; }
b { font-weight: extra-bold; }
span, i { opacity: 0.85; }
img {
width: 5rem;
float: right;
border-radius: 4px;
}
`;
const messages = [
'Nice try! But scanning this app is like trying to tickle yourself. It just doesn\'t work!',
'Recursive scanning detected. The universe might implode...or it might not. But let\'s not try to find out.',
'Hey, stop checking us out! We\'re blushing... 😉',
'Hmmm, scanning us, are you? We feel so special!',
'Alert! Mirror scanning detected. Trust us, we\'re looking good 😉',
'We\'re flattered you\'re trying to scan us, but we can\'t tickle ourselves!',
'Oh, inspecting the inspector, aren\'t we? Inception much?',
'Just a second...wait a minute...you\'re scanning us?! Well, that\'s an interesting twist!',
'Scanning us? It\'s like asking a mirror to reflect on itself.',
'Well, this is awkward... like a dog chasing its own tail!',
'Ah, I see you\'re scanning this site... But alas, this did not cause an infinite recursive loop (this time)',
];
const SelfScanMsg = () => {
return (
<StyledSelfScanMsg>
<img src="https://i.ibb.co/0tQbCPJ/test2.png" alt="Self-Scan" />
<b>{messages[Math.floor(Math.random() * messages.length)]}</b>
<br />
<span>
But if you want to see how this site is built, why not check out
the <a href='https://github.com/lissy93/web-check'>source code</a>?
</span>
<br />
<i>Do me a favour, and drop the repo a Star while you're there</i> 😉
</StyledSelfScanMsg>
);
};
export default SelfScanMsg;

View File

@@ -1,107 +0,0 @@
import React, { 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';
const CardStyles = `
margin: 0 auto 1rem auto;
width: 95vw;
position: relative;
transition: all 0.2s ease-in-out;
display: flex;
flex-direction: column;
a {
color: ${colors.primary};
}
.controls {
display: flex;
flex-wrap: wrap;
button {
max-width: 300px;
}
}
small {
margin-top: 0.5rem;
font-size: 0.8rem;
opacity: 0.5;
}
`;
const StyledIframe = styled.iframe`
width: calc(100% - 2rem);
outline: none;
border: none;
border-radius: 4px;
min-height: 50vh;
height: 100%;
margin: 1rem;
background: ${colors.background};
`;
const ViewRaw = (props: { everything: { id: string, result: any}[] }) => {
const [resultUrl, setResultUrl] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const makeResults = () => {
const result: {[key: string]: any} = {};
props.everything.forEach((item: {id: string, result: any}) => {
result[item.id] = item.result;
});
return result;
};
const fetchResultsUrl = async () => {
const resultContent = makeResults();
const response = await fetch('https://jsonhero.io/api/create.json', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
title: 'web-check results',
content: resultContent,
readOnly: true,
ttl: 3600,
})
});
if (!response.ok) {
setError(`HTTP error! status: ${response.status}`);
} else {
setError(null);
}
await response.json().then(
(data) => setResultUrl(data.location)
)
};
const handleDownload = () => {
const blob = new Blob([JSON.stringify(makeResults(), null, 2)], { type: "application/json" });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = 'web-check-results.json';
link.click();
URL.revokeObjectURL(url);
}
return (
<Card heading="View / Download Raw Data" styles={CardStyles}>
<div className="controls">
<Button onClick={handleDownload}>Download Results</Button>
<Button onClick={fetchResultsUrl}>{resultUrl ? 'Update Results' : 'View Results'}</Button>
{ resultUrl && <Button onClick={() => setResultUrl('') }>Hide Results</Button> }
</div>
{ resultUrl && !error &&
<>
<StyledIframe title="Results, via JSON Hero" src={resultUrl} />
<small>Your results are available to view <a href={resultUrl}>here</a>.</small>
</>
}
{ error && <p className="error">{error}</p> }
<small>
These are the raw results generated from your URL, and in JSON format.
You can import these into your own program, for further analysis.
</small>
</Card>
);
};
export default ViewRaw;