Migrate to Astro base
This commit is contained in:
@@ -1,7 +0,0 @@
|
||||
interface Props {
|
||||
message: string,
|
||||
};
|
||||
|
||||
const Demo = ({ message }: Props): JSX.Element => <div>{message}</div>;
|
||||
|
||||
export default Demo;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user