Rename v1 to web-check-live

This commit is contained in:
Alicia Sykes
2024-05-08 21:23:03 +01:00
parent e5738d1f5b
commit 7e27143a90
77 changed files with 240 additions and 240 deletions

View File

@@ -0,0 +1,24 @@
import { Routes, Route, Outlet } from "react-router-dom";
import Home from 'web-check-live/views/Home.tsx';
import Results from 'web-check-live/views/Results.tsx';
import About from 'web-check-live/views/About.tsx';
import NotFound from 'web-check-live/views/NotFound.tsx';
export default function App() {
return (
<Routes>
<Route path="/check" element={<Layout />}>
<Route index element={<Home />} />
<Route path="home" element={<Home />} />
<Route path="about" element={<About />} />
<Route path=":urlToScan" element={<Results />} />
<Route path="*" element={<NotFound />} />
</Route>
</Routes>
);
}
function Layout() {
return (<Outlet />);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,88 @@
import { type ReactNode, type MouseEventHandler } from 'react';
import styled from '@emotion/styled';
import { keyframes } from '@emotion/react';
import colors from 'web-check-live/styles/colors';
import { type InputSize, applySize } from 'web-check-live/styles/dimensions';
type LoadState = 'loading' | 'success' | 'error';
interface ButtonProps {
children: ReactNode;
onClick?: MouseEventHandler<HTMLButtonElement>;
size?: InputSize,
bgColor?: string,
fgColor?: string,
styles?: string,
title?: string,
type?: 'button' | 'submit' | 'reset' | undefined,
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, type } = props;
return (
<StyledButton
onClick={onClick || (() => null) }
size={size}
bgColor={bgColor}
fgColor={fgColor}
styles={styles}
title={title?.toString()}
type={type || 'button'}
>
{ loadState && <Loader loadState={loadState} /> }
{children}
</StyledButton>
);
};
export default Button;

View File

@@ -0,0 +1,41 @@
import styled from '@emotion/styled';
import { type ReactNode } from 'react';
import ErrorBoundary from 'web-check-live/components/misc/ErrorBoundary';
import Heading from 'web-check-live/components/Form/Heading';
import colors from 'web-check-live/styles/colors';
export const StyledCard = styled.section<{ styles?: string}>`
background: ${colors.backgroundLighter};
color: ${colors.textColor};
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: ReactNode;
heading?: string,
styles?: string;
actionButtons?: ReactNode | undefined;
};
export const Card = (props: CardProps): JSX.Element => {
const { children, heading, styles, actionButtons } = props;
return (
<ErrorBoundary title={heading}>
<StyledCard styles={styles}>
{ actionButtons && actionButtons }
{ heading && <Heading className="inner-heading" as="h3" align="left" color={colors.primary}>{heading}</Heading> }
{children}
</StyledCard>
</ErrorBoundary>
);
}
export default StyledCard;

View File

@@ -0,0 +1,67 @@
import styled from '@emotion/styled';
import colors from 'web-check-live/styles/colors';
import { TextSizes } from 'web-check-live/styles/typography';
import type { ReactNode } from 'react';
interface HeadingProps {
as?: 'h1' | 'h2' | 'h3' | 'h4' | 'p';
align?: 'left' | 'center' | 'right';
color?: string;
size?: 'xSmall' | 'small' | 'medium' | 'large' | 'xLarge';
inline?: boolean;
children: ReactNode;
id?: string;
className?: string;
};
const StyledHeading = styled.h1<HeadingProps>`
margin: 0.5rem 0;
text-shadow: 2px 2px 0px ${colors.bgShadowColor};
display: flex;
flex-wrap: wrap;
gap: 1rem;
align-items: center;
font-size: ${TextSizes.medium};
img { // Some titles have an icon
width: 2.5rem;
border-radius: 4px;
}
a { // If a title is a link, keep title styles
color: inherit;
text-decoration: none;
display: flex;
}
${props => {
switch (props.size) {
case 'xSmall': return `font-size: ${TextSizes.xSmall};`;
case 'small': return `font-size: ${TextSizes.small};`;
case 'medium': return `font-size: ${TextSizes.large};`;
case 'large': return `font-size: ${TextSizes.xLarge};`;
case 'xLarge': return `font-size: ${TextSizes.xLarge};`;
}
}};
${props => {
switch (props.align) {
case 'left': return 'text-align: left;';
case 'right': return 'text-align: right;';
case 'center': return 'text-align: center; justify-content: center;';
}
}};
${props => props.color ? `color: ${props.color};` : '' }
${props => props.inline ? 'display: inline;' : '' }
`;
const makeAnchor = (title: string): string => {
return title.toLowerCase().replace(/[^\w\s]|_/g, "").replace(/\s+/g, "-");
};
const Heading = (props: HeadingProps): JSX.Element => {
const { children, as, size, align, color, inline, id, className } = props;
return (
<StyledHeading as={as} size={size} align={align} color={color} inline={inline} className={className} id={id || makeAnchor((children || '')?.toString())}>
{children}
</StyledHeading>
);
}
export default Heading;

View File

@@ -0,0 +1,74 @@
import { type InputHTMLAttributes } from 'react';
import styled from '@emotion/styled';
import colors from 'web-check-live/styles/colors';
import { type InputSize, applySize } from 'web-check-live/styles/dimensions';
type Orientation = 'horizontal' | 'vertical';
interface Props {
id: string,
value: string,
name?: string,
label?: string,
placeholder?: string,
disabled?: boolean,
size?: InputSize,
orientation?: Orientation;
handleChange: (nweVal: React.ChangeEvent<HTMLInputElement>) => void,
handleKeyDown?: (keyEvent: React.KeyboardEvent<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, name, disabled, size, orientation, handleChange, handleKeyDown } = inputProps;
return (
<InputContainer orientation={orientation}>
{ label && <StyledLabel htmlFor={id} inputSize={size}>{ label }</StyledLabel> }
<StyledInput
id={id}
value={value}
placeholder={placeholder}
name={name}
disabled={disabled}
onChange={handleChange}
inputSize={size}
onKeyDown={handleKeyDown || (() => {})}
/>
</InputContainer>
);
};
export default Input;

View File

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

View File

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

View File

@@ -0,0 +1,207 @@
import type { ReactNode } from 'react';
import styled from '@emotion/styled';
import colors from 'web-check-live/styles/colors';
import Heading from 'web-check-live/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;
&li { border-bottom: 1px dashed ${colors.primaryTransparent} !important; }
&: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};
}
}
`;
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 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}>
<StyledRow as="summary" key={`${lbl}-${val}`}>
<span className="lbl" title={title?.toString()}>{lbl}</span>
<span className="val" title={val?.toString()}>{val.toString()}</span>
</StyledRow>
{ rowList &&
<SubRowList>
{ rowList?.map((row: RowProps, index: number) => {
return (
<StyledRow as="li" 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>)}
</StyledRow>
)
})}
</SubRowList>
}
</Details>
);
};
export const ListRow = (props: { list: string[], title: string }) => {
const { list, title } = props;
return (
<>
<Heading as="h4" size="small" align="left" color={colors.primary}>{title}</Heading>
{ list.map((entry: string, index: number) => {
return (
<Row lbl="" val="" key={`${entry}-${title.toLocaleLowerCase()}-${index}`}>
<span>{ entry }</span>
</Row>
)}
)}
</>
);
}
const Row = (props: RowProps) => {
const { lbl, val, title, plaintext, listResults, children } = props;
if (children) return <StyledRow key={`${lbl}-${val}`}>{children}</StyledRow>;
return (
<StyledRow key={`${lbl}-${val}`}>
{ lbl && <span className="lbl" title={title?.toString()}>{lbl}</span> }
<span className="val" title={val} onClick={() => copyToClipboard(val)}>
{formatValue(val)}
</span>
{ plaintext && <PlainText>{plaintext}</PlainText> }
{ listResults && (<List>
{listResults.map((listItem: string, listIndex: number) => (
<li key={listIndex} title={listItem}>{snip(listItem)}</li>
))}
</List>)}
</StyledRow>
);
};
export default Row;

View File

@@ -0,0 +1,37 @@
import styled from '@emotion/styled';
import colors from 'web-check-live/styles/colors';
import { Card } from 'web-check-live/components/Form/Card';
import Row from 'web-check-live/components/Form/Row';
const Note = styled.small`
opacity: 0.5;
display: block;
margin-top: 0.5rem;
a {
color: ${colors.primary};
}
`;
const ArchivesCard = (props: { data: any, title: string, actionButtons: any }): JSX.Element => {
const data = props.data;
return (
<Card heading={props.title} actionButtons={props.actionButtons}>
<Row lbl="First Scan" val={data.firstScan} />
<Row lbl="Last Scan" val={data.lastScan} />
<Row lbl="Total Scans" val={data.totalScans} />
<Row lbl="Change Count" val={data.changeCount} />
<Row lbl="Avg Size" val={`${data.averagePageSize} bytes`} />
{ data.scanFrequency?.scansPerDay > 1 ?
<Row lbl="Avg Scans Per Day" val={data.scanFrequency.scansPerDay} /> :
<Row lbl="Avg Days between Scans" val={data.scanFrequency.daysBetweenScans} />
}
<Note>
View historical versions of this page <a rel="noreferrer" target="_blank" href={`https://web.archive.org/web/*/${data.scanUrl}`}>here</a>,
via the Internet Archive's Wayback Machine.
</Note>
</Card>
);
}
export default ArchivesCard;

View File

@@ -0,0 +1,21 @@
import { Card } from 'web-check-live/components/Form/Card';
import Row from 'web-check-live/components/Form/Row';
const BlockListsCard = (props: {data: any, title: string, actionButtons: any }): JSX.Element => {
const blockLists = props.data.blocklists;
return (
<Card heading={props.title} actionButtons={props.actionButtons}>
{ blockLists.map((blocklist: any, blockIndex: number) => (
<Row
title={blocklist.serverIp}
lbl={blocklist.server}
val={blocklist.isBlocked ? '❌ Blocked' : '✅ Not Blocked'}
key={`blocklist-${blockIndex}-${blocklist.serverIp}`}
/>
))}
</Card>
);
}
export default BlockListsCard;

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,49 @@
import { Card } from 'web-check-live/components/Form/Card';
import { ExpandableRow } from 'web-check-live/components/Form/Row';
import type { Cookie } from 'web-check-live/utils/result-processor';
export const parseHeaderCookies = (cookiesHeader: string[]): Cookie[] => {
if (!cookiesHeader || !cookiesHeader.length) return [];
const cookies = cookiesHeader.flatMap(cookieHeader => {
return cookieHeader.split(/,(?=\s[A-Za-z0-9]+=)/).map(cookieString => {
const [nameValuePair, ...attributePairs] = cookieString.split('; ').map(part => part.trim());
const [name, value] = nameValuePair.split('=');
const attributes: Record<string, string> = {};
attributePairs.forEach(pair => {
const [attributeName, attributeValue = ''] = pair.split('=');
attributes[attributeName] = attributeValue;
});
return { name, value, attributes };
});
});
return cookies;
};
const CookiesCard = (props: { data: any, title: string, actionButtons: any}): JSX.Element => {
const headerCookies = parseHeaderCookies(props.data.headerCookies) || [];
const clientCookies = props.data.clientCookies || [];
return (
<Card heading={props.title} actionButtons={props.actionButtons}>
{
headerCookies.map((cookie: any, index: number) => {
const attributes = Object.keys(cookie.attributes).map((key: string) => {
return { lbl: key, val: cookie.attributes[key] }
});
return (
<ExpandableRow key={`cookie-${index}`} lbl={cookie.name} val={cookie.value} rowList={attributes} />
)
})
}
{
clientCookies.map((cookie: any) => {
const nameValPairs = Object.keys(cookie).map((key: string) => { return { lbl: key, val: cookie[key] }});
return (
<ExpandableRow key={`cookie-${cookie.name}`} lbl={cookie.name} val="" rowList={nameValPairs} />
);
})
}
</Card>
);
}
export default CookiesCard;

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,32 @@
import colors from 'web-check-live/styles/colors';
import { Card } from 'web-check-live/components/Form/Card';
import Row from 'web-check-live/components/Form/Row';
const cardStyles = `
span.val {
&.up { color: ${colors.success}; }
&.down { color: ${colors.danger}; }
}
`;
const DomainLookupCard = (props: { data: any, title: string, actionButtons: any }): JSX.Element => {
const domain = props.data.internicData || {};
return (
<Card heading={props.title} actionButtons={props.actionButtons} styles={cardStyles}>
{ domain.Domain_Name && <Row lbl="Registered Domain" val={domain.Domain_Name} /> }
{ domain.Creation_Date && <Row lbl="Creation Date" val={domain.Creation_Date} /> }
{ domain.Updated_Date && <Row lbl="Updated Date" val={domain.Updated_Date} /> }
{ domain.Registry_Expiry_Date && <Row lbl="Registry Expiry Date" val={domain.Registry_Expiry_Date} /> }
{ domain.Registry_Domain_ID && <Row lbl="Registry Domain ID" val={domain.Registry_Domain_ID} /> }
{ domain.Registrar_WHOIS_Server && <Row lbl="Registrar WHOIS Server" val={domain.Registrar_WHOIS_Server} /> }
{ domain.Registrar && <Row lbl="" val="">
<span className="lbl">Registrar</span>
<span className="val"><a href={domain.Registrar_URL || '#'}>{domain.Registrar}</a></span>
</Row> }
{ domain.Registrar_IANA_ID && <Row lbl="Registrar IANA ID" val={domain.Registrar_IANA_ID} /> }
</Card>
);
}
export default DomainLookupCard;

View File

@@ -0,0 +1,24 @@
import styled from '@emotion/styled';
import { Card } from 'web-check-live/components/Form/Card';
import Row from 'web-check-live/components/Form/Row';
const Note = styled.small`
opacity: 0.5;
display: block;
margin-top: 0.5rem;
`;
const FirewallCard = (props: { data: any, title: string, actionButtons: any }): JSX.Element => {
const data = props.data;
return (
<Card heading={props.title} actionButtons={props.actionButtons}>
<Row lbl="Firewall" val={data.hasWaf ? '✅ Yes' : '❌ No*' } />
{ data.waf && <Row lbl="WAF" val={data.waf} /> }
{ !data.hasWaf && (<Note>
*The domain may be protected with a proprietary or custom WAF which we were unable to identify automatically
</Note>) }
</Card>
);
}
export default FirewallCard;

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,17 @@
import { Card } from 'web-check-live/components/Form/Card';
import Row from 'web-check-live/components/Form/Row';
const HttpSecurityCard = (props: { data: any, title: string, actionButtons: any }): JSX.Element => {
const data = props.data;
return (
<Card heading={props.title} actionButtons={props.actionButtons}>
<Row lbl="Content Security Policy" val={data.contentSecurityPolicy ? '✅ Yes' : '❌ No' } />
<Row lbl="Strict Transport Policy" val={data.strictTransportPolicy ? '✅ Yes' : '❌ No' } />
<Row lbl="X-Content-Type-Options" val={data.xContentTypeOptions ? '✅ Yes' : '❌ No' } />
<Row lbl="X-Frame-Options" val={data.xFrameOptions ? '✅ Yes' : '❌ No' } />
<Row lbl="X-XSS-Protection" val={data.xXSSProtection ? '✅ Yes' : '❌ No' } />
</Card>
);
}
export default HttpSecurityCard;

View File

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

View File

@@ -0,0 +1,45 @@
import { Card } from 'web-check-live/components/Form/Card';
import Row from 'web-check-live/components/Form/Row';
import Heading from 'web-check-live/components/Form/Heading';
import colors from 'web-check-live/styles/colors';
const cardStyles = ``;
const MailConfigCard = (props: {data: any, title: string, actionButtons: any }): JSX.Element => {
const mailServer = props.data;
const txtRecords = (mailServer.txtRecords || []).join('').toLowerCase() || '';
return (
<Card heading={props.title} actionButtons={props.actionButtons} styles={cardStyles}>
<Heading as="h3" color={colors.primary} size="small">Mail Security Checklist</Heading>
<Row lbl="SPF" val={txtRecords.includes('spf')} />
<Row lbl="DKIM" val={txtRecords.includes('dkim')} />
<Row lbl="DMARC" val={txtRecords.includes('dmarc')} />
<Row lbl="BIMI" val={txtRecords.includes('bimi')} />
{ mailServer.mxRecords && <Heading as="h3" color={colors.primary} size="small">MX Records</Heading>}
{ mailServer.mxRecords && mailServer.mxRecords.map((record: any, index: number) => (
<Row lbl="" val="" key={index}>
<span>{record.exchange}</span>
<span>{record.priority ? `Priority: ${record.priority}` : ''}</span>
</Row>
))
}
{ mailServer.mailServices.length > 0 && <Heading as="h3" color={colors.primary} size="small">External Mail Services</Heading>}
{ mailServer.mailServices && mailServer.mailServices.map((service: any, index: number) => (
<Row lbl={service.provider} title={service.value} val="" key={index} />
))
}
{ mailServer.txtRecords && <Heading as="h3" color={colors.primary} size="small">Mail-related TXT Records</Heading>}
{ mailServer.txtRecords && mailServer.txtRecords.map((record: any, index: number) => (
<Row lbl="" val="" key={index}>
<span>{record}</span>
</Row>
))
}
</Card>
);
}
export default MailConfigCard;

View File

@@ -0,0 +1,27 @@
import { Card } from 'web-check-live/components/Form/Card';
import Row from 'web-check-live/components/Form/Row';
const cardStyles = `
small { margin-top: 1rem; opacity: 0.5; }
`;
const OpenPortsCard = (props: { data: any, title: string, actionButtons: any }): JSX.Element => {
const portData = props.data;
return (
<Card heading={props.title} actionButtons={props.actionButtons} styles={cardStyles}>
{portData.openPorts.map((port: any) => (
<Row key={port} lbl="" val="">
<span>{port}</span>
</Row>
)
)}
<br />
<small>
Unable to establish connections to:<br />
{portData.failedPorts.join(', ')}
</small>
</Card>
);
}
export default OpenPortsCard;

View File

