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,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;