@@ -0,0 +1,77 @@
import { AreaChart, Area, Tooltip, CartesianGrid, ResponsiveContainer } from 'recharts';
import colors from 'web-check-live/styles/colors';
import { Card } from 'web-check-live/components/Form/Card';
import Row from 'web-check-live/components/Form/Row';
const cardStyles = `
span.val {
&.up { color: ${colors.success}; }
&.down { color: ${colors.danger}; }
}
.rank-average {
text-align: center;
font-size: 1.8rem;
font-weight: bold;
}
.chart-container {
margin-top: 1rem;
}
`;
const makeRankStats = (data: {date: string, rank: number }[]) => {
const average = Math.round(data.reduce((acc, cur) => acc + cur.rank, 0) / data.length);
const today = data[0].rank;
const yesterday = data[1].rank;
const percentageChange = ((today - yesterday) / yesterday) * 100;
return {
average,
percentageChange
};
};
const makeChartData = (data: {date: string, rank: number }[]) => {
return data.map((d) => {
return {
date: d.date,
uv: d.rank
};
});
};
function Chart(chartData: { date: string; uv: number; }[], data: any) {
return <ResponsiveContainer width="100%" height={100}>
<AreaChart width={400} height={100} data={chartData}>
<defs>
<linearGradient id="colorUv" x1="0" y1="0" x2="0" y2="1">
<stop offset="20%" stopColor="#0f1620" stopOpacity={0.8} />
<stop offset="80%" stopColor="#0f1620" stopOpacity={0} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="4" strokeWidth={0.25} verticalPoints={[50, 100, 150, 200, 250, 300, 350]} horizontalPoints={[25, 50, 75]} />
<Tooltip contentStyle={{ background: colors.background, color: colors.textColor, borderRadius: 4 }}
labelFormatter={(value) => ['Date : ', data[value].date]} />
<Area type="monotone" dataKey="uv" stroke="#9fef00" fillOpacity={1} name="Rank" fill={`${colors.backgroundDarker}a1`} />
</AreaChart>
</ResponsiveContainer>;
}
const RankCard = (props: { data: any, title: string, actionButtons: any }): JSX.Element => {
const data = props.data.ranks || [];
const { average, percentageChange } = makeRankStats(data);
const chartData = makeChartData(data);
return (
<Card heading={props.title} actionButtons={props.actionButtons} styles={cardStyles}>
<div className="rank-average">{data[0].rank.toLocaleString()}</div>
<Row lbl="Change since Yesterday" val={`${percentageChange > 0 ? '+':''} ${percentageChange.toFixed(2)}%`} />
<Row lbl="Historical Average Rank" val={average.toLocaleString()} />
<div className="chart-container">
{Chart(chartData, data)}
</div>
</Card>
);
}
export default RankCard;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,27 @@
import colors from 'web-check-live/styles/colors';
import { Card } from 'web-check-live/components/Form/Card';
import Row from 'web-check-live/components/Form/Row';
const cardStyles = `
span.val {
&.up { color: ${colors.success}; }
&.down { color: ${colors.danger}; }
}
`;
const ServerStatusCard = (props: { data: any, title: string, actionButtons: any }): JSX.Element => {
const serverStatus = props.data;
return (
<Card heading={props.title.toString()} actionButtons={props.actionButtons} styles={cardStyles}>
<Row lbl="" val="">
<span className="lbl">Is Up?</span>
{ serverStatus.isUp ? <span className="val up"> Online</span> : <span className="val down"> Offline</span>}
</Row>
<Row lbl="Status Code" val={serverStatus.responseCode} />
{ serverStatus.responseTime && <Row lbl="Response Time" val={`${Math.round(serverStatus.responseTime)}ms`} /> }
</Card>
);
}
export default ServerStatusCard;

View File

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

View File

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

View File

@@ -0,0 +1,44 @@
import { Card } from 'web-check-live/components/Form/Card';
import Row from 'web-check-live/components/Form/Row';
import colors from 'web-check-live/styles/colors';
const cardStyles = `
.banner-image img {
width: 100%;
border-radius: 4px;
margin: 0.5rem 0;
}
.color-field {
border-radius: 4px;
&:hover {
color: ${colors.primary};
}
}
`;
const SocialTagsCard = (props: {data: any, title: string, actionButtons: any }): JSX.Element => {
const tags = props.data;
return (
<Card heading={props.title} actionButtons={props.actionButtons} styles={cardStyles}>
{ tags.title && <Row lbl="Title" val={tags.title} /> }
{ tags.description && <Row lbl="Description" val={tags.description} /> }
{ tags.keywords && <Row lbl="Keywords" val={tags.keywords} /> }
{ tags.canonicalUrl && <Row lbl="Canonical URL" val={tags.canonicalUrl} /> }
{ tags.themeColor && <Row lbl="" val="">
<span className="lbl">Theme Color</span>
<span className="val color-field" style={{background: tags.themeColor}}>{tags.themeColor}</span>
</Row> }
{ tags.twitterSite && <Row lbl="" val="">
<span className="lbl">Twitter Site</span>
<span className="val"><a href={`https://x.com/${tags.twitterSite}`}>{tags.twitterSite}</a></span>
</Row> }
{ tags.author && <Row lbl="Author" val={tags.author} />}
{ tags.publisher && <Row lbl="Publisher" val={tags.publisher} />}
{ tags.generator && <Row lbl="Generator" val={tags.generator} />}
{ tags.ogImage && <div className="banner-image"><img src={tags.ogImage} alt="Banner" /></div> }
</Card>
);
}
export default SocialTagsCard;

View File

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

View File

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

View File

@@ -0,0 +1,88 @@
import styled from '@emotion/styled';
import colors from 'web-check-live/styles/colors';
import { Card } from 'web-check-live/components/Form/Card';
import Row, { ExpandableRow } from 'web-check-live/components/Form/Row';
const Expandable = styled.details`
margin-top: 0.5rem;
cursor: pointer;
summary::marker {
color: ${colors.primary};
}
`;
const getExpandableTitle = (urlObj: any) => {
let pathName = '';
try {
pathName = new URL(urlObj.url).pathname;
} catch(e) {}
return `${pathName} (${urlObj.id})`;
}
const convertToDate = (dateString: string): string => {
const [date, time] = dateString.split(' ');
const [year, month, day] = date.split('-').map(Number);
const [hour, minute, second] = time.split(':').map(Number);
const dateObject = new Date(year, month - 1, day, hour, minute, second);
if (isNaN(dateObject.getTime())) {
return dateString;
}
return dateObject.toString();
}
const MalwareCard = (props: {data: any, title: string, actionButtons: any }): JSX.Element => {
const urlHaus = props.data.urlHaus || {};
const phishTank = props.data.phishTank || {};
const cloudmersive = props.data.cloudmersive || {};
const safeBrowsing = props.data.safeBrowsing || {};
return (
<Card heading={props.title} actionButtons={props.actionButtons}>
{ safeBrowsing && !safeBrowsing.error && (
<Row lbl="Google Safe Browsing" val={safeBrowsing.unsafe ? '❌ Unsafe' : '✅ Safe'} />
)}
{ ((cloudmersive && !cloudmersive.error) || safeBrowsing?.details) && (
<Row lbl="Threat Type" val={safeBrowsing?.details?.threatType || cloudmersive.WebsiteThreatType || 'None :)'} />
)}
{ phishTank && !phishTank.error && (
<Row lbl="Phishing Status" val={phishTank?.url0?.in_database !== 'false' ? '❌ Phishing Identified' : '✅ No Phishing Found'} />
)}
{ phishTank.url0 && phishTank.url0.phish_detail_page && (
<Row lbl="" val="">
<span className="lbl">Phish Info</span>
<span className="val"><a href={phishTank.url0.phish_detail_page}>{phishTank.url0.phish_id}</a></span>
</Row>
)}
{ urlHaus.query_status === 'no_results' && <Row lbl="Malware Status" val="✅ No Malwares Found" />}
{ urlHaus.query_status === 'ok' && (
<>
<Row lbl="Status" val="❌ Malware Identified" />
<Row lbl="First Seen" val={convertToDate(urlHaus.firstseen)} />
<Row lbl="Bad URLs Count" val={urlHaus.url_count} />
</>
)}
{urlHaus.urls && (
<Expandable>
<summary>Expand Results</summary>
{ urlHaus.urls.map((urlResult: any, index: number) => {
const rows = [
{ lbl: 'ID', val: urlResult.id },
{ lbl: 'Status', val: urlResult.url_status },
{ lbl: 'Date Added', val: convertToDate(urlResult.date_added) },
{ lbl: 'Threat Type', val: urlResult.threat },
{ lbl: 'Reported By', val: urlResult.reporter },
{ lbl: 'Takedown Time', val: urlResult.takedown_time_seconds },
{ lbl: 'Larted', val: urlResult.larted },
{ lbl: 'Tags', val: (urlResult.tags || []).join(', ') },
{ lbl: 'Reference', val: urlResult.urlhaus_reference },
{ lbl: 'File Path', val: urlResult.url },
];
return (<ExpandableRow lbl={getExpandableTitle(urlResult)} val="" rowList={rows} />)
})}
</Expandable>
)}
</Card>
);
}
export default MalwareCard;

View File

@@ -0,0 +1,70 @@
import { useState, useEffect } from 'react';
import { Card } from 'web-check-live/components/Form/Card';
import Button from 'web-check-live/components/Form/Button';
import { ExpandableRow } from 'web-check-live/components/Form/Row';
const makeCipherSuites = (results: any) => {
if (!results || !results.connection_info || (results.connection_info.ciphersuite || [])?.length === 0) {
return [];
}
return results.connection_info.ciphersuite.map((ciphersuite: any) => {
return {
title: ciphersuite.cipher,
fields: [
{ lbl: 'Code', val: ciphersuite.code },
{ lbl: 'Protocols', val: ciphersuite.protocols.join(', ') },
{ lbl: 'Pubkey', val: ciphersuite.pubkey },
{ lbl: 'Sigalg', val: ciphersuite.sigalg },
{ lbl: 'Ticket Hint', val: ciphersuite.ticket_hint },
{ lbl: 'OCSP Stapling', val: ciphersuite.ocsp_stapling ? '✅ Enabled' : '❌ Disabled' },
{ lbl: 'PFS', val: ciphersuite.pfs },
ciphersuite.curves ? { lbl: 'Curves', val: ciphersuite.curves.join(', ') } : {},
]};
});
};
const TlsCard = (props: {data: any, title: string, actionButtons: any }): JSX.Element => {
const [cipherSuites, setCipherSuites] = useState(makeCipherSuites(props.data));
const [loadState, setLoadState] = useState<undefined | 'loading' | 'success' | 'error'>(undefined);
useEffect(() => { // Update cipher suites when data changes
setCipherSuites(makeCipherSuites(props.data));
}, [props.data]);
const updateData = (id: number) => {
setCipherSuites([]);
setLoadState('loading');
const fetchUrl = `https://tls-observatory.services.mozilla.com/api/v1/results?id=${id}`;
fetch(fetchUrl)
.then((response) => response.json())
.then((data) => {
setCipherSuites(makeCipherSuites(data));
setLoadState('success');
}).catch((error) => {
setLoadState('error');
});
};
const scanId = props.data?.id;
return (
<Card heading={props.title} actionButtons={props.actionButtons}>
{ cipherSuites.length && cipherSuites.map((cipherSuite: any, index: number) => {
return (
<ExpandableRow key={`tls-${index}`} lbl={cipherSuite.title} val="" rowList={cipherSuite.fields} />
);
})}
{ !cipherSuites.length && (
<div>
<p>No cipher suites found.<br />
This sometimes happens when the report didn't finish generating in-time, you can try re-requesting it.
</p>
<Button loadState={loadState} onClick={() => updateData(scanId)}>Refetch Report</Button>
</div>
)}
</Card>
);
}
export default TlsCard;

View File

@@ -0,0 +1,84 @@
import { useState, useEffect } from 'react';
import { Card } from 'web-check-live/components/Form/Card';
import Button from 'web-check-live/components/Form/Button';
import { ExpandableRow } from 'web-check-live/components/Form/Row';
const makeClientSupport = (results: any) => {
if (!results?.analysis) return [];
const target = results.target;
const sslLabsClientSupport = (
results.analysis.find((a: any) => a.analyzer === 'sslLabsClientSupport')
).result;
return sslLabsClientSupport.map((sup: any) => {
return {
title: `${sup.name} ${sup.platform ? `(on ${sup.platform})`: sup.version}`,
value: sup.is_supported ? '✅' : '❌',
fields: sup.is_supported ? [
sup.curve ? { lbl: 'Curve', val: sup.curve } : {},
{ lbl: 'Protocol', val: sup.protocol },
{ lbl: 'Cipher Suite', val: sup.ciphersuite },
{ lbl: 'Protocol Code', val: sup.protocol_code },
{ lbl: 'Cipher Suite Code', val: sup.ciphersuite_code },
{ lbl: 'Curve Code', val: sup.curve_code },
] : [
{ lbl: '', val: '',
plaintext: `The host ${target} does not support ${sup.name} `
+`${sup.version ? `version ${sup.version} `: ''} `
+ `${sup.platform ? `on ${sup.platform} `: ''}`}
],
};
});
};
const TlsCard = (props: {data: any, title: string, actionButtons: any }): JSX.Element => {
const [clientSupport, setClientSupport] = useState(makeClientSupport(props.data));
const [loadState, setLoadState] = useState<undefined | 'loading' | 'success' | 'error'>(undefined);
useEffect(() => {
setClientSupport(makeClientSupport(props.data));
}, [props.data]);
const updateData = (id: number) => {
setClientSupport([]);
setLoadState('loading');
const fetchUrl = `https://tls-observatory.services.mozilla.com/api/v1/results?id=${id}`;
fetch(fetchUrl)
.then((response) => response.json())
.then((data) => {
setClientSupport(makeClientSupport(data));
setLoadState('success');
}).catch(() => {
setLoadState('error');
});
};
const scanId = props.data?.id;
return (
<Card heading={props.title} actionButtons={props.actionButtons}>
{clientSupport.map((support: any, index: number) => {
return (
<ExpandableRow
key={`tls-client-${index}`}
lbl={support.title}
val={support.value || '?'}
rowList={support.fields}
/>
)
})}
{ !clientSupport.length && (
<div>
<p>No entries available to analyze.<br />
This sometimes happens when the report didn't finish generating in-time, you can try re-requesting it.
</p>
<Button loadState={loadState} onClick={() => updateData(scanId)}>Refetch Report</Button>
</div>
)}
</Card>
);
}
export default TlsCard;

View File

@@ -0,0 +1,131 @@
import { useState, useEffect } from 'react';
import styled from '@emotion/styled';
import colors from 'web-check-live/styles/colors';
import { Card } from 'web-check-live/components/Form/Card';
import Button from 'web-check-live/components/Form/Button';
import Row, { ExpandableRow } from 'web-check-live/components/Form/Row';
const Expandable = styled.details`
margin-top: 0.5rem;
cursor: pointer;
summary::marker {
color: ${colors.primary};
}
`;
const makeExpandableData = (results: any) => {
if (!results || !results.analysis || results.analysis.length === 0) {
return [];
}
return results.analysis.map((analysis: any) => {
const fields = Object.keys(analysis.result).map((label) => {
const lbl = isNaN(parseInt(label, 10)) ? label : '';
const val = analysis.result[label] || 'None';
if (typeof val !== 'object') {
return { lbl, val };
}
return { lbl, val: '', plaintext: JSON.stringify(analysis.result[label])};
});
return {
title: analysis.analyzer,
value: analysis.success ? '✅' : '❌',
fields,
};
});
};
const makeResults = (results: any) => {
const rows: { lbl: string; val?: any; plaintext?: string; list?: string[] }[] = [];
if (!results || !results.analysis || results.analysis.length === 0) {
return rows;
}
const caaWorker = results.analysis.find((a: any) => a.analyzer === 'caaWorker');
if (caaWorker.result.host) rows.push({ lbl: 'Host', val: caaWorker.result.host });
if (typeof caaWorker.result.has_caa === 'boolean') rows.push({ lbl: 'CA Authorization', val: caaWorker.result.has_caa });
if (caaWorker.result.issue) rows.push({ lbl: 'CAAs allowed to Issue Certs', plaintext: caaWorker.result.issue.join('\n') });
const mozillaGradingWorker = (results.analysis.find((a: any) => a.analyzer === 'mozillaGradingWorker')).result;
if (mozillaGradingWorker.grade) rows.push({ lbl: 'Mozilla Grading', val: mozillaGradingWorker.grade });
if (mozillaGradingWorker.gradeTrust) rows.push({ lbl: 'Mozilla Trust', val: mozillaGradingWorker.gradeTrust });
const symantecDistrust = (results.analysis.find((a: any) => a.analyzer === 'symantecDistrust')).result;
if (typeof symantecDistrust.isDistrusted === 'boolean') rows.push({ lbl: 'No distrusted symantec SSL?', val: !symantecDistrust.isDistrusted });
if (symantecDistrust.reasons) rows.push({ lbl: 'Symantec Distrust', plaintext: symantecDistrust.reasons.join('\n') });
const top1m = (results.analysis.find((a: any) => a.analyzer === 'top1m')).result;
if (top1m.certificate.rank) rows.push({ lbl: 'Certificate Rank', val: top1m.certificate.rank.toLocaleString() });
const mozillaEvaluationWorker = (results.analysis.find((a: any) => a.analyzer === 'mozillaEvaluationWorker')).result;
if (mozillaEvaluationWorker.level) rows.push({ lbl: 'Mozilla Evaluation Level', val: mozillaEvaluationWorker.level });
if (mozillaEvaluationWorker.failures) {
const { bad, old, intermediate, modern } = mozillaEvaluationWorker.failures;
if (bad) rows.push({ lbl: `Critical Security Issues (${bad.length})`, list: bad });
if (old) rows.push({ lbl: `Compatibility Config Issues (${old.length})`, list: old });
if (intermediate) rows.push({ lbl: `Intermediate Issues (${intermediate.length})`, list: intermediate });
if (modern) rows.push({ lbl: `Modern Issues (${modern.length})`, list: modern });
}
return rows;
};
const TlsCard = (props: {data: any, title: string, actionButtons: any }): JSX.Element => {
const [tlsRowData, setTlsRowWata] = useState(makeExpandableData(props.data));
const [tlsResults, setTlsResults] = useState(makeResults(props.data));
const [loadState, setLoadState] = useState<undefined | 'loading' | 'success' | 'error'>(undefined);
useEffect(() => {
setTlsRowWata(makeExpandableData(props.data));
setTlsResults(makeResults(props.data));
}, [props.data]);
const updateData = (id: number) => {
setTlsRowWata([]);
setLoadState('loading');
const fetchUrl = `https://tls-observatory.services.mozilla.com/api/v1/results?id=${id}`;
fetch(fetchUrl)
.then((response) => response.json())
.then((data) => {
setTlsRowWata(makeExpandableData(data));
setTlsResults(makeResults(data));
setLoadState('success');
}).catch(() => {
setLoadState('error');
});
};
const scanId = props.data?.id;
return (
<Card heading={props.title} actionButtons={props.actionButtons}>
{ tlsResults.length > 0 && tlsResults.map((row: any, index: number) => {
return (
<Row
lbl={row.lbl}
val={row.val}
plaintext={row.plaintext}
listResults={row.list}
key={`tls-issues-${index}`}
/>
);
})}
<Expandable>
<summary>Full Analysis Results</summary>
{ tlsRowData.length > 0 && tlsRowData.map((cipherSuite: any, index: number) => {
return (
<ExpandableRow lbl={cipherSuite.title} val={cipherSuite.value || '?'} rowList={cipherSuite.fields} />
);
})}
</Expandable>
{ !tlsRowData.length && (
<div>
<p>No entries available to analyze.<br />
This sometimes happens when the report didn't finish generating in-time, you can try re-requesting it.
</p>
<Button loadState={loadState} onClick={() => updateData(scanId)}>Refetch Report</Button>
</div>
)}
</Card>
);
}
export default TlsCard;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,352 @@
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 id='fancy-background'>
<p><span className='dead'>0</span></p>
<p><span className='alive'>0</span></p>
<p><span className='drawn'>0</span></p>
<p><span className='fps'>0</span></p>
</div>
);
}
export default FancyBackground;

View File

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

View File

@@ -0,0 +1,61 @@
import styled from '@emotion/styled';
import colors from 'web-check-live/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="/check/about">Web-Check</Link> is
licensed under <Link href={licenseUrl}>MIT</Link> -
© <Link href={authorUrl}>Alicia Sykes</Link> 2023
</span>
</StyledFooter>
);
}
export default Footer;

View File

@@ -0,0 +1,103 @@
import styled from '@emotion/styled';
import { StyledCard } from 'web-check-live/components/Form/Card';
import Heading from 'web-check-live/components/Form/Heading';
import colors from 'web-check-live/styles/colors';
const LoaderContainer = styled(StyledCard)`
margin: 0 auto 1rem auto;
width: 95vw;
position: relative;
transition: all 0.2s ease-in-out;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
gap: 2rem;
height: 50vh;
transition: all 0.3s ease-in-out;
p.loadTimeInfo {
text-align: center;
margin: 0;
color: ${colors.textColorSecondary};
opacity: 0.5;
}
&.flex {
display: flex;
}
&.finished {
height: 0;
overflow: hidden;
opacity: 0;
margin: 0;
padding: 0;
svg { width: 0; }
h4 { font-size: 0; }
}
&.hide {
display: none;
}
`;
const StyledSvg = styled.svg`
width: 200px;
margin: 0 auto;
path {
fill: ${colors.primary};
&:nth-child(2) { opacity: 0.8; }
&:nth-child(3) { opacity: 0.5; }
}
`;
const Loader = (props: { show: boolean }): JSX.Element => {
return (
<LoaderContainer className={props.show ? '' : 'finished'}>
<Heading as="h4" color={colors.primary}>Crunching data...</Heading>
<StyledSvg version="1.1" id="L7" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px"
viewBox="0 0 100 100" enableBackground="new 0 0 100 100">
<path fill="#fff" d="M31.6,3.5C5.9,13.6-6.6,42.7,3.5,68.4c10.1,25.7,39.2,38.3,64.9,28.1l-3.1-7.9c-21.3,8.4-45.4-2-53.8-23.3
c-8.4-21.3,2-45.4,23.3-53.8L31.6,3.5z">
<animateTransform
attributeName="transform"
attributeType="XML"
type="rotate"
dur="2s"
from="0 50 50"
to="360 50 50"
repeatCount="indefinite" />
</path>
<path fill="#fff" d="M42.3,39.6c5.7-4.3,13.9-3.1,18.1,2.7c4.3,5.7,3.1,13.9-2.7,18.1l4.1,5.5c8.8-6.5,10.6-19,4.1-27.7
c-6.5-8.8-19-10.6-27.7-4.1L42.3,39.6z">
<animateTransform
attributeName="transform"
attributeType="XML"
type="rotate"
dur="1s"
from="0 50 50"
to="-360 50 50"
repeatCount="indefinite" />
</path>
<path fill="#fff" d="M82,35.7C74.1,18,53.4,10.1,35.7,18S10.1,46.6,18,64.3l7.6-3.4c-6-13.5,0-29.3,13.5-35.3s29.3,0,35.3,13.5
L82,35.7z">
<animateTransform
attributeName="transform"
attributeType="XML"
type="rotate"
dur="2s"
from="0 50 50"
to="360 50 50"
repeatCount="indefinite" />
</path>
</StyledSvg>
<p className="loadTimeInfo">
It may take up-to a minute for all jobs to complete<br />
You can view preliminary results as they come in below
</p>
</LoaderContainer>
);
}
export default Loader;

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,107 @@
import React, { useState } from 'react';
import styled from '@emotion/styled';
import colors from 'web-check-live/styles/colors';
import { Card } from 'web-check-live/components/Form/Card';
import Button from 'web-check-live/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;

View File

@@ -0,0 +1,108 @@
import { useState, useEffect } from 'react';
import { toast } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import type { LoadingState } from 'web-check-live/components/misc/ProgressBar';
import type { AddressType } from 'web-check-live/utils/address-type-checker';
interface UseIpAddressProps<ResultType = any> {
// Unique identifier for this job type
jobId: string | string[];
// The actual fetch request
fetchRequest: () => Promise<ResultType>;
// Function to call to update the loading state in parent
updateLoadingJobs: (job: string | string[], newState: LoadingState, error?: string, retry?: (data?: any) => void | null, data?: any) => void;
addressInfo: {
// The hostname/ip address that we're checking
address: string | undefined;
// The type of address (e.g. url, ipv4)
addressType: AddressType;
// The valid address types for this job
expectedAddressTypes: AddressType[];
};
}
type ResultType = any;
type ReturnType = [ResultType | undefined, (data?: any) => void];
const useMotherOfAllHooks = <ResultType = any>(params: UseIpAddressProps<ResultType>): ReturnType => {
// Destructure params
const { addressInfo, fetchRequest, jobId, updateLoadingJobs } = params;
const { address, addressType, expectedAddressTypes } = addressInfo;
// Build useState that will be returned
const [result, setResult] = useState<ResultType>();
// Fire off the HTTP fetch request, then set results and update loading / error state
const doTheFetch = () => {
return fetchRequest()
.then((res: any) => {
if (!res) { // No response :(
updateLoadingJobs(jobId, 'error', 'No response', reset);
} else if (res.error) { // Response returned an error message
if (res.error.includes("timed-out")) { // Specific handling for timeout errors
updateLoadingJobs(jobId, 'timed-out', res.error, reset);
} else {
updateLoadingJobs(jobId, 'error', res.error, reset);
}
} else if (res.skipped) { // Response returned a skipped message
updateLoadingJobs(jobId, 'skipped', res.skipped, reset);
} else { // Yay, everything went to plan :)
setResult(res);
updateLoadingJobs(jobId, 'success', '', undefined, res);
}
})
.catch((err) => {
// Something fucked up
updateLoadingJobs(jobId, 'error', err.error || err.message || 'Unknown error', reset);
throw err;
})
}
// For when the user manually re-triggers the job
const reset = (data: any) => {
// If data is provided, then update state
if (data && !(data instanceof Event) && !data?._reactName) {
setResult(data);
} else { // Otherwise, trigger a data re-fetch
updateLoadingJobs(jobId, 'loading');
const fetchyFetch = doTheFetch();
const toastOptions = {
pending: `Updating Data (${jobId})`,
success: `Completed (${jobId})`,
error: `Failed to update (${jobId})`,
skipped: `Skipped job (${jobId}), as no valid results for host`,
};
// Initiate fetch, and show progress toast
toast.promise(fetchyFetch, toastOptions).catch(() => {});
}
};
useEffect(() => {
// Still waiting for this upstream, cancel job
if (!address || !addressType) {
return;
}
// This job isn't needed for this address type, cancel job
if (!expectedAddressTypes.includes(addressType)) {
if (addressType !== 'empt') updateLoadingJobs(jobId, 'skipped');
return;
}
// Initiate the data fetching process
doTheFetch().catch(() => {});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [address, addressType]);
return [result, reset];
};
export default useMotherOfAllHooks;
// I really fucking hate TypeScript sometimes....
// Feels like a weak attempt at trying to make JavaScript less crappy,
// when the real solution would be to just switch to a proper, typed, safe language
// ... Either that, or I'm just really shit at it.

View File

@@ -0,0 +1,10 @@
import { BrowserRouter } from "react-router-dom";
import { StaticRouter } from "react-router-dom/server";
import App from "./App.tsx";
// import "./App.css";
export default ({ pathname }: { pathname: string }) => (
import.meta.env.SSR
? <StaticRouter location={pathname}><App/></StaticRouter>
: <BrowserRouter><App/></BrowserRouter>
)

View File

@@ -0,0 +1,23 @@
const colors = {
primary: '#9fef00',
primaryLighter: '#cff97a',
textColor: '#ffffff',
textColorSecondary: '#a4b1cd',
background: '#141d2b',
backgroundDarker: '#111927',
backgroundLighter: '#1a2332',
bgShadowColor: '#0f1620',
fgShadowColor: '#456602',
primaryTransparent: '#9fef0012',
// Action Colors
info: '#04e4f4',
success: '#20e253',
warning: '#f6f000',
error: '#fca016',
danger: '#f80363',
neutral: '#272f4d',
};
export default colors;

View File

@@ -0,0 +1,30 @@
export type InputSize = 'small' | 'medium' | 'large';
export const applySize = (inputSize?: InputSize) => {
const sizeSpecificStyles = {
small: `
font-size: 1rem;
border-radius: 0.25rem;
padding: 0.5rem 1rem;
margin: 0.5rem;
`,
medium: `
font-size: 1.5rem;
border-radius: 0.25rem;
padding: 0.75rem 1.5rem;
margin: 0.5rem;
`,
large: `
font-size: 2rem;
border-radius: 0.25rem;
padding: 1rem 1.75rem;
margin: 0.5rem;
`,
};
switch (inputSize) {
case 'small': return sizeSpecificStyles.small;
case 'medium': return sizeSpecificStyles.medium;
case 'large': return sizeSpecificStyles.large;
default: return sizeSpecificStyles.small;
}
};

View File

@@ -0,0 +1,17 @@
import { Global, css } from '@emotion/react';
const GlobalStyles = () => (
<Global
styles={css`
@font-face {
font-family: PTMono;
font-style: normal;
font-weight: 400;
src: url('/fonts/PTMono.ttf') format('ttf');
}
body { font-family: PTMono; }
`}
/>
);
export default GlobalStyles;

View File

@@ -0,0 +1,51 @@
@font-face {
font-family: 'PTMono';
src: url('/fonts/PTMono-Regular.ttf') format('truetype');
font-weight: normal;
font-style: normal;
}
html {
scroll-behavior: smooth;
}
body {
margin: 0;
font-family: 'PTMono', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background: #141d2b;
color: #fff;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
#fancy-background { color: var(--background, #141d2b); }
::selection {
background: var(--primary, #9fef00);
color: var(--background, #141d2b);
}
svg.rsm-svg {
background: #2a3240;
border-radius: 6px;
}
:root {
--toastify-color-dark: #111927 !important;
--toastify-color-info: #04e4f4 !important;
--toastify-color-success: #20e253 !important;
--toastify-color-warning: #f6f000 !important;
--toastify-color-error: #f80363 !important;
}
#fancy-background {
position: absolute;
}

View File

@@ -0,0 +1,23 @@
export const TextSizes = {
xSmall: '0.75rem',
small: '1rem',
medium: '1.5rem',
large: '2rem',
xLarge: '3rem',
xxLarge: '4rem',
};
export const TextReset = `
font-size: ${TextSizes.small};
font-family: PTMono, Helvetica, Arial, sans-serif;
font-weight: 400;
font-style: normal;
font-stretch: normal;
line-height: normal;
letter-spacing: 0.38px;
margin: 0;
padding: 0;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
`;

View File

@@ -0,0 +1 @@
declare module "*.ttf"

View File

@@ -0,0 +1 @@
declare module 'react-simple-maps';

View File

@@ -0,0 +1,72 @@
/**
* Helper functions to determine if a string is a valid web address,
* and what type of address it is: URL, IPv4, IPv6 or none of those.
*/
export type AddressType = 'ipV4' | 'ipV6' | 'url' | 'err' | 'empt';
/* Checks if a given string looks like a URL */
const isUrl = (value: string):boolean => {
var urlPattern = new RegExp('^(https?:\\/\\/)?'+ // validate protocol
'((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|'+ // validate domain name
'((\\d{1,3}\\.){3}\\d{1,3}))'+ // validate OR ip (v4) address
'(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*'+ // validate port and path
'(\\?[;&a-z\\d%_.~+=-]*)?'+ // validate query string
'(\\#[-a-z\\d_]*)?$','i'); // validate fragment locator
return urlPattern.test(value);
};
// /* Checks if a given string looks like a URL */
// const isUrl = (value: string):boolean => {
// const urlRegex= new RegExp(''
// // + /(?:(?:(https?|ftp):)?\/\/)/.source
// + /(?:([^:\n\r]+):([^@\n\r]+)@)?/.source
// + /(?:(?:www\.)?([^/\n\r]+))/.source
// + /(\/[^?\n\r]+)?/.source
// + /(\?[^#\n\r]*)?/.source
// + /(#?[^\n\r]*)?/.source
// );
// return urlRegex.test(value);
// };
/* Checks if a given string looks like an IP Version 4 Address */
const isIpV4 = (value: string): boolean => {
const ipPart = '(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)';
const ipV4Regex = new RegExp(`^${ipPart}.${ipPart}.${ipPart}.${ipPart}$`);
return ipV4Regex.test(value);
};
/* Checks if a given string looks like an IP Version 6 Address */
const isIPv6 = (value: string): boolean => {
const components = value.split(':');
if ((components.length < 2 || components.length > 8) ||
((components[0] !== '' || components[1] !== '')
&& !components[0].match(/^[\da-f]{1,4}/i))
) return false;
let numberOfZeroCompressions = 0;
for (let i = 1; i < components.length; ++i) {
if (components[i] === '') {
++numberOfZeroCompressions;
if (numberOfZeroCompressions > 1) return false;
continue;
}
if (!components[i].match(/^[\da-f]{1,4}/i)) return false;
}
return true;
};
const isShort = (value: string): boolean => {
return (!value || value.length <=3);
};
/* Returns the address type for a given address */
export const determineAddressType = (address: string | undefined): AddressType => {
if (!address) return 'err';
if (isShort(address)) return 'empt';
if (isUrl(address)) return 'url';
if (isIpV4(address)) return 'ipV4';
if (isIPv6(address)) return 'ipV6';
return 'err';
};

View File

@@ -0,0 +1,573 @@
export interface Doc {
id: string;
title: string;
description: string;
use: string;
resources: string[] | { title: string, link: string}[];
screenshot?: string;
}
const docs: Doc[] = [
{
id: "get-ip",
title: "IP Info",
description:
"An IP address (Internet Protocol address) is a numerical label assigned to each device connected to a network / the internet. The IP associated with a given domain can be found by querying the Domain Name System (DNS) for the domain's A (address) record.",
use: "Finding the IP of a given server is the first step to conducting further investigations, as it allows us to probe the server for additional info. Including creating a detailed map of a target's network infrastructure, pinpointing the physical location of a server, identifying the hosting service, and even discovering other domains that are hosted on the same IP address.",
resources: [
{ title: 'Understanding IP Addresses', link: 'https://www.digitalocean.com/community/tutorials/understanding-ip-addresses-subnets-and-cidr-notation-for-networking'},
{ title: 'IP Addresses - Wiki', link: 'https://en.wikipedia.org/wiki/IP_address'},
{ title: 'RFC-791 Internet Protocol', link: 'https://tools.ietf.org/html/rfc791'},
{ title: 'whatismyipaddress.com', link: 'https://whatismyipaddress.com/'},
],
},
{
id: "ssl",
title: "SSL Chain",
description:
"SSL certificates are digital certificates that authenticate the identity of a website or server, enable secure encrypted communication (HTTPS), and establish trust between clients and servers. A valid SSL certificate is required for a website to be able to use the HTTPS protocol, and encrypt user + site data in transit. SSL certificates are issued by Certificate Authorities (CAs), which are trusted third parties that verify the identity and legitimacy of the certificate holder.",
use: "SSL certificates not only provide the assurance that data transmission to and from the website is secure, but they also provide valuable OSINT data. Information from an SSL certificate can include the issuing authority, the domain name, its validity period, and sometimes even organization details. This can be useful for verifying the authenticity of a website, understanding its security setup, or even for discovering associated subdomains or other services.",
resources: [
{ title: 'TLS - Wiki', link: 'https://en.wikipedia.org/wiki/Transport_Layer_Security'},
{ title: 'What is SSL (via Cloudflare learning)', link: 'https://www.cloudflare.com/learning/ssl/what-is-ssl/'},
{ title: 'RFC-8446 - TLS', link: 'https://tools.ietf.org/html/rfc8446'},
{ title: 'SSL Checker', link: 'https://www.sslshopper.com/ssl-checker.html'},
],
screenshot: 'https://i.ibb.co/kB7LsV1/wc-ssl.png',
},
{
id: "dns",
title: "DNS Records",
description:
"This task involves looking up the DNS records associated with a specific domain. DNS is a system that translates human-readable domain names into IP addresses that computers use to communicate. Various types of DNS records exist, including A (address), MX (mail exchange), NS (name server), CNAME (canonical name), and TXT (text), among others.",
use: "Extracting DNS records can provide a wealth of information in an OSINT investigation. For example, A and AAAA records can disclose IP addresses associated with a domain, potentially revealing the location of servers. MX records can give clues about a domain's email provider. TXT records are often used for various administrative purposes and can sometimes inadvertently leak internal information. Understanding a domain's DNS setup can also be useful in understanding how its online infrastructure is built and managed.",
resources: [
{ title: 'What are DNS records? (via Cloudflare learning)', link: 'https://www.cloudflare.com/learning/dns/dns-records/'},
{ title: 'DNS Record Types', link: 'https://en.wikipedia.org/wiki/List_of_DNS_record_types'},
{ title: 'RFC-1035 - DNS', link: 'https://tools.ietf.org/html/rfc1035'},
{ title: 'DNS Lookup (via MxToolbox)', link: 'https://mxtoolbox.com/DNSLookup.aspx'},
],
screenshot: 'https://i.ibb.co/7Q1kMwM/wc-dns.png',
},
{
id: "cookies",
title: "Cookies",
description:
"The Cookies task involves examining the HTTP cookies set by the target website. Cookies are small pieces of data stored on the user's computer by the web browser while browsing a website. They hold a modest amount of data specific to a particular client and website, such as site preferences, the state of the user's session, or tracking information.",
use: "Cookies can disclose information about how the website tracks and interacts with its users. For instance, session cookies can reveal how user sessions are managed, and tracking cookies can hint at what kind of tracking or analytics frameworks are being used. Additionally, examining cookie policies and practices can offer insights into the site's security settings and compliance with privacy regulations.",
resources: [
{ title: 'HTTP Cookie Docs (Mozilla)', link: 'https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies' },
{ title: 'What are Cookies (via Cloudflare Learning)', link: 'https://www.cloudflare.com/learning/privacy/what-are-cookies/' },
{ title: 'Testing for Cookie Attributes (OWASP)', link: 'https://owasp.org/www-project-web-security-testing-guide/v42/4-Web_Application_Security_Testing/06-Session_Management_Testing/02-Testing_for_Cookies_Attributes' },
{ title: 'RFC-6265 - Coolies', link: 'https://tools.ietf.org/html/rfc6265' },
],
screenshot: 'https://i.ibb.co/TTQ6DtP/wc-cookies.png',
},
{
id: "robots-txt",
title: "Crawl Rules",
description:
"Robots.txt is a file found (usually) at the root of a domain, and is used to implement the Robots Exclusion Protocol (REP) to indicate which pages should be ignored by which crawlers and bots. It's good practice to avoid search engine crawlers from over-loading your site, but should not be used to keep pages out of search results (use the noindex meta tag or header instead).",
use: "It's often useful to check the robots.txt file during an investigation, as it can sometimes disclose the directories and pages that the site owner doesn't want to be indexed, potentially because they contain sensitive information, or reveal the existence of otherwise hidden or unlinked directories. Additionally, understanding crawl rules may offer insights into a website's SEO strategies.",
resources: [
{ title: 'Google Search Docs - Robots.txt', link: 'https://developers.google.com/search/docs/advanced/robots/intro' },
{ title: 'Learn about robots.txt (via Moz.com)', link: 'https://moz.com/learn/seo/robotstxt' },
{ title: 'RFC-9309 - Robots Exclusion Protocol', link: 'https://datatracker.ietf.org/doc/rfc9309/' },
{ title: 'Robots.txt - wiki', link: 'https://en.wikipedia.org/wiki/Robots_exclusion_standard' },
],
screenshot: 'https://i.ibb.co/KwQCjPf/wc-robots.png',
},
{
id: "headers",
title: "Headers",
description:
"The Headers task involves extracting and interpreting the HTTP headers sent by the target website during the request-response cycle. HTTP headers are key-value pairs sent at the start of an HTTP response, or before the actual data. Headers contain important directives for how to handle the data being transferred, including cache policies, content types, encoding, server information, security policies, and more.",
use: "Analyzing HTTP headers can provide significant insights in an OSINT investigation. Headers can reveal specific server configurations, chosen technologies, caching directives, and various security settings. This information can help to determine a website's underlying technology stack, server-side security measures, potential vulnerabilities, and general operational practices.",
resources: [
{ title: 'HTTP Headers - Docs', link: 'https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers' },
{ title: 'RFC-7231 Section 7 - Headers', link: 'https://datatracker.ietf.org/doc/html/rfc7231#section-7' },
{ title: 'List of header response fields', link: 'https://en.wikipedia.org/wiki/List_of_HTTP_header_fields' },
{ title: 'OWASP Secure Headers Project', link: 'https://owasp.org/www-project-secure-headers/' },
],
screenshot: 'https://i.ibb.co/t3xcwP1/wc-headers.png',
},
{
id: "quality",
title: "Quality Metrics",
description:
"Using Lighthouse, the Quality Metrics task measures the performance, accessibility, best practices, and SEO of the target website. This returns a simple checklist of 100 core metrics, along with a score for each category, to gauge the overall quality of a given site.",
use: "Useful for assessing a site's technical health, SEO issues, identify vulnerabilities, and ensure compliance with standards.",
resources: [
{ title: 'Lighthouse Docs', link: 'https://developer.chrome.com/docs/lighthouse/' },
{ title: 'Google Page Speed Tools', link: 'https://developers.google.com/speed' },
{ title: 'W3 Accessibility Tools', link: 'https://www.w3.org/WAI/test-evaluate/' },
{ title: 'Google Search Console', link: 'https://search.google.com/search-console' },
{ title: 'SEO Checker', link: 'https://www.seobility.net/en/seocheck/' },
{ title: 'PWA Builder', link: 'https://www.pwabuilder.com/' },
],
screenshot: 'https://i.ibb.co/Kqg8rx7/wc-quality.png',
},
{
id: "location",
title: "Server Location",
description:
"The Server Location task determines the physical location of the server hosting a given website based on its IP address. This is done by looking up the IP in a location database, which maps the IP to a lat + long of known data centers and ISPs. From the latitude and longitude, it's then possible to show additional contextual info, like a pin on the map, along with address, flag, time zone, currency, etc.",
use: "Knowing the server location is a good first step in better understanding a website. For site owners this aids in optimizing content delivery, ensuring compliance with data residency requirements, and identifying potential latency issues that may impact user experience in specific geographical regions. And for security researcher, assess the risk posed by specific regions or jurisdictions regarding cyber threats and regulations.",
resources: [
{ title: 'IP Locator', link: 'https://geobytes.com/iplocator/' },
{ title: 'Internet Geolocation - Wiki', link: 'https://en.wikipedia.org/wiki/Internet_geolocation' },
],
screenshot: 'https://i.ibb.co/cXH2hfR/wc-location.png',
},
{
id: "hosts",
title: "Associated Hosts",
description:
"This task involves identifying and listing all domains and subdomains (hostnames) that are associated with the website's primary domain. This process often involves DNS enumeration to discover any linked domains and hostnames, as well as looking at known DNS records.",
use: "During an investigation, understanding the full scope of a target's web presence is critical. Associated domains could lead to uncovering related projects, backup sites, development/test sites, or services linked to the main site. These can sometimes provide additional information or potential security vulnerabilities. A comprehensive list of associated domains and hostnames can also give an overview of the organization's structure and online footprint.",
resources: [
{ title: 'DNS Enumeration - Wiki', link: 'https://en.wikipedia.org/wiki/DNS_enumeration' },
{ title: 'OWASP - Enumerate Applications on Webserver', link: 'https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/01-Information_Gathering/04-Enumerate_Applications_on_Webserver' },
{ title: 'DNS Enumeration - DNS Dumpster', link: 'https://dnsdumpster.com/' },
{ title: 'Subdomain Finder', link: 'https://subdomainfinder.c99.nl/' },
],
screenshot: 'https://i.ibb.co/25j1sT7/wc-hosts.png',
},
{
id: "redirects",
title: "Redirect Chain",
description:
"This task traces the sequence of HTTP redirects that occur from the original URL to the final destination URL. An HTTP redirect is a response with a status code that advises the client to go to another URL. Redirects can occur for several reasons, such as URL normalization (directing to the www version of the site), enforcing HTTPS, URL shorteners, or forwarding users to a new site location.",
use: "Understanding the redirect chain can be useful for several reasons. From a security perspective, long or complicated redirect chains can be a sign of potential security risks, such as unencrypted redirects in the chain. Additionally, redirects can impact website performance and SEO, as each redirect introduces additional round-trip-time (RTT). For OSINT, understanding the redirect chain can help identify relationships between different domains or reveal the use of certain technologies or hosting providers.",
resources: [
{ title: 'HTTP Redirects - MDN', link: 'https://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections' },
{ title: 'URL Redirection - Wiki', link: 'https://en.wikipedia.org/wiki/URL_redirection' },
{ title: '301 Redirects explained', link: 'https://ahrefs.com/blog/301-redirects/' },
],
screenshot: 'https://i.ibb.co/hVVrmwh/wc-redirects.png',
},
{
id: "txt-records",
title: "TXT Records",
description:
"TXT records are a type of DNS record that provides text information to sources outside your domain. They can be used for a variety of purposes, such as verifying domain ownership, ensuring email security, and even preventing unauthorized changes to your website.",
use: "The TXT records often reveal which external services and technologies are being used with a given domain. They may reveal details about the domain's email configuration, the use of specific services like Google Workspace or Microsoft 365, or security measures in place such as SPF and DKIM. Understanding these details can give an insight into the technologies used by the organization, their email security practices, and potential vulnerabilities.",
resources: [
{ title: 'TXT Records (via Cloudflare Learning)', link: 'https://www.cloudflare.com/learning/dns/dns-records/dns-txt-record/' },
{ title: 'TXT Records - Wiki', link: 'https://en.wikipedia.org/wiki/TXT_record' },
{ title: 'RFC-1464 - TXT Records', link: 'https://datatracker.ietf.org/doc/html/rfc1464' },
{ title: 'TXT Record Lookup (via MxToolbox)', link: 'https://mxtoolbox.com/TXTLookup.aspx' },
],
screenshot: 'https://i.ibb.co/wyt21QN/wc-txt-records.png',
},
{
id: "status",
title: "Server Status",
description: "Checks if a server is online and responding to requests.",
use: "",
resources: [
],
screenshot: 'https://i.ibb.co/V9CNLBK/wc-status.png',
},
{
id: "ports",
title: "Open Ports",
description:
"Open ports on a server are endpoints of communication which are available for establishing connections with clients. Each port corresponds to a specific service or protocol, such as HTTP (port 80), HTTPS (port 443), FTP (port 21), etc. The open ports on a server can be determined using techniques such as port scanning.",
use: "Knowing which ports are open on a server can provide information about the services running on that server, useful for understanding the potential vulnerabilities of the system, or for understanding the nature of the services the server is providing.",
resources: [
{ title: 'List of TCP & UDP Port Numbers', link: 'https://en.wikipedia.org/wiki/List_of_TCP_and_UDP_port_numbers' },
{ title: 'NMAP - Port Scanning Basics', link: 'https://nmap.org/book/man-port-scanning-basics.html' },
],
screenshot: 'https://i.ibb.co/F8D1hmf/wc-ports.png',
},
{
id: "trace-route",
title: "Traceroute",
description:
"Traceroute is a network diagnostic tool used to track in real-time the pathway taken by a packet of information from one system to another. It records each hop along the route, providing details about the IPs of routers and the delay at each point.",
use: "In OSINT investigations, traceroute can provide insights about the routing paths and geography of the network infrastructure supporting a website or service. This can help to identify network bottlenecks, potential censorship or manipulation of network traffic, and give an overall sense of the network's structure and efficiency. Additionally, the IP addresses collected during the traceroute may provide additional points of inquiry for further OSINT investigation.",
resources: [
"https://www.cloudflare.com/learning/network-layer/what-is-traceroute/",
"https://tools.ietf.org/html/rfc1393",
"https://en.wikipedia.org/wiki/Traceroute",
"https://www.ripe.net/publications/docs/ripe-611",
],
screenshot: 'https://i.ibb.co/M59qgxP/wc-trace-route.png',
},
{
id: "carbon",
title: "Carbon Footprint",
description:
"This task calculates the estimated carbon footprint of a website. It's based on the amount of data being transferred and processed, and the energy usage of the servers that host and deliver the website. The larger the website and the more complex its features, the higher its carbon footprint is likely to be.",
use: "From an OSINT perspective, understanding a website's carbon footprint doesn't directly provide insights into its internal workings or the organization behind it. However, it can still be valuable data in broader analyses, especially in contexts where environmental impact is a consideration. For example, it can be useful for activists, researchers, or ethical hackers who are interested in the sustainability of digital infrastructure, and who want to hold organizations accountable for their environmental impact.",
resources: [
{ title: 'WebsiteCarbon - Carbon Calculator', link: 'https://www.websitecarbon.com/' },
{ title: 'The Green Web Foundation', link: 'https://www.thegreenwebfoundation.org/' },
{ title: 'The Eco Friendly Web Alliance', link: 'https://ecofriendlyweb.org/' },
{ title: 'Reset.org', link: 'https://en.reset.org/' },
{ title: 'Your website is killing the planet - via Wired', link: 'https://www.wired.co.uk/article/internet-carbon-footprint' },
],
screenshot: 'https://i.ibb.co/5v6fSyw/Screenshot-from-2023-07-29-19-07-50.png',
},
{
id: "server-info",
title: "Server Info",
description:
"This task retrieves various pieces of information about the server hosting the target website. This can include the server type (e.g., Apache, Nginx), the hosting provider, the Autonomous System Number (ASN), and more. The information is usually obtained through a combination of IP address lookups and analysis of HTTP response headers.",
use: "In an OSINT context, server information can provide valuable clues about the organization behind a website. For instance, the choice of hosting provider could suggest the geographical region in which the organization operates, while the server type could hint at the technologies used by the organization. The ASN could also be used to find other domains hosted by the same organization.",
resources: [
"https://en.wikipedia.org/wiki/List_of_HTTP_header_fields",
"https://en.wikipedia.org/wiki/Autonomous_system_(Internet)",
"https://tools.ietf.org/html/rfc7231#section-7.4.2",
"https://builtwith.com/",
],
screenshot: 'https://i.ibb.co/Mk1jx32/wc-server.png',
},
{
id: "domain",
title: "Whois Lookup",
description:
"This task retrieves Whois records for the target domain. Whois records are a rich source of information, including the name and contact information of the domain registrant, the domain's creation and expiration dates, the domain's nameservers, and more. The information is usually obtained through a query to a Whois database server.",
use: "In an OSINT context, Whois records can provide valuable clues about the entity behind a website. They can show when the domain was first registered and when it's set to expire, which could provide insights into the operational timeline of the entity. The contact information, though often redacted or anonymized, can sometimes lead to additional avenues of investigation. The nameservers could also be used to link together multiple domains owned by the same entity.",
resources: [
"https://en.wikipedia.org/wiki/WHOIS",
"https://www.icann.org/resources/pages/whois-2018-01-17-en",
"https://whois.domaintools.com/",
],
screenshot: 'https://i.ibb.co/89WLp14/wc-domain.png',
},
{
id: "whois",
title: "Domain Info",
description:
"This task retrieves Whois records for the target domain. Whois records are a rich source of information, including the name and contact information of the domain registrant, the domain's creation and expiration dates, the domain's nameservers, and more. The information is usually obtained through a query to a Whois database server.",
use: "In an OSINT context, Whois records can provide valuable clues about the entity behind a website. They can show when the domain was first registered and when it's set to expire, which could provide insights into the operational timeline of the entity. The contact information, though often redacted or anonymized, can sometimes lead to additional avenues of investigation. The nameservers could also be used to link together multiple domains owned by the same entity.",
resources: [
"https://en.wikipedia.org/wiki/WHOIS",
"https://www.icann.org/resources/pages/whois-2018-01-17-en",
"https://whois.domaintools.com/",
],
screenshot: 'https://i.ibb.co/89WLp14/wc-domain.png',
},
{
id: "dnssec",
title: "DNS Security Extensions",
description:
"Without DNSSEC, it's possible for MITM attackers to spoof records and lead users to phishing sites. This is because the DNS system includes no built-in methods to verify that the response to the request was not forged, or that any other part of the process wasnt interrupted by an attacker. The DNS Security Extensions (DNSSEC) secures DNS lookups by signing your DNS records using public keys, so browsers can detect if the response has been tampered with. Another solution to this issue is DoH (DNS over HTTPS) and DoT (DNS over TLD).",
use: "DNSSEC information provides insight into an organization's level of cybersecurity maturity and potential vulnerabilities, particularly around DNS spoofing and cache poisoning. If no DNS secururity (DNSSEC, DoH, DoT, etc) is implemented, this may provide an entry point for an attacker.",
resources: [
"https://dnssec-analyzer.verisignlabs.com/",
"https://www.cloudflare.com/dns/dnssec/how-dnssec-works/",
"https://en.wikipedia.org/wiki/Domain_Name_System_Security_Extensions",
"https://www.icann.org/resources/pages/dnssec-what-is-it-why-important-2019-03-05-en",
"https://support.google.com/domains/answer/6147083",
"https://www.internetsociety.org/resources/deploy360/2013/dnssec-test-sites/",
],
screenshot: 'https://i.ibb.co/J54zVmQ/wc-dnssec.png',
},
{
id: "features",
title: "Site Features",
description: 'Checks which core features are present on a site. If a feature as marked as dead, that means it\'s not being actively used at load time',
use: "This is useful to understand what a site is capable of, and what technologies to look for",
resources: [],
screenshot: 'https://i.ibb.co/gP4P6kp/wc-features.png',
},
{
id: "hsts",
title: "HTTP Strict Transport Security",
description: 'HTTP Strict Transport Security (HSTS) is a web security policy '
+'mechanism that helps protect websites against protocol downgrade attacks and '
+ 'cookie hijacking. A website can be included in the HSTS preload list by '
+ 'conforming to a set of requirements and then submitting itself to the list.',
use: `There are several reasons why it's important for a site to be HSTS enabled:
1. User bookmarks or manually types http://example.com and is subject to a man-in-the-middle attacker
HSTS automatically redirects HTTP requests to HTTPS for the target domain
2. Web application that is intended to be purely HTTPS inadvertently contains HTTP links or serves content over HTTP
HSTS automatically redirects HTTP requests to HTTPS for the target domain
3. A man-in-the-middle attacker attempts to intercept traffic from a victim user using an invalid certificate and hopes the user will accept the bad certificate
HSTS does not allow a user to override the invalid certificate message
`,
resources: [
'https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security',
'https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Strict_Transport_Security_Cheat_Sheet.html',
'https://hstspreload.org/'
],
screenshot: 'https://i.ibb.co/k253fq4/Screenshot-from-2023-07-17-20-10-52.png',
},
{
id: 'dns-server',
title: 'DNS Server',
description: 'This check determines the DNS server(s) that the requested URL / IP resolves to. Also fires off a rudimentary check to see if the DNS server supports DoH, and weather it\'s vulnerable to DNS cache poisoning.',
use: '',
resources: [],
screenshot: 'https://i.ibb.co/tKpL8F9/Screenshot-from-2023-08-12-15-43-12.png',
},
{
id: 'tech-stack',
title: 'Tech Stack',
description: 'Checks what technologies a site is built with. '
+ 'This is done by fetching and parsing the site, then comparing it against a bit list of RegEx maintained by Wappalyzer to identify the unique fingerprints that different technologies leave.',
use: 'Identifying a website\'s tech stack aids in evaluating its security by exposing potential vulnerabilities, '
+ 'informs competitive analyses and development decisions, and can guide tailored marketing strategies. '
+ 'Ethical application of this knowledge is crucial to avoid harmful activities like data theft or unauthorized intrusion.',
resources: [
{ title: 'Wappalyzer fingerprints', link: 'https://github.com/wappalyzer/wappalyzer/tree/master/src/technologies'},
{ title: 'BuiltWith - Check what tech a site is using', link: 'https://builtwith.com/'},
],
screenshot: 'https://i.ibb.co/bBQSQNz/Screenshot-from-2023-08-12-15-43-46.png',
},
{
id: 'sitemap',
title: 'Listed Pages',
description: 'This job finds and parses a site\'s listed sitemap. This file lists public sub-pages on the site, which the author wishes to be crawled by search engines. Sitemaps help with SEO, but are also useful for seeing all a sites public content at a glance.',
use: 'Understand the structure of a site\'s public-facing content, and for site-owners, check that you\'re site\'s sitemap is accessible, parsable and contains everything you wish it to.',
resources: [
{ title: 'Learn about Sitemaps', link: 'https://developers.google.com/search/docs/crawling-indexing/sitemaps/overview'},
{ title: 'Sitemap XML spec', link: 'https://www.sitemaps.org/protocol.html'},
{ title: 'Sitemap tutorial', link: 'https://www.conductor.com/academy/xml-sitemap/'},
],
screenshot: 'https://i.ibb.co/GtrCQYq/Screenshot-from-2023-07-21-12-28-38.png',
},
{
id: 'security-txt',
title: 'Security.txt',
description: "The security.txt file tells researchers how they can responsibly disclose any security issues found on your site. "
+ "The standard was proposed in RFC 9116, and specifies that this file should include a point of contact (email address), "
+ "as well as optionally other info, like a link to the security disclosure policy, PGP key, proffered language, policy expiry and more. "
+ "The file should be located at the root of your domain, either at /security.txt or /.well-known/security.txt.",
use: "This is important, as without a defined point of contact a security researcher may be unable to report a critical security issue, "
+ "or may use insecure or possibly public channels to do so. From an OSINT perspective, you may also glean info about a site including "
+ "their posture on security, their CSAF provider, and meta data from the PGP public key.",
resources: [
{ title: 'securitytxt.org', link: 'https://securitytxt.org/'},
{ title: 'RFC-9116 Proposal', link: 'https://datatracker.ietf.org/doc/html/rfc9116'},
{ title: 'RFC-9116 History', link: 'https://datatracker.ietf.org/doc/rfc9116/'},
{ title: 'Security.txt (Wikipedia)', link: 'https://en.wikipedia.org/wiki/Security.txt'},
{ title: 'Example security.txt (Cloudflare)', link: 'https://www.cloudflare.com/.well-known/security.txt'},
{ title: 'Tutorial for creating security.txt (Pieter Bakker)', link: 'https://pieterbakker.com/implementing-security-txt/'},
],
screenshot: 'https://i.ibb.co/tq1FT5r/Screenshot-from-2023-07-24-20-31-21.png',
},
{
id: 'linked-pages',
title: 'Linked Pages',
description: 'Displays all internal and external links found on a site, identified by the href attributes attached to anchor elements.',
use: "For site owners, this is useful for diagnosing SEO issues, improving the site structure, understanding how content is inter-connected. External links can show partnerships, dependencies, and potential reputation risks. " +
"From a security standpoint, the outbound links can help identify any potential malicious or compromised sites the website is unknowingly linking to. Analyzing internal links can aid in understanding the site's structure and potentially uncover hidden or vulnerable pages which are not intended to be public. " +
"And for an OSINT investigator, it can aid in building a comprehensive understanding of the target, uncovering related entities, resources, or even potential hidden parts of the site.",
resources: [
{ title: 'W3C Link Checker', link: 'https://validator.w3.org/checklink'},
],
screenshot: 'https://i.ibb.co/LtK14XR/Screenshot-from-2023-07-29-11-16-44.png',
},
{
id: 'social-tags',
title: 'Social Tags',
description: 'Websites can include certain meta tags, that tell search engines and social media platforms what info to display. This usually includes a title, description, thumbnail, keywords, author, social accounts, etc.',
use: 'Adding this data to your site will boost SEO, and as an OSINT researcher it can be useful to understand how a given web app describes itself',
resources: [
{ title: 'SocialSharePreview.com', link: 'https://socialsharepreview.com/'},
{ title: 'The guide to social meta tags', link: 'https://css-tricks.com/essential-meta-tags-social-media/'},
{ title: 'Web.dev metadata tags', link: 'https://web.dev/learn/html/metadata/'},
{ title: 'Open Graph Protocol', link: 'https://ogp.me/'},
{ title: 'Twitter Cards', link: 'https://developer.twitter.com/en/docs/twitter-for-websites/cards/overview/abouts-cards'},
{ title: 'Facebook Open Graph', link: 'https://developers.facebook.com/docs/sharing/webmasters'},
],
screenshot: 'https://i.ibb.co/4srTT1w/Screenshot-from-2023-07-29-11-15-27.png',
},
{
id: 'mail-config',
title: 'Email Configuration',
description: "DMARC (Domain-based Message Authentication, Reporting & Conformance): DMARC is an email authentication protocol that works with SPF and DKIM to prevent email spoofing and phishing. It allows domain owners to specify how to handle unauthenticated mail via a published policy in DNS, and provides a way for receiving mail servers to send feedback about emails' compliance to the sender. " +
"BIMI (Brand Indicators for Message Identification): BIMI is an emerging email standard that enables organizations to display a logo in their customers' email clients automatically. BIMI ties the logo to the domain's DMARC record, providing another level of visual assurance to recipients that the email is legitimate. " +
"DKIM (DomainKeys Identified Mail): DKIM is an email security standard designed to make sure that messages were not altered in transit between the sending and recipient servers. It uses digital signatures linked to the domain of the sender to verify the sender and ensure message integrity. " +
"SPF (Sender Policy Framework): SPF is an email authentication method designed to prevent email spoofing. It specifies which mail servers are authorized to send email on behalf of a domain by creating a DNS record. This helps protect against spam by providing a way for receiving mail servers to check that incoming mail from a domain comes from a host authorized by that domain's administrators.",
use: "This information is helpful for researchers as it helps assess a domain's email security posture, uncover potential vulnerabilities, and verify the legitimacy of emails for phishing detection. These details can also provide insight into the hosting environment, potential service providers, and the configuration patterns of a target organization, assisting in investigative efforts.",
resources: [
{ title: 'Intro to DMARC, DKIM, and SPF (via Cloudflare)', link: 'https://www.cloudflare.com/learning/email-security/dmarc-dkim-spf/' },
{ title: 'EasyDMARC Domain Scanner', link: 'https://easydmarc.com/tools/domain-scanner' },
{ title: 'MX Toolbox', link: 'https://mxtoolbox.com/' },
{ title: 'RFC-7208 - SPF', link: 'https://datatracker.ietf.org/doc/html/rfc7208' },
{ title: 'RFC-6376 - DKIM', link: 'https://datatracker.ietf.org/doc/html/rfc6376' },
{ title: 'RFC-7489 - DMARC', link: 'https://datatracker.ietf.org/doc/html/rfc7489' },
{ title: 'BIMI Group', link: 'https://bimigroup.org/' },
],
screenshot: 'https://i.ibb.co/yqhwx5G/Screenshot-from-2023-07-29-18-22-20.png',
},
{
id: 'firewall',
title: 'Firewall Detection',
description: 'A WAF or web application firewall helps protect web applications by filtering and monitoring HTTP traffic between a web application and the Internet. It typically protects web applications from attacks such as cross-site forgery, cross-site-scripting (XSS), file inclusion, and SQL injection, among others.',
use: 'It\'s useful to understand if a site is using a WAF, and which firewall software / service it is using, as this provides an insight into the sites protection against several attack vectors, but also may reveal vulnerabilities in the firewall itself.',
resources: [
{ title: 'What is a WAF (via Cloudflare Learning)', link: 'https://www.cloudflare.com/learning/ddos/glossary/web-application-firewall-waf/' },
{ title: 'OWASP - Web Application Firewalls', link: 'https://owasp.org/www-community/Web_Application_Firewall' },
{ title: 'Web Application Firewall Best Practices', link: 'https://owasp.org/www-pdf-archive/Best_Practices_Guide_WAF_v104.en.pdf' },
{ title: 'WAF - Wiki', link: 'https://en.wikipedia.org/wiki/Web_application_firewall' },
],
screenshot: 'https://i.ibb.co/MfcxQt2/Screenshot-from-2023-08-12-15-40-52.png',
},
{
id: 'http-security',
title: 'HTTP Security Features',
description: 'Correctly configured security HTTP headers adds a layer of protection against common attacks to your site. The main headers to be aware of are: '
+ 'HTTP Strict Transport Security (HSTS): Enforces the use of HTTPS, mitigating man-in-the-middle attacks and protocol downgrade attempts. '
+ 'Content Security Policy (CSP): Constrains web page resources to prevent cross-site scripting and data injection attacks. '
+ 'X-Content-Type-Options: Prevents browsers from MIME-sniffing a response away from the declared content type, curbing MIME-type confusion attacks. '
+ 'X-Frame-Options: Protects users from clickjacking attacks by controlling whether a browser should render the page in a <frame>, <iframe>, <embed>, or <object>. ',
use: 'Reviewing security headers is important, as it offers insights into a site\'s defensive posture and potential vulnerabilities, enabling proactive mitigation and ensuring compliance with security best practices.',
resources: [
{ title: 'OWASP Secure Headers Project', link: 'https://owasp.org/www-project-secure-headers/'},
{ title: 'HTTP Header Cheatsheet', link: 'https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html' },
{ title: 'content-security-policy.com', link: 'https://content-security-policy.com/' },
{ title: 'resourcepolicy.fyi', link: 'https://resourcepolicy.fyi/' },
{ title: 'HTTP Security Headers', link: 'https://securityheaders.com/' },
{ title: 'Mozilla Observatory', link: 'https://observatory.mozilla.org/' },
{ title: 'CSP Docs', link: 'https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP' },
{ title: 'HSTS Docs', link: 'https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security' },
{ title: 'X-Content-Type-Options Docs', link: 'https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options' },
{ title: 'X-Frame-Options Docs', link: 'https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options' },
{ title: 'X-XSS-Protection Docs', link: 'https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-XSS-Protection' },
],
screenshot: 'https://i.ibb.co/LP05HMV/Screenshot-from-2023-08-12-15-40-28.png',
},
{
id: 'archives',
title: 'Archive History',
description: 'Fetches full history of archives from the Wayback machine',
use: 'This is useful for understanding the history of a site, and how it has changed over time. It can also be useful for finding old versions of a site, or for finding content that has been removed.',
resources: [
{ title: 'Wayback Machine', link: 'https://archive.org/web/'},
],
screenshot: 'https://i.ibb.co/nB9szT1/Screenshot-from-2023-08-14-22-31-16.png',
},
{
id: 'rank',
title: 'Global Ranking',
description: 'This check shows the global rank of the requested site. This is only accurate for websites which are in the top 100 million list. We\'re using data from the Tranco project (see below), which collates the top sites on the web from Umbrella, Majestic, Quantcast, the Chrome User Experience Report and Cloudflare Radar.',
use: 'Knowing a websites overall global rank can be useful for understanding the scale of the site, and for comparing it to other sites. It can also be useful for understanding the relative popularity of a site, and for identifying potential trends.',
resources: [
{ title: 'Tranco List', link: 'https://tranco-list.eu/' },
{ title: 'Tranco Research Paper', link: 'https://tranco-list.eu/assets/tranco-ndss19.pdf'},
],
screenshot: 'https://i.ibb.co/nkbczgb/Screenshot-from-2023-08-14-22-02-40.png',
},
{
id: 'block-lists',
title: 'Block Detection',
description: 'Checks access to the URL using 10+ of the most popular privacy, malware and parental control blocking DNS servers.',
use: '',
resources: [
{ title: 'ThreatJammer Lists', link: 'https://threatjammer.com/osint-lists'},
],
screenshot: 'https://i.ibb.co/M5JSXbW/Screenshot-from-2023-08-26-12-12-43.png',
},
{
id: 'threats',
title: 'Malware & Phishing Detection',
description: 'Checks if a site appears in several common malware and phishing lists, to determine it\'s threat level.',
use: 'Knowing if a site is listed as a threat by any of these services can be useful for understanding the reputation of a site, and for identifying potential trends.',
resources: [
{ title: 'URLHaus', link: 'https://urlhaus-api.abuse.ch/'},
{ title: 'PhishTank', link: 'https://www.phishtank.com/'},
],
screenshot: 'https://i.ibb.co/hYgy621/Screenshot-from-2023-08-26-12-07-47.png',
},
{
id: 'tls-cipher-suites',
title: 'TLS Cipher Suites',
description: 'These are combinations of cryptographic algorithms used by the server to establish a secure connection. It includes the key exchange algorithm, bulk encryption algorithm, MAC algorithm, and PRF (pseudorandom function).',
use: 'This is important info to test for from a security perspective. Because a cipher suite is only as secure as the algorithms that it contains. If the version of encryption or authentication algorithm in a cipher suite have known vulnerabilities the cipher suite and TLS connection may then vulnerable to a downgrade or other attack',
resources: [
{ title: 'sslscan2 CLI', link: 'https://github.com/rbsec/sslscan' },
{ title: 'ssl-enum-ciphers (NPMAP script)', link: 'https://nmap.org/nsedoc/scripts/ssl-enum-ciphers.html' }
],
screenshot: 'https://i.ibb.co/6ydtH5R/Screenshot-from-2023-08-26-12-09-58.png',
},
{
id: 'tls-security-config',
title: 'TLS Security Config',
description: 'This uses guidelines from Mozilla\'s TLS Observatory to check the security of the TLS configuration. It checks for bad configurations, which may leave the site vulnerable to attack, as well as giving advice on how to fix. It will also give suggestions around outdated and modern TLS configs',
use: 'Understanding issues with a site\'s TLS configuration will help you address potential vulnerabilities, and ensure the site is using the latest and most secure TLS configuration.',
resources: [],
screenshot: 'https://i.ibb.co/FmksZJt/Screenshot-from-2023-08-26-12-12-09.png',
},
{
id: 'tls-client-support',
title: 'TLS Handshake Simulation',
description: 'This simulates how different clients (browsers, operating systems) would perform a TLS handshake with the server. It helps identify compatibility issues and insecure configurations.',
use: '',
resources: [
{ title: 'TLS Handshakes (via Cloudflare Learning)', link: 'https://www.cloudflare.com/learning/ssl/what-happens-in-a-tls-handshake/' },
{ title: 'SSL Test (via SSL Labs)', link: 'https://www.ssllabs.com/ssltest/' },
],
screenshot: 'https://i.ibb.co/F7qRZkh/Screenshot-from-2023-08-26-12-11-28.png',
},
{
id: 'screenshot',
title: 'Screenshot',
description: 'This check takes a screenshot of webpage that the requested URL / IP resolves to, and displays it.',
use: 'This may be useful to see what a given website looks like, free of the constraints of your browser, IP, or location.',
resources: [],
screenshot: 'https://i.ibb.co/2F0x8kP/Screenshot-from-2023-07-29-18-34-48.png',
},
];
export const featureIntro = [
'When conducting an OSINT investigation on a given website or host, there are several key areas to look at. Each of these are documented below, along with links to the tools and techniques you can use to gather the relevant information.',
'Web-Check can automate the process of gathering this data, but it will be up to you to interpret the results and draw conclusions.',
];
export const about = [
`Web-Check is a powerful all-in-one tool for discovering information about a website/host.
The core philosophy is simple: feed Web-Check a URL and let it gather, collate, and present a broad array of open data for you to delve into.`,
`The report shines a spotlight onto potential attack vectors, existing security measures,
and the web of connections within a site's architecture.
The results can also help optimizing server responses, configuring redirects,
managing cookies, or fine-tuning DNS records for your site.`,
`So, whether you're a developer, system administrator, security researcher, penetration
tester or are just interested in discovering the underlying technologies of a given site
- I'm sure you'll find this a useful addition to your toolbox.`,
];
export const license = `The MIT License (MIT)
Copyright (c) Alicia Sykes <alicia@omg.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sub-license, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included install
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANT ABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
`;
export const supportUs = [
"Web-Check is free to use without restriction.",
"All the code is open source, so you're also free to deploy your own instance, as well as fork, modify and distribute the code in both private and commercial settings.",
"Running web-check does cost me a small amount of money each month, so if you're finding the app useful, consider <a href='https://github.com/sponsors/Lissy93'>sponsoring me on GitHub</a> if you're able to. Even just $1 or $2/month would be a huge help in supporting the ongoing project running costs.",
"Otherwise, there are other ways you can help out, like submitting or reviewing a pull request to the <a href='https://github.com/Lissy93/web-check'>GitHub repo</a>, upvoting us on <a href='https://www.producthunt.com/posts/web-check'>Product Hunt</a>, or by sharing with your network.",
"But don't feel obliged to do anything, as this app (and all my other projects) will always remain 100% free and open source, and I will do my best to ensure the managed instances remain up and available for as long as possible :)",
];
export const fairUse = [
'Please use this tool responsibly. Do not use it for hosts you do not have permission to scan. Do not use it as part of a scheme to attack or disrupt services.',
'Requests may be rate-limited to prevent abuse. If you need to make more bandwidth, please deploy your own instance.',
'There is no guarantee of uptime or availability. If you need to make sure the service is available, please deploy your own instance.',
'Please use fairly, as excessive use will quickly deplete the lambda function credits, making the service unavailable for others (and/or empty my bank account!).',
];
export default docs;

View File

@@ -0,0 +1,14 @@
const keys = {
shodan: import.meta.env.REACT_APP_SHODAN_API_KEY || "default_value_if_not_set",
whoApi: import.meta.env.REACT_APP_WHO_API_KEY || "default_value_if_not_set",
};
// const keys = process && process.env ? {
// shodan: process.env.REACT_APP_SHODAN_API_KEY,
// whoApi: process.env.REACT_APP_WHO_API_KEY,
// } : {
// shodan: import.meta.env.REACT_APP_SHODAN_API_KEY || "default_value_if_not_set",
// whoApi: import.meta.env.REACT_APP_WHO_API_KEY || "default_value_if_not_set",
// };
export default keys;

View File

@@ -0,0 +1,188 @@
import type { RowProps } from 'web-check-live/components/Form/Row';
export interface ServerLocation {
city: string,
region: string,
country: string,
postCode: string,
regionCode: string,
countryCode: string,
coords: {
latitude: number,
longitude: number,
},
isp: string,
timezone: string,
languages: string,
currency: string,
currencyCode: string,
countryDomain: string,
countryAreaSize: number,
countryPopulation: number,
};
export interface Whois {
created: string,
expires: string,
updated: string,
nameservers: string[],
}
export const getLocation = (response: any): ServerLocation => {
return {
city: response.city,
region: response.region,
country: response.country_name,
postCode: response.postal,
regionCode: response.region_code,
countryCode: response.country_code,
coords: {
latitude: response.latitude,
longitude: response.longitude,
},
isp: response.org,
timezone: response.timezone,
languages: response.languages,
currencyCode: response.currency,
currency: response.currency_name,
countryDomain: response.country_tld,
countryAreaSize: response.country_area,
countryPopulation: response.country_population,
};
};
export interface ServerInfo {
org: string,
asn: string,
isp: string,
os?: string,
ip?: string,
ports?: string,
loc?: string,
type?: string,
};
export const getServerInfo = (response: any): ServerInfo => {
return {
org: response.org,
asn: response.asn,
isp: response.isp,
os: response.os,
ip: response.ip_str,
ports: response?.ports?.toString(),
loc: response.city ? `${response.city}, ${response.country_name}` : '',
type: response.tags ? response.tags.toString() : '',
};
};
export interface HostNames {
domains: string[],
hostnames: string[],
};
export const getHostNames = (response: any): HostNames | null => {
const { hostnames, domains } = response;
if ((!domains || domains.length < 1) && (!hostnames || hostnames.length < 1)) {
return null;
}
const results: HostNames = {
domains: domains || [],
hostnames: hostnames || [],
};
return results;
};
export interface ShodanResults {
hostnames: HostNames | null,
serverInfo: ServerInfo,
}
export const parseShodanResults = (response: any): ShodanResults => {
return {
hostnames: getHostNames(response),
serverInfo: getServerInfo(response),
};
}
export interface Technology {
Categories?: string[];
Parent?: string;
Name: string;
Description: string;
Link: string;
Tag: string;
FirstDetected: number;
LastDetected: number;
IsPremium: string;
}
export interface TechnologyGroup {
tag: string;
technologies: Technology[];
}
export const makeTechnologies = (response: any): TechnologyGroup[] => {
let flatArray = response.Results[0].Result.Paths
.reduce((accumulator: any, obj: any) => accumulator.concat(obj.Technologies), []);
let technologies = flatArray.reduce((groups: any, item: any) => {
let tag = item.Tag;
if (!groups[tag]) groups[tag] = [];
groups[tag].push(item);
return groups;
}, {});
return technologies;
};
export type Cookie = {
name: string;
value: string;
attributes: Record<string, string>;
};
export const parseRobotsTxt = (content: string): { robots: RowProps[] } => {
const lines = content.split('\n');
const rules: RowProps[] = [];
lines.forEach(line => {
line = line.trim(); // This removes trailing and leading whitespaces
let match = line.match(/^(Allow|Disallow):\s*(\S*)$/i);
if (match) {
const rule: RowProps = {
lbl: match[1],
val: match[2],
};
rules.push(rule);
} else {
match = line.match(/^(User-agent):\s*(\S*)$/i);
if (match) {
const rule: RowProps = {
lbl: match[1],
val: match[2],
};
rules.push(rule);
}
}
});
return { robots: rules };
}
export const applyWhoIsResults = (response: any) => {
if (response.status !== '0') {
return {
error: response.status_desc,
}
}
const whoIsResults: Whois = {
created: response.date_created,
expires: response.date_expires,
updated: response.date_updated,
nameservers: response.nameservers,
};
return whoIsResults;
}

View File

@@ -0,0 +1,333 @@
import styled from '@emotion/styled';
import colors from 'web-check-live/styles/colors';
import Heading from 'web-check-live/components/Form/Heading';
import Footer from 'web-check-live/components/misc/Footer';
import Nav from 'web-check-live/components/Form/Nav';
import Button from 'web-check-live/components/Form/Button';
import AdditionalResources from 'web-check-live/components/misc/AdditionalResources';
import { StyledCard } from 'web-check-live/components/Form/Card';
import docs, { about, featureIntro, license, fairUse, supportUs } from 'web-check-live/utils/docs';
const AboutContainer = styled.div`
width: 95vw;
max-width: 1000px;
margin: 2rem auto;
padding-bottom: 1rem;
header {
margin 1rem 0;
width: auto;
}
section {
width: auto;
.inner-heading { display: none; }
}
`;
const HeaderLinkContainer = styled.nav`
display: flex;
flex-wrap: wrap;
gap: 1rem;
a {
text-decoration: none;
}
`;
const Section = styled(StyledCard)`
margin-bottom: 2rem;
overflow: clip;
max-height: 100%;
section {
clear: both;
}
h3 {
font-size: 1.5rem;
}
hr {
border: none;
border-top: 1px dashed ${colors.primary};
margin: 1.5rem auto;
}
ul {
padding: 0 0 0 1rem;
list-style: circle;
}
a {
color: ${colors.primary};
&:visited { opacity: 0.8; }
}
pre {
background: ${colors.background};
border-radius: 4px;
padding: 0.5rem;
width: fit-content;
}
small { opacity: 0.7; }
.contents {
ul {
list-style: none;
li {
a {
// color: ${colors.textColor};
&:visited { opacity: 0.8; }
}
b {
opacity: 0.75;
display: inline-block;
width: 1.5rem;
}
}
}
}
.example-screenshot {
float: right;
display: inline-flex;
flex-direction: column;
clear: both;
max-width: 300px;
img {
float: right;
break-inside: avoid;
max-width: 300px;
// max-height: 30rem;
border-radius: 6px;
clear: both;
}
figcaption {
font-size: 0.8rem;
text-align: center;
opacity: 0.7;
}
}
`;
const SponsorshipContainer = styled.div`
display: flex;
justify-content: space-between;
gap: 1rem;
flex-wrap: wrap;
align-items: center;
line-height: 1.5rem;
img {
border-radius: 4px;
}
`;
const makeAnchor = (title: string): string => {
return title.toLowerCase().replace(/[^\w\s]|_/g, "").replace(/\s+/g, "-");
};
const About = (): JSX.Element => {
return (
<div>
<AboutContainer>
<Nav>
<HeaderLinkContainer>
<a target="_blank" rel="noreferrer" href="https://github.com/lissy93/web-check"><Button>View on GitHub</Button></a>
</HeaderLinkContainer>
</Nav>
<Heading as="h2" size="medium" color={colors.primary}>Intro</Heading>
<Section>
{about.map((para, index: number) => (
<p key={index}>{para}</p>
))}
<hr />
<SponsorshipContainer>
<p>
Web-Check is kindly sponsored
by <a target="_blank" rel="noreferrer" href="https://terminaltrove.com/?utm_campaign=github&utm_medium=referral&utm_content=web-check&utm_source=wcgh">
Terminal Trove
</a>
<br />
The $HOME of all things in the terminal.
<br />
<small>
<a target="_blank" rel="noreferrer" href="https://terminaltrove.com/newsletter?utm_campaign=github&utm_medium=referral&utm_content=web-check&utm_source=wcgh">
Find your next CLI / TUI tool, and get updates to your inbox
</a>
</small>
</p>
<a target="_blank" rel="noreferrer" href="https://terminaltrove.com/?utm_campaign=github&utm_medium=referral&utm_content=web-check&utm_source=wcgh">
<img width="300" alt="Terminal Trove" src="https://i.ibb.co/T1KzVmR/terminal-trove-green.png" />
</a>
</SponsorshipContainer>
<hr />
<p>
Web-Check is developed and maintained by <a target="_blank" rel="noreferrer" href="https://aliciasykes.com">Alicia Sykes</a>.
It's licensed under the <a target="_blank" rel="noreferrer" href="https://github.com/Lissy93/web-check/blob/master/LICENSE">MIT license</a>,
and is completely free to use, modify and distribute in both personal and commercial settings.<br />
Source code and self-hosting docs are available on <a target="_blank" rel="noreferrer" href="https://github.com/lissy93/web-check">GitHub</a>.
If you've found this service useful, consider <a target="_blank" rel="noreferrer" href="https://github.com/sponsors/Lissy93">sponsoring me</a> from $1/month,
to help with the ongoing hosting and development costs.
</p>
</Section>
<Heading as="h2" size="medium" color={colors.primary}>Features</Heading>
<Section>
{featureIntro.map((fi: string, i: number) => (<p key={i}>{fi}</p>))}
<div className="contents">
<Heading as="h3" size="small" id="#feature-contents" color={colors.primary}>Contents</Heading>
<ul>
{docs.map((section, index: number) => (
<li>
<b>{index + 1}</b>
<a href={`#${makeAnchor(section.title)}`}>{section.title}</a></li>
))}
</ul>
<hr />
</div>
{docs.map((section, sectionIndex: number) => (
<section key={section.title}>
{ sectionIndex > 0 && <hr /> }
<Heading as="h3" size="small" id={makeAnchor(section.title)} color={colors.primary}>{section.title}</Heading>
{section.screenshot &&
<figure className="example-screenshot">
<img className="screenshot" src={section.screenshot} alt={`Example Screenshot ${section.title}`} />
<figcaption>Fig.{sectionIndex + 1} - Example of {section.title}</figcaption>
</figure>
}
{section.description && <>
<Heading as="h4" size="small">Description</Heading>
<p>{section.description}</p>
</>}
{ section.use && <>
<Heading as="h4" size="small">Use Cases</Heading>
<p>{section.use}</p>
</>}
{section.resources && section.resources.length > 0 && <>
<Heading as="h4" size="small">Useful Links</Heading>
<ul>
{section.resources.map((link: string | { title: string, link: string }, linkIndx: number) => (
typeof link === 'string' ? (
<li id={`link-${linkIndx}`}><a target="_blank" rel="noreferrer" href={link}>{link}</a></li>
) : (
<li id={`link-${linkIndx}`}><a target="_blank" rel="noreferrer" href={link.link}>{link.title}</a></li>
)
))}
</ul>
</>}
</section>
))}
</Section>
<Heading as="h2" size="medium" color={colors.primary}>Deploy your own Instance</Heading>
<Section>
<p>Web-Check is designed to be easily self-hosted.</p>
<Heading as="h3" size="small" color={colors.primary}>Option #1 - Netlify</Heading>
<p>Click the button below to deploy to Netlify</p>
<a target="_blank" rel="noreferrer" href="https://app.netlify.com/start/deploy?repository=https://github.com/lissy93/web-check">
<img src="https://www.netlify.com/img/deploy/button.svg" alt="Deploy to Netlify" />
</a>
<Heading as="h3" size="small" color={colors.primary}>Option #2 - Vercel</Heading>
<p>Click the button below to deploy to Vercel</p>
<a target="_blank" rel="noreferrer" href="https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Flissy93%2Fweb-check&project-name=web-check&repository-name=web-check-fork&demo-title=Web-Check%20Demo&demo-description=Check%20out%20web-check.xyz%20to%20see%20a%20live%20demo%20of%20this%20application%20running.&demo-url=https%3A%2F%2Fweb-check.xyz&demo-image=https%3A%2F%2Fraw.githubusercontent.com%2FLissy93%2Fweb-check%2Fmaster%2F.github%2Fscreenshots%2Fweb-check-screenshot10.png">
<img src="https://vercel.com/button" alt="Deploy with Vercel" />
</a>
<Heading as="h3" size="small" color={colors.primary}>Option #3 - Docker</Heading>
<p>
A Docker container is published to <a target="_blank" rel="noreferrer" href="https://hub.docker.com/r/lissy93/web-check">DockerHub</a>
<br />
Run this command, then open <code>localhost:3000</code>
<pre>docker run -p 3000:3000 lissy93/web-check</pre>
</p>
<Heading as="h3" size="small" color={colors.primary}>Option #4 - Manual</Heading>
<pre>
git clone https://github.com/Lissy93/web-check.git<br />
cd web-check # Move into the project directory<br />
yarn install # Install dependencies<br />
yarn build # Build the app for production<br />
yarn serve # Start the app (API and GUI)<br />
</pre>
<Heading as="h3" size="small" color={colors.primary}>Further Docs</Heading>
<p>
More detailed installation and setup instructions can be found in the
GitHub repository - <a target="_blank" rel="noreferrer" href="https://github.com/lissy93/web-check#readme">github.com/lissy93/web-check</a>
</p>
<Heading as="h3" size="small" color={colors.primary}>Configuring</Heading>
<p>
There are some optional environmental variables you can specify to give you access to some additional Web-Checks.
See the README for full list of options.
</p>
<ul>
<li>
<code>GOOGLE_CLOUD_API_KEY</code>
: <a target="_blank" rel="noreferrer" href="https://cloud.google.com/api-gateway/docs/authenticate-api-keys">A Google API key</a>
<i> Used to return quality metrics for a site</i>
</li>
<li>
<code>REACT_APP_SHODAN_API_KEY</code>
: <a target="_blank" rel="noreferrer" href="https://account.shodan.io/">A Shodan API key</a>
<i> To show associated hosts for a domain</i>
</li>
<li>
<code>REACT_APP_WHO_API_KEY</code>
: <a target="_blank" rel="noreferrer" href="https://whoapi.com/">A WhoAPI key</a>
<i> Allows for more comprehensive WhoIs records</i>
</li>
</ul>
{/*
**Configuration Settings**:
- `CHROME_PATH` (e.g. `/usr/bin/chromium`) - The path the the Chromium executable
- `PORT` (e.g. `3000`) - Port to serve the API, when running server.js
- `DISABLE_GUI` (e.g. `false`) - Disable the GUI, and only serve the API
- `API_TIMEOUT_LIMIT` (e.g. `10000`) - The timeout limit for API requests, in milliseconds
- `REACT_APP_API_ENDPOINT` (e.g. `/api`) - The endpoint for the API (can be local or remote)</p> */}
</Section>
<Heading as="h2" size="medium" color={colors.primary}>API Documentation</Heading>
<Section>
{/* eslint-disable-next-line*/}
<p>// Coming soon...</p>
</Section>
<Heading as="h2" size="medium" color={colors.primary}>Additional Resources</Heading>
<AdditionalResources />
<Heading as="h2" size="medium" color={colors.primary}>Support Us</Heading>
<Section>
{supportUs.map((para, index: number) => (<p dangerouslySetInnerHTML={{__html: para}} />))}
</Section>
<Heading as="h2" size="medium" color={colors.primary}>Terms & Info</Heading>
<Section>
<Heading as="h3" size="small" color={colors.primary}>License</Heading>
<b>
<a target="_blank" rel="noreferrer" href="https://github.com/lissy93/web-check">Web-Check</a> is distributed under the MIT license,
© <a target="_blank" rel="noreferrer" href="https://aliciasykes.com">Alicia Sykes</a> { new Date().getFullYear()}
</b>
<br />
<small>For more info, see <a target="_blank" rel="noreferrer" href="https://tldrlegal.com/license/mit-license">TLDR Legal MIT</a></small>
<pre>{license}</pre>
<hr />
<Heading as="h3" size="small" color={colors.primary}>Fair Use</Heading>
<ul>
{fairUse.map((para, index: number) => (<li>{para}</li>))}
</ul>
<hr />
<Heading as="h3" size="small" color={colors.primary}>Privacy</Heading>
<p>
Analytics are used on the demo instance (via a self-hosted Plausible instance), this only records the URL you visited but no personal data.
There's also some basic error logging (via a self-hosted GlitchTip instance), this is only used to help me fix bugs.
<br />
<br />
Neither your IP address, browser/OS/hardware info, nor any other data will ever be collected or logged.
(You may verify this yourself, either by inspecting the source code or the using developer tools)
</p>
</Section>
</AboutContainer>
<Footer />
</div>
);
}
export default About;

View File

@@ -0,0 +1,300 @@
import styled from '@emotion/styled';
import { type ChangeEvent, type FormEvent, useState, useEffect } from 'react';
import { useNavigate, useLocation, type NavigateOptions } from 'react-router-dom';
import Heading from 'web-check-live/components/Form/Heading';
import Input from 'web-check-live/components/Form/Input'
import Button from 'web-check-live/components/Form/Button';
import { StyledCard } from 'web-check-live/components/Form/Card';
import Footer from 'web-check-live/components/misc/Footer';
import FancyBackground from 'web-check-live/components/misc/FancyBackground';
import docs from 'web-check-live/utils/docs';
import colors from 'web-check-live/styles/colors';
import { determineAddressType } from 'web-check-live/utils/address-type-checker';
const HomeContainer = styled.section`
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100%;
font-family: 'PTMono';
padding: 1.5rem 1rem 4rem 1rem;
footer {
z-index: 1;
}
`;
const UserInputMain = styled.form`
background: ${colors.backgroundLighter};
box-shadow: 4px 4px 0px ${colors.bgShadowColor};
border-radius: 8px;
padding: 1rem;
z-index: 5;
margin: 1rem;
width: calc(100% - 2rem);
max-width: 60rem;
z-index: 2;
`;
const SponsorCard = styled.div`
background: ${colors.backgroundLighter};
box-shadow: 4px 4px 0px ${colors.bgShadowColor};
border-radius: 8px;
padding: 1rem;
z-index: 5;
margin: 1rem;
width: calc(100% - 2rem);
max-width: 60rem;
z-index: 2;
.inner {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
gap: 1rem;
p {
margin: 0.25rem 0;
}
}
a {
color: ${colors.textColor};
}
img {
border-radius: 0.25rem;
box-shadow: 2px 2px 0px ${colors.fgShadowColor};
transition: box-shadow 0.2s;
margin: 0 auto;
display: block;
width: 200px;
&:hover {
box-shadow: 4px 4px 0px ${colors.fgShadowColor};
}
&:active {
box-shadow: -2px -2px 0px ${colors.fgShadowColor};
}
}
.cta {
font-size: 0.78rem;
a { color: ${colors.primary}; }
}
`;
// const FindIpButton = styled.a`
// margin: 0.5rem;
// cursor: pointer;
// display: block;
// text-align: center;
// color: ${colors.primary};
// text-decoration: underline;
// `;
const ErrorMessage = styled.p`
color: ${colors.danger};
margin: 0.5rem;
`;
const SiteFeaturesWrapper = styled(StyledCard)`
margin: 1rem;
width: calc(100% - 2rem);
max-width: 60rem;
z-index: 2;
.links {
display: flex;
justify-content: center;
gap: 0.5rem;
a {
width: 100%;
button {
width: calc(100% - 2rem);
}
}
@media(max-width: 600px) {
flex-wrap: wrap;
}
}
ul {
-webkit-column-width: 150px;
-moz-column-width: 150px;
column-width: 150px;
list-style: none;
padding: 0 1rem;
font-size: 0.9rem;
color: ${colors.textColor};
li {
margin: 0.1rem 0;
text-indent: -1.2rem;
break-inside: avoid-column;
}
li:before {
content: '✓';
color: ${colors.primary};
margin-right: 0.5rem;
}
}
a {
color: ${colors.primary};
}
`;
const Home = (): JSX.Element => {
const defaultPlaceholder = 'e.g. https://duck.com/';
const [userInput, setUserInput] = useState('');
const [errorMsg, setErrMsg] = useState('');
const [placeholder] = useState(defaultPlaceholder);
const [inputDisabled] = useState(false);
const navigate = useNavigate();
const location = useLocation();
/* Redirect strait to results, if somehow we land on /check?url=[] */
useEffect(() => {
const query = new URLSearchParams(location.search);
const urlFromQuery = query.get('url');
if (urlFromQuery) {
navigate(`/check/${encodeURIComponent(urlFromQuery)}`, { replace: true });
}
}, [navigate, location.search]);
/* Check is valid address, either show err or redirect to results page */
const submit = () => {
let address = userInput.endsWith("/") ? userInput.slice(0, -1) : userInput;
const addressType = determineAddressType(address);
if (addressType === 'empt') {
setErrMsg('Field must not be empty');
} else if (addressType === 'err') {
setErrMsg('Must be a valid URL, IPv4 or IPv6 Address');
} else {
// if the addressType is 'url' and address doesn't start with 'http://' or 'https://', prepend 'https://'
if (addressType === 'url' && !/^https?:\/\//i.test(address)) {
address = 'https://' + address;
}
const resultRouteParams: NavigateOptions = { state: { address, addressType } };
navigate(`/check/${encodeURIComponent(address)}`, resultRouteParams);
}
};
/* Update user input state, and hide error message if field is valid */
const inputChange = (event: ChangeEvent<HTMLInputElement>) => {
setUserInput(event.target.value);
const isError = ['err', 'empt'].includes(determineAddressType(event.target.value));
if (!isError) setErrMsg('');
};
const handleKeyPress = (event: React.KeyboardEvent<HTMLInputElement>) => {
console.log(event.key);
if (event.key === 'Enter') {
event.preventDefault();
submit();
}
};
const formSubmitEvent = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
submit();
}
// const findIpAddress = () => {
// setUserInput('');
// setPlaceholder('Looking up your IP...');
// setInputDisabled(true);
// fetch('https://ipapi.co/json/')
// .then(function(response) {
// response.json().then(jsonData => {
// setUserInput(jsonData.ip);
// setPlaceholder(defaultPlaceholder);
// setInputDisabled(true);
// });
// })
// .catch(function(error) {
// console.log('Failed to get IP address :\'(', error)
// });
// };
return (
<HomeContainer>
<FancyBackground />
<UserInputMain onSubmit={formSubmitEvent}>
<Heading as="h1" size="xLarge" align="center" color={colors.primary}>
<img width="64" src="/web-check.png" alt="Web Check Icon" />
Web Check
</Heading>
<Input
id="user-input"
value={userInput}
label="Enter a URL"
size="large"
orientation="vertical"
name="url"
placeholder={placeholder}
disabled={inputDisabled}
handleChange={inputChange}
handleKeyDown={handleKeyPress}
/>
{/* <FindIpButton onClick={findIpAddress}>Or, find my IP</FindIpButton> */}
{ errorMsg && <ErrorMessage>{errorMsg}</ErrorMessage>}
<Button type="submit" styles="width: calc(100% - 1rem);" size="large" onClick={submit}>Analyze!</Button>
</UserInputMain>
<SponsorCard>
<Heading as="h2" size="small" color={colors.primary}>Sponsored by</Heading>
<div className="inner">
<p>
<a
target="_blank"
rel="noreferrer"
href="https://terminaltrove.com/?utm_campaign=github&utm_medium=referral&utm_content=web-check&utm_source=wcgh"
>
Terminal Trove
</a> - The $HOME of all things in the terminal.
<br />
<span className="cta">
Get updates on the latest CLI/TUI tools via
the <a
target="_blank"
rel="noreferrer"
className="cta"
href="https://terminaltrove.com/newsletter?utm_campaign=github&utm_medium=referral&utm_content=web-check&utm_source=wcgh"
>
Terminal Trove newsletter
</a>
</span>
</p>
<a
target="_blank"
rel="noreferrer"
href="https://terminaltrove.com/?utm_campaign=github&utm_medium=referral&utm_content=web-check&utm_source=wcgh">
<img width="120" alt="Terminal Trove" src="https://i.ibb.co/NKtYjJ1/terminal-trove-web-check.png" />
</a>
</div>
</SponsorCard>
<SiteFeaturesWrapper>
<div className="features">
<Heading as="h2" size="small" color={colors.primary}>Supported Checks</Heading>
<ul>
{docs.map((doc, index) => (<li key={index}>{doc.title}</li>))}
<li><a href="/check/about">+ more!</a></li>
</ul>
</div>
<div className="links">
<a target="_blank" rel="noreferrer" href="https://github.com/lissy93/web-check" title="Check out the source code and documentation on GitHub, and get support or contribute">
<Button>View on GitHub</Button>
</a>
<a target="_blank" rel="noreferrer" href="https://app.netlify.com/start/deploy?repository=https://github.com/lissy93/web-check" title="Deploy your own private or public instance of Web-Check to Netlify">
<Button>Deploy your own</Button>
</a>
<a href="about#api-documentation" title="View the API documentation, to use Web-Check programmatically">
<Button>API Docs</Button>
</a>
</div>
</SiteFeaturesWrapper>
<Footer isFixed={true} />
</HomeContainer>
);
}
export default Home;

View File

@@ -0,0 +1,68 @@
import styled from '@emotion/styled';
import colors from 'web-check-live/styles/colors';
import Heading from 'web-check-live/components/Form/Heading';
import Footer from 'web-check-live/components/misc/Footer';
import Nav from 'web-check-live/components/Form/Nav';
import Button from 'web-check-live/components/Form/Button';
import { StyledCard } from 'web-check-live/components/Form/Card';
const AboutContainer = styled.div`
width: 95vw;
max-width: 1000px;
margin: 2rem auto;
padding-bottom: 1rem;
header {
margin 1rem 0;
}
a {
color: ${colors.primary};
}
.im-drink { font-size: 6rem; }
header {
width: auto;
margin: 1rem;
}
`;
const HeaderLinkContainer = styled.nav`
display: flex;
flex-wrap: wrap;
gap: 1rem;
a {
text-decoration: none;
}
`;
const NotFoundInner = styled(StyledCard)`
display: flex;
flex-direction: column;
align-items: center;
margin: 1rem;
gap: 0.5rem;
h2 { font-size: 8rem; }
`;
const NotFound = (): JSX.Element => {
return (
<>
<AboutContainer>
<Nav />
<NotFoundInner>
<Heading as="h2" size="large" color={colors.primary}>404</Heading>
<span className="im-drink">🥴</span>
<Heading as="h3" size="large" color={colors.primary}>Not Found</Heading>
<HeaderLinkContainer>
<a href="/"><Button>Back to Homepage</Button></a>
</HeaderLinkContainer>
<a target="_blank" rel="noreferrer" href="https://github.com/lissy93/web-check">Report Issue</a>
</NotFoundInner>
</AboutContainer>
<Footer isFixed={true} />
</>
);
};
export default NotFound;

View File

@@ -0,0 +1,949 @@
import { useState, useEffect, useCallback, type ReactNode } from 'react';
import { useParams, useLocation } from 'react-router-dom';
import styled from '@emotion/styled';
import { ToastContainer } from 'react-toastify';
import Masonry from 'react-masonry-css'
import colors from 'web-check-live/styles/colors';
import Heading from 'web-check-live/components/Form/Heading';
import Modal from 'web-check-live/components/Form/Modal';
import Footer from 'web-check-live/components/misc/Footer';
import Nav from 'web-check-live/components/Form/Nav';
import type { RowProps } from 'web-check-live/components/Form/Row';
import Loader from 'web-check-live/components/misc/Loader';
import ErrorBoundary from 'web-check-live/components/misc/ErrorBoundary';
import SelfScanMsg from 'web-check-live/components/misc/SelfScanMsg';
import DocContent from 'web-check-live/components/misc/DocContent';
import ProgressBar, { type LoadingJob, type LoadingState, initialJobs } from 'web-check-live/components/misc/ProgressBar';
import ActionButtons from 'web-check-live/components/misc/ActionButtons';
import AdditionalResources from 'web-check-live/components/misc/AdditionalResources';
import ViewRaw from 'web-check-live/components/misc/ViewRaw';
import ServerLocationCard from 'web-check-live/components/Results/ServerLocation';
import ServerInfoCard from 'web-check-live/components/Results/ServerInfo';
import HostNamesCard from 'web-check-live/components/Results/HostNames';
import WhoIsCard from 'web-check-live/components/Results/WhoIs';
import LighthouseCard from 'web-check-live/components/Results/Lighthouse';
import ScreenshotCard from 'web-check-live/components/Results/Screenshot';
import SslCertCard from 'web-check-live/components/Results/SslCert';
import HeadersCard from 'web-check-live/components/Results/Headers';
import CookiesCard from 'web-check-live/components/Results/Cookies';
import RobotsTxtCard from 'web-check-live/components/Results/RobotsTxt';
import DnsRecordsCard from 'web-check-live/components/Results/DnsRecords';
import RedirectsCard from 'web-check-live/components/Results/Redirects';
import TxtRecordCard from 'web-check-live/components/Results/TxtRecords';
import ServerStatusCard from 'web-check-live/components/Results/ServerStatus';
import OpenPortsCard from 'web-check-live/components/Results/OpenPorts';
import TraceRouteCard from 'web-check-live/components/Results/TraceRoute';
import CarbonFootprintCard from 'web-check-live/components/Results/CarbonFootprint';
import SiteFeaturesCard from 'web-check-live/components/Results/SiteFeatures';
import DnsSecCard from 'web-check-live/components/Results/DnsSec';
import HstsCard from 'web-check-live/components/Results/Hsts';
import SitemapCard from 'web-check-live/components/Results/Sitemap';
import DomainLookup from 'web-check-live/components/Results/DomainLookup';
import DnsServerCard from 'web-check-live/components/Results/DnsServer';
import TechStackCard from 'web-check-live/components/Results/TechStack';
import SecurityTxtCard from 'web-check-live/components/Results/SecurityTxt';
import ContentLinksCard from 'web-check-live/components/Results/ContentLinks';
import SocialTagsCard from 'web-check-live/components/Results/SocialTags';
import MailConfigCard from 'web-check-live/components/Results/MailConfig';
import HttpSecurityCard from 'web-check-live/components/Results/HttpSecurity';
import FirewallCard from 'web-check-live/components/Results/Firewall';
import ArchivesCard from 'web-check-live/components/Results/Archives';
import RankCard from 'web-check-live/components/Results/Rank';
import BlockListsCard from 'web-check-live/components/Results/BlockLists';
import ThreatsCard from 'web-check-live/components/Results/Threats';
import TlsCipherSuitesCard from 'web-check-live/components/Results/TlsCipherSuites';
import TlsIssueAnalysisCard from 'web-check-live/components/Results/TlsIssueAnalysis';
import TlsClientSupportCard from 'web-check-live/components/Results/TlsClientSupport';
import keys from 'web-check-live/utils/get-keys';
import { determineAddressType, type AddressType } from 'web-check-live/utils/address-type-checker';
import useMotherHook from 'web-check-live/hooks/motherOfAllHooks';
import {
getLocation, type ServerLocation,
type Cookie,
applyWhoIsResults, type Whois,
parseShodanResults, type ShodanResults
} from 'web-check-live/utils/result-processor';
const ResultsOuter = styled.div`
display: flex;
flex-direction: column;
.masonry-grid {
display: flex;
width: auto;
}
.masonry-grid-col section { margin: 1rem 0.5rem; }
`;
const ResultsContent = styled.section`
width: 95vw;
display: grid;
grid-auto-flow: dense;
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
gap: 1rem;
margin: auto;
width: calc(100% - 2rem);
padding-bottom: 1rem;
`;
const FilterButtons = styled.div`
width: 95vw;
margin: auto;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: 1rem;
.one-half {
display: flex;
flex-wrap: wrap;
gap: 1rem;
align-items: center;
}
button, input, .toggle-filters {
background: ${colors.backgroundLighter};
color: ${colors.textColor};
border: none;
border-radius: 4px;
font-family: 'PTMono';
padding: 0.25rem 0.5rem;
border: 1px solid transparent;
transition: all 0.2s ease-in-out;
}
button, .toggle-filters {
cursor: pointer;
text-transform: capitalize;
box-shadow: 2px 2px 0px ${colors.bgShadowColor};
transition: all 0.2s ease-in-out;
&:hover {
box-shadow: 4px 4px 0px ${colors.bgShadowColor};
color: ${colors.primary};
}
&.selected {
border: 1px solid ${colors.primary};
color: ${colors.primary};
}
}
input:focus {
border: 1px solid ${colors.primary};
outline: none;
}
.clear {
color: ${colors.textColor};
text-decoration: underline;
cursor: pointer;
font-size: 0.8rem;
opacity: 0.8;
}
.toggle-filters {
font-size: 0.8rem;
}
.control-options {
display: flex;
flex-wrap: wrap;
gap: 1rem;
align-items: center;
a {
text-decoration: none;
}
}
`;
const Results = (props: { address?: string } ): JSX.Element => {
const startTime = new Date().getTime();
const address = props.address || useParams().address || '';
const [ addressType, setAddressType ] = useState<AddressType>('empt');
const [loadingJobs, setLoadingJobs] = useState<LoadingJob[]>(initialJobs);
const [modalOpen, setModalOpen] = useState(false);
const [modalContent, setModalContent] = useState<ReactNode>(<></>);
const [showFilters, setShowFilters] = useState(false);
const [searchTerm, setSearchTerm] = useState<string>('');
const [tags, setTags] = useState<string[]>([]);
const clearFilters = () => {
setTags([]);
setSearchTerm('');
};
const updateTags = (tag: string) => {
// Remove current tag if it exists, otherwise add it
// setTags(tags.includes(tag) ? tags.filter(t => t !== tag) : [...tags, tag]);
setTags(tags.includes(tag) ? tags.filter(t => t !== tag) : [tag]);
};
const updateLoadingJobs = useCallback((jobs: string | string[], newState: LoadingState, error?: string, retry?: () => void, data?: any) => {
(typeof jobs === 'string' ? [jobs] : jobs).forEach((job: string) => {
const now = new Date();
const timeTaken = now.getTime() - startTime;
setLoadingJobs((prevJobs) => {
const newJobs = prevJobs.map((loadingJob: LoadingJob) => {
if (job.includes(loadingJob.name)) {
return { ...loadingJob, error, state: newState, timeTaken, retry };
}
return loadingJob;
});
const timeString = `[${now.getHours().toString().padStart(2, '0')}:`
+`${now.getMinutes().toString().padStart(2, '0')}:`
+ `${now.getSeconds().toString().padStart(2, '0')}]`;
if (newState === 'success') {
console.log(
`%cFetch Success - ${job}%c\n\n${timeString}%c The ${job} job succeeded in ${timeTaken}ms`
+ `\n%cRun %cwindow.webCheck['${job}']%c to inspect the raw the results`,
`background:${colors.success};color:${colors.background};padding: 4px 8px;font-size:16px;`,
`font-weight: bold; color: ${colors.success};`,
`color: ${colors.success};`,
`color: #1d8242;`,`color: #1d8242;text-decoration:underline;`,`color: #1d8242;`,
);
if (!(window as any).webCheck) (window as any).webCheck = {};
if (data) (window as any).webCheck[job] = data;
}
if (newState === 'error') {
console.log(
`%cFetch Error - ${job}%c\n\n${timeString}%c The ${job} job failed `
+`after ${timeTaken}ms, with the following error:%c\n${error}`,
`background: ${colors.danger}; color:${colors.background}; padding: 4px 8px; font-size: 16px;`,
`font-weight: bold; color: ${colors.danger};`,
`color: ${colors.danger};`,
`color: ${colors.warning};`,
);
}
if (newState === 'timed-out') {
console.log(
`%cFetch Timeout - ${job}%c\n\n${timeString}%c The ${job} job timed out `
+`after ${timeTaken}ms, with the following error:%c\n${error}`,
`background: ${colors.info}; color:${colors.background}; padding: 4px 8px; font-size: 16px;`,
`font-weight: bold; color: ${colors.info};`,
`color: ${colors.info};`,
`color: ${colors.warning};`,
);
}
return newJobs;
});
});
}, [startTime]);
const parseJson = (response: Response): Promise<any> => {
return new Promise((resolve) => {
response.json()
.then(data => resolve(data))
.catch(error => resolve(
{ error: `Failed to get a valid response 😢\n`
+ 'This is likely due the target not exposing the required data, '
+ 'or limitations in imposed by the infrastructure this instance '
+ 'of Web Check is running on.\n\n'
+ `Error info:\n${error}`}
));
});
};
const urlTypeOnly = ['url'] as AddressType[]; // Many jobs only run with these address types
// const api = process.env.REACT_APP_API_ENDPOINT || '/api'; // Where is the API hosted?
const api = '/api'; // Where is the API hosted?
// Fetch and parse IP address for given URL
const [ipAddress, setIpAddress] = useMotherHook({
jobId: 'get-ip',
updateLoadingJobs,
addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly },
fetchRequest: () => fetch(`${api}/get-ip?url=${address}`)
.then(res => parseJson(res))
.then(res => res.ip),
});
useEffect(() => {
if (!addressType || addressType === 'empt') {
setAddressType(determineAddressType(address || ''));
}
if (addressType === 'ipV4' && address) {
setIpAddress(address);
}
}, [address, addressType, setIpAddress]);
// Get IP address location info
const [locationResults, updateLocationResults] = useMotherHook<ServerLocation>({
jobId: 'location',
updateLoadingJobs,
addressInfo: { address: ipAddress, addressType: 'ipV4', expectedAddressTypes: ['ipV4', 'ipV6'] },
fetchRequest: () => fetch(`https://ipapi.co/${ipAddress}/json/`)
.then(res => parseJson(res))
.then(res => getLocation(res)),
});
// Fetch and parse SSL certificate info
const [sslResults, updateSslResults] = useMotherHook({
jobId: 'ssl',
updateLoadingJobs,
addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly },
fetchRequest: () => fetch(`${api}/ssl?url=${address}`).then((res) => parseJson(res)),
});
// Run a manual whois lookup on the domain
const [domainLookupResults, updateDomainLookupResults] = useMotherHook({
jobId: 'domain',
updateLoadingJobs,
addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly },
fetchRequest: () => fetch(`${api}/whois?url=${address}`).then(res => parseJson(res)),
});
// Fetch and parse Lighthouse performance data
const [lighthouseResults, updateLighthouseResults] = useMotherHook({
jobId: 'quality',
updateLoadingJobs,
addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly },
fetchRequest: () => fetch(`${api}/quality?url=${address}`)
.then(res => parseJson(res))
.then(res => res?.lighthouseResult || { error: res.error || 'No Data' }),
});
// Get the technologies used to build site, using Wappalyzer
const [techStackResults, updateTechStackResults] = useMotherHook({
jobId: 'tech-stack',
updateLoadingJobs,
addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly },
fetchRequest: () => fetch(`${api}/tech-stack?url=${address}`).then(res => parseJson(res)),
});
// Get hostnames and associated domains from Shodan
const [shoadnResults, updateShodanResults] = useMotherHook<ShodanResults>({
jobId: ['hosts', 'server-info'],
updateLoadingJobs,
addressInfo: { address: ipAddress, addressType: 'ipV4', expectedAddressTypes: ['ipV4', 'ipV6'] },
fetchRequest: () => fetch(`https://api.shodan.io/shodan/host/${ipAddress}?key=${keys.shodan}`)
.then(res => parseJson(res))
.then(res => parseShodanResults(res)),
});
// Fetch and parse cookies info
const [cookieResults, updateCookieResults] = useMotherHook<{cookies: Cookie[]}>({
jobId: 'cookies',
updateLoadingJobs,
addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly },
fetchRequest: () => fetch(`${api}/cookies?url=${address}`)
.then(res => parseJson(res)),
});
// Fetch and parse headers
const [headersResults, updateHeadersResults] = useMotherHook({
jobId: 'headers',
updateLoadingJobs,
addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly },
fetchRequest: () => fetch(`${api}/headers?url=${address}`).then(res => parseJson(res)),
});
// Fetch and parse DNS records
const [dnsResults, updateDnsResults] = useMotherHook({
jobId: 'dns',
updateLoadingJobs,
addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly },
fetchRequest: () => fetch(`${api}/dns?url=${address}`).then(res => parseJson(res)),
});
// Get HTTP security
const [httpSecurityResults, updateHttpSecurityResults] = useMotherHook({
jobId: 'http-security',
updateLoadingJobs,
addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly },
fetchRequest: () => fetch(`${api}/http-security?url=${address}`).then(res => parseJson(res)),
});
// Get social media previews, from a sites social meta tags
const [socialTagResults, updateSocialTagResults] = useMotherHook({
jobId: 'social-tags',
updateLoadingJobs,
addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly },
fetchRequest: () => fetch(`${api}/social-tags?url=${address}`).then(res => parseJson(res)),
});
// Get trace route for a given hostname
const [traceRouteResults, updateTraceRouteResults] = useMotherHook({
jobId: 'trace-route',
updateLoadingJobs,
addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly },
fetchRequest: () => fetch(`${api}/trace-route?url=${address}`).then(res => parseJson(res)),
});
// Get a websites listed pages, from sitemap
const [securityTxtResults, updateSecurityTxtResults] = useMotherHook({
jobId: 'security-txt',
updateLoadingJobs,
addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly },
fetchRequest: () => fetch(`${api}/security-txt?url=${address}`).then(res => parseJson(res)),
});
// Get the DNS server(s) for a domain, and test DoH/DoT support
const [dnsServerResults, updateDnsServerResults] = useMotherHook({
jobId: 'dns-server',
updateLoadingJobs,
addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly },
fetchRequest: () => fetch(`${api}/dns-server?url=${address}`).then(res => parseJson(res)),
});
// Get the WAF and Firewall info for a site
const [firewallResults, updateFirewallResults] = useMotherHook({
jobId: 'firewall',
updateLoadingJobs,
addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly },
fetchRequest: () => fetch(`${api}/firewall?url=${address}`).then(res => parseJson(res)),
});
// Get DNSSEC info
const [dnsSecResults, updateDnsSecResults] = useMotherHook({
jobId: 'dnssec',
updateLoadingJobs,
addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly },
fetchRequest: () => fetch(`${api}/dnssec?url=${address}`).then(res => parseJson(res)),
});
// Check if a site is on the HSTS preload list
const [hstsResults, updateHstsResults] = useMotherHook({
jobId: 'hsts',
updateLoadingJobs,
addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly },
fetchRequest: () => fetch(`${api}/hsts?url=${address}`).then(res => parseJson(res)),
});
// Check if a host is present on the URLHaus malware list
const [threatResults, updateThreatResults] = useMotherHook({
jobId: 'threats',
updateLoadingJobs,
addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly },
fetchRequest: () => fetch(`${api}/threats?url=${address}`).then(res => parseJson(res)),
});
// Get mail config for server, based on DNS records
const [mailConfigResults, updateMailConfigResults] = useMotherHook({
jobId: 'mail-config',
updateLoadingJobs,
addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly },
fetchRequest: () => fetch(`${api}/mail-config?url=${address}`).then(res => parseJson(res)),
});
// Get list of archives from the Wayback Machine
const [archivesResults, updateArchivesResults] = useMotherHook({
jobId: 'archives',
updateLoadingJobs,
addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly },
fetchRequest: () => fetch(`${api}/archives?url=${address}`).then(res => parseJson(res)),
});
// Get website's global ranking, from Tranco
const [rankResults, updateRankResults] = useMotherHook({
jobId: 'rank',
updateLoadingJobs,
addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly },
fetchRequest: () => fetch(`${api}/rank?url=${address}`).then(res => parseJson(res)),
});
// Take a screenshot of the website
const [screenshotResult, updateScreenshotResult] = useMotherHook({
jobId: 'screenshot',
updateLoadingJobs,
addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly },
fetchRequest: () => fetch(`${api}/screenshot?url=${address}`).then(res => parseJson(res)),
});
// Get TLS security info, from Mozilla Observatory
const [tlsResults, updateTlsResults] = useMotherHook({
jobId: ['tls-cipher-suites', 'tls-security-config', 'tls-client-support'],
updateLoadingJobs,
addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly },
fetchRequest: () => fetch(`${api}/tls?url=${address}`).then(res => parseJson(res)),
});
// Fetches URL redirects
const [redirectResults, updateRedirectResults] = useMotherHook({
jobId: 'redirects',
updateLoadingJobs,
addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly },
fetchRequest: () => fetch(`${api}/redirects?url=${address}`).then(res => parseJson(res)),
});
// Get list of links included in the page content
const [linkedPagesResults, updateLinkedPagesResults] = useMotherHook({
jobId: 'linked-pages',
updateLoadingJobs,
addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly },
fetchRequest: () => fetch(`${api}/linked-pages?url=${address}`).then(res => parseJson(res)),
});
// Fetch and parse crawl rules from robots.txt
const [robotsTxtResults, updateRobotsTxtResults] = useMotherHook<{robots: RowProps[]}>({
jobId: 'robots-txt',
updateLoadingJobs,
addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly },
fetchRequest: () => fetch(`${api}/robots-txt?url=${address}`)
.then(res => parseJson(res)),
});
// Get current status and response time of server
const [serverStatusResults, updateServerStatusResults] = useMotherHook({
jobId: 'status',
updateLoadingJobs,
addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly },
fetchRequest: () => fetch(`${api}/status?url=${address}`).then(res => parseJson(res)),
});
// Check for open ports
const [portsResults, updatePortsResults] = useMotherHook({
jobId: 'ports',
updateLoadingJobs,
addressInfo: { address: ipAddress, addressType: 'ipV4', expectedAddressTypes: ['ipV4', 'ipV6'] },
fetchRequest: () => fetch(`${api}/ports?url=${ipAddress}`)
.then(res => parseJson(res)),
});
// Fetch and parse domain whois results
const [whoIsResults, updateWhoIsResults] = useMotherHook<Whois | { error: string }>({
jobId: 'whois',
updateLoadingJobs,
addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly },
fetchRequest: () => fetch(`https://api.whoapi.com/?domain=${address}&r=whois&apikey=${keys.whoApi}`)
.then(res => parseJson(res))
.then(res => applyWhoIsResults(res)),
});
// Fetches DNS TXT records
const [txtRecordResults, updateTxtRecordResults] = useMotherHook({
jobId: 'txt-records',
updateLoadingJobs,
addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly },
fetchRequest: () => fetch(`${api}/txt-records?url=${address}`).then(res => parseJson(res)),
});
// Check site against DNS blocklists
const [blockListsResults, updateBlockListsResults] = useMotherHook({
jobId: 'block-lists',
updateLoadingJobs,
addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly },
fetchRequest: () => fetch(`${api}/block-lists?url=${address}`).then(res => parseJson(res)),
});
// Get a websites listed pages, from sitemap
const [sitemapResults, updateSitemapResults] = useMotherHook({
jobId: 'sitemap',
updateLoadingJobs,
addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly },
fetchRequest: () => fetch(`${api}/sitemap?url=${address}`).then(res => parseJson(res)),
});
// Fetch carbon footprint data for a given site
const [carbonResults, updateCarbonResults] = useMotherHook({
jobId: 'carbon',
updateLoadingJobs,
addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly },
fetchRequest: () => fetch(`${api}/carbon?url=${address}`).then(res => parseJson(res)),
});
// Get site features from BuiltWith
const [siteFeaturesResults, updateSiteFeaturesResults] = useMotherHook({
jobId: 'features',
updateLoadingJobs,
addressInfo: { address, addressType, expectedAddressTypes: urlTypeOnly },
fetchRequest: () => fetch(`${api}/features?url=${address}`)
.then(res => parseJson(res))
.then(res => {
if (res.Errors && res.Errors.length > 0) {
return { error: `No data returned, because ${res.Errors[0].Message || 'API lookup failed'}` };
}
return res;
}),
});
/* Cancel remaining jobs after 10 second timeout */
useEffect(() => {
const checkJobs = () => {
loadingJobs.forEach(job => {
if (job.state === 'loading') {
updateLoadingJobs(job.name, 'timed-out');
}
});
};
const timeoutId = setTimeout(checkJobs, 10000);
return () => {
clearTimeout(timeoutId);
};
}, [loadingJobs, updateLoadingJobs]);
const makeSiteName = (address: string): string => {
try {
return new URL(address).hostname.replace('www.', '');
} catch (error) {
return address;
}
}
// A list of state sata, corresponding component and title for each card
const resultCardData = [
{
id: 'location',
title: 'Server Location',
result: locationResults,
Component: ServerLocationCard,
refresh: updateLocationResults,
tags: ['server'],
}, {
id: 'ssl',
title: 'SSL Certificate',
result: sslResults,
Component: SslCertCard,
refresh: updateSslResults,
tags: ['server', 'security'],
}, {
id: 'domain',
title: 'Domain Whois',
result: domainLookupResults,
Component: DomainLookup,
refresh: updateDomainLookupResults,
tags: ['server'],
}, {
id: 'quality',
title: 'Quality Summary',
result: lighthouseResults,
Component: LighthouseCard,
refresh: updateLighthouseResults,
tags: ['client'],
}, {
id: 'tech-stack',
title: 'Tech Stack',
result: techStackResults,
Component: TechStackCard,
refresh: updateTechStackResults,
tags: ['client', 'meta'],
}, {
id: 'server-info',
title: 'Server Info',
result: shoadnResults?.serverInfo,
Component: ServerInfoCard,
refresh: updateShodanResults,
tags: ['server'],
}, {
id: 'cookies',
title: 'Cookies',
result: cookieResults,
Component: CookiesCard,
refresh: updateCookieResults,
tags: ['client', 'security'],
}, {
id: 'headers',
title: 'Headers',
result: headersResults,
Component: HeadersCard,
refresh: updateHeadersResults,
tags: ['client', 'security'],
}, {
id: 'dns',
title: 'DNS Records',
result: dnsResults,
Component: DnsRecordsCard,
refresh: updateDnsResults,
tags: ['server'],
}, {
id: 'hosts',
title: 'Host Names',
result: shoadnResults?.hostnames,
Component: HostNamesCard,
refresh: updateShodanResults,
tags: ['server'],
}, {
id: 'http-security',
title: 'HTTP Security',
result: httpSecurityResults,
Component: HttpSecurityCard,
refresh: updateHttpSecurityResults,
tags: ['security'],
}, {
id: 'social-tags',
title: 'Social Tags',
result: socialTagResults,
Component: SocialTagsCard,
refresh: updateSocialTagResults,
tags: ['client', 'meta'],
}, {
id: 'trace-route',
title: 'Trace Route',
result: traceRouteResults,
Component: TraceRouteCard,
refresh: updateTraceRouteResults,
tags: ['server'],
}, {
id: 'security-txt',
title: 'Security.Txt',
result: securityTxtResults,
Component: SecurityTxtCard,
refresh: updateSecurityTxtResults,
tags: ['security'],
}, {
id: 'dns-server',
title: 'DNS Server',
result: dnsServerResults,
Component: DnsServerCard,
refresh: updateDnsServerResults,
tags: ['server'],
}, {
id: 'firewall',
title: 'Firewall',
result: firewallResults,
Component: FirewallCard,
refresh: updateFirewallResults,
tags: ['server', 'security'],
}, {
id: 'dnssec',
title: 'DNSSEC',
result: dnsSecResults,
Component: DnsSecCard,
refresh: updateDnsSecResults,
tags: ['security'],
}, {
id: 'hsts',
title: 'HSTS Check',
result: hstsResults,
Component: HstsCard,
refresh: updateHstsResults,
tags: ['security'],
}, {
id: 'threats',
title: 'Threats',
result: threatResults,
Component: ThreatsCard,
refresh: updateThreatResults,
tags: ['security'],
}, {
id: 'mail-config',
title: 'Email Configuration',
result: mailConfigResults,
Component: MailConfigCard,
refresh: updateMailConfigResults,
tags: ['server'],
}, {
id: 'archives',
title: 'Archive History',
result: archivesResults,
Component: ArchivesCard,
refresh: updateArchivesResults,
tags: ['meta'],
}, {
id: 'rank',
title: 'Global Ranking',
result: rankResults,
Component: RankCard,
refresh: updateRankResults,
tags: ['meta'],
}, {
id: 'screenshot',
title: 'Screenshot',
result: screenshotResult || lighthouseResults?.fullPageScreenshot?.screenshot,
Component: ScreenshotCard,
refresh: updateScreenshotResult,
tags: ['client', 'meta'],
}, {
id: 'tls-cipher-suites',
title: 'TLS Cipher Suites',
result: tlsResults,
Component: TlsCipherSuitesCard,
refresh: updateTlsResults,
tags: ['server', 'security'],
}, {
id: 'tls-security-config',
title: 'TLS Security Issues',
result: tlsResults,
Component: TlsIssueAnalysisCard,
refresh: updateTlsResults,
tags: ['security'],
}, {
id: 'tls-client-support',
title: 'TLS Handshake Simulation',
result: tlsResults,
Component: TlsClientSupportCard,
refresh: updateTlsResults,
tags: ['security'],
}, {
id: 'redirects',
title: 'Redirects',
result: redirectResults,
Component: RedirectsCard,
refresh: updateRedirectResults,
tags: ['meta'],
}, {
id: 'linked-pages',
title: 'Linked Pages',
result: linkedPagesResults,
Component: ContentLinksCard,
refresh: updateLinkedPagesResults,
tags: ['client', 'meta'],
}, {
id: 'robots-txt',
title: 'Crawl Rules',
result: robotsTxtResults,
Component: RobotsTxtCard,
refresh: updateRobotsTxtResults,
tags: ['meta'],
}, {
id: 'status',
title: 'Server Status',
result: serverStatusResults,
Component: ServerStatusCard,
refresh: updateServerStatusResults,
tags: ['server'],
}, {
id: 'ports',
title: 'Open Ports',
result: portsResults,
Component: OpenPortsCard,
refresh: updatePortsResults,
tags: ['server'],
}, {
id: 'whois',
title: 'Domain Info',
result: whoIsResults,
Component: WhoIsCard,
refresh: updateWhoIsResults,
tags: ['server'],
}, {
id: 'txt-records',
title: 'TXT Records',
result: txtRecordResults,
Component: TxtRecordCard,
refresh: updateTxtRecordResults,
tags: ['server'],
}, {
id: 'block-lists',
title: 'Block Lists',
result: blockListsResults,
Component: BlockListsCard,
refresh: updateBlockListsResults,
tags: ['security', 'meta'],
}, {
id: 'features',
title: 'Site Features',
result: siteFeaturesResults,
Component: SiteFeaturesCard,
refresh: updateSiteFeaturesResults,
tags: ['meta'],
}, {
id: 'sitemap',
title: 'Pages',
result: sitemapResults,
Component: SitemapCard,
refresh: updateSitemapResults,
tags: ['meta'],
}, {
id: 'carbon',
title: 'Carbon Footprint',
result: carbonResults,
Component: CarbonFootprintCard,
refresh: updateCarbonResults,
tags: ['meta'],
},
];
const makeActionButtons = (title: string, refresh: () => void, showInfo: (id: string) => void): ReactNode => {
const actions = [
{ label: `Info about ${title}`, onClick: showInfo, icon: 'ⓘ'},
{ label: `Re-fetch ${title} data`, onClick: refresh, icon: '↻'},
];
return (
<ActionButtons actions={actions} />
);
};
const showInfo = (id: string) => {
setModalContent(DocContent(id));
setModalOpen(true);
};
const showErrorModal = (content: ReactNode) => {
setModalContent(content);
setModalOpen(true);
};
return (
<ResultsOuter>
<Nav>
{ address &&
<Heading color={colors.textColor} size="medium">
{ addressType === 'url' && <a target="_blank" rel="noreferrer" href={address}><img width="32px" src={`https://icon.horse/icon/${makeSiteName(address)}`} alt="" /></a> }
{makeSiteName(address)}
</Heading>
}
</Nav>
<ProgressBar loadStatus={loadingJobs} showModal={showErrorModal} showJobDocs={showInfo} />
{/* { address?.includes(window?.location?.hostname || 'web-check.xyz') && <SelfScanMsg />} */}
<Loader show={loadingJobs.filter((job: LoadingJob) => job.state !== 'loading').length < 5} />
<FilterButtons>{ showFilters ? <>
<div className="one-half">
<span className="group-label">Filter by</span>
{['server', 'client', 'meta'].map((tag: string) => (
<button
key={tag}
className={tags.includes(tag) ? 'selected' : ''}
onClick={() => updateTags(tag)}>
{tag}
</button>
))}
{(tags.length > 0 || searchTerm.length > 0) && <span onClick={clearFilters} className="clear">Clear Filters</span> }
</div>
<div className="one-half">
<span className="group-label">Search</span>
<input
type="text"
placeholder="Filter Results"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
<span className="toggle-filters" onClick={() => setShowFilters(false)}>Hide</span>
</div>
</> : (
<div className="control-options">
<span className="toggle-filters" onClick={() => setShowFilters(true)}>Show Filters</span>
<a href="#view-download-raw-data"><span className="toggle-filters">Export Data</span></a>
<a href="/check/about"><span className="toggle-filters">Learn about the Results</span></a>
<a href="/check/about#additional-resources"><span className="toggle-filters">More tools</span></a>
<a target="_blank" rel="noreferrer" href="https://github.com/lissy93/web-check"><span className="toggle-filters">View GitHub</span></a>
</div>
) }
</FilterButtons>
<ResultsContent>
<Masonry
breakpointCols={{ 10000: 12, 4000: 9, 3600: 8, 3200: 7, 2800: 6, 2400: 5, 2000: 4, 1600: 3, 1200: 2, 800: 1 }}
className="masonry-grid"
columnClassName="masonry-grid-col">
{
resultCardData
.map(({ id, title, result, tags, refresh, Component }, index: number) => {
const show = (tags.length === 0 || tags.some(tag => tags.includes(tag)))
&& title.toLowerCase().includes(searchTerm.toLowerCase())
&& (result && !result.error);
return show ? (
<ErrorBoundary title={title} key={`eb-${index}`}>
<Component
key={`${title}-${index}`}
data={{...result}}
title={title}
actionButtons={refresh ? makeActionButtons(title, refresh, () => showInfo(id)) : undefined}
/>
</ErrorBoundary>
) : null})
}
</Masonry>
</ResultsContent>
<ViewRaw everything={resultCardData} />
<AdditionalResources url={address} />
<Footer />
<Modal isOpen={modalOpen} closeModal={()=> setModalOpen(false)}>{modalContent}</Modal>
<ToastContainer limit={3} draggablePercent={60} autoClose={2500} theme="dark" position="bottom-right" />
</ResultsOuter>
);
}
export default Results;