Rename v1 to web-check-live
This commit is contained in:
37
src/web-check-live/components/Results/Archives.tsx
Normal file
37
src/web-check-live/components/Results/Archives.tsx
Normal 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;
|
||||
21
src/web-check-live/components/Results/BlockLists.tsx
Normal file
21
src/web-check-live/components/Results/BlockLists.tsx
Normal 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;
|
||||
58
src/web-check-live/components/Results/BuiltWith.tsx
Normal file
58
src/web-check-live/components/Results/BuiltWith.tsx
Normal 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;
|
||||
49
src/web-check-live/components/Results/CarbonFootprint.tsx
Normal file
49
src/web-check-live/components/Results/CarbonFootprint.tsx
Normal 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;
|
||||
77
src/web-check-live/components/Results/ContentLinks.tsx
Normal file
77
src/web-check-live/components/Results/ContentLinks.tsx
Normal 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;
|
||||
49
src/web-check-live/components/Results/Cookies.tsx
Normal file
49
src/web-check-live/components/Results/Cookies.tsx
Normal 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;
|
||||
30
src/web-check-live/components/Results/DnsRecords.tsx
Normal file
30
src/web-check-live/components/Results/DnsRecords.tsx
Normal 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;
|
||||
210
src/web-check-live/components/Results/DnsSec.tsx
Normal file
210
src/web-check-live/components/Results/DnsSec.tsx
Normal 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;
|
||||
37
src/web-check-live/components/Results/DnsServer.tsx
Normal file
37
src/web-check-live/components/Results/DnsServer.tsx
Normal 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;
|
||||
32
src/web-check-live/components/Results/DomainLookup.tsx
Normal file
32
src/web-check-live/components/Results/DomainLookup.tsx
Normal 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;
|
||||
24
src/web-check-live/components/Results/Firewall.tsx
Normal file
24
src/web-check-live/components/Results/Firewall.tsx
Normal 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;
|
||||
20
src/web-check-live/components/Results/Headers.tsx
Normal file
20
src/web-check-live/components/Results/Headers.tsx
Normal 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;
|
||||
49
src/web-check-live/components/Results/HostNames.tsx
Normal file
49
src/web-check-live/components/Results/HostNames.tsx
Normal 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;
|
||||
42
src/web-check-live/components/Results/Hsts.tsx
Normal file
42
src/web-check-live/components/Results/Hsts.tsx
Normal 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;
|
||||
17
src/web-check-live/components/Results/HttpSecurity.tsx
Normal file
17
src/web-check-live/components/Results/HttpSecurity.tsx
Normal 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;
|
||||
52
src/web-check-live/components/Results/Lighthouse.tsx
Normal file
52
src/web-check-live/components/Results/Lighthouse.tsx
Normal 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;
|
||||
45
src/web-check-live/components/Results/MailConfig.tsx
Normal file
45
src/web-check-live/components/Results/MailConfig.tsx
Normal 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;
|
||||
27
src/web-check-live/components/Results/OpenPorts.tsx
Normal file
27
src/web-check-live/components/Results/OpenPorts.tsx
Normal 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;
|
||||
77
src/web-check-live/components/Results/Rank.tsx
Normal file
77
src/web-check-live/components/Results/Rank.tsx
Normal 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;
|
||||
|
||||
|
||||
42
src/web-check-live/components/Results/Redirects.tsx
Normal file
42
src/web-check-live/components/Results/Redirects.tsx
Normal 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;
|
||||
33
src/web-check-live/components/Results/RobotsTxt.tsx
Normal file
33
src/web-check-live/components/Results/RobotsTxt.tsx
Normal 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;
|
||||
24
src/web-check-live/components/Results/Screenshot.tsx
Normal file
24
src/web-check-live/components/Results/Screenshot.tsx
Normal 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;
|
||||
67
src/web-check-live/components/Results/SecurityTxt.tsx
Normal file
67
src/web-check-live/components/Results/SecurityTxt.tsx
Normal 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;
|
||||
22
src/web-check-live/components/Results/ServerInfo.tsx
Normal file
22
src/web-check-live/components/Results/ServerInfo.tsx
Normal 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;
|
||||
58
src/web-check-live/components/Results/ServerLocation.tsx
Normal file
58
src/web-check-live/components/Results/ServerLocation.tsx
Normal 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;
|
||||
27
src/web-check-live/components/Results/ServerStatus.tsx
Normal file
27
src/web-check-live/components/Results/ServerStatus.tsx
Normal 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;
|
||||
65
src/web-check-live/components/Results/SiteFeatures.tsx
Normal file
65
src/web-check-live/components/Results/SiteFeatures.tsx
Normal 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;
|
||||
60
src/web-check-live/components/Results/Sitemap.tsx
Normal file
60
src/web-check-live/components/Results/Sitemap.tsx
Normal 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;
|
||||
44
src/web-check-live/components/Results/SocialTags.tsx
Normal file
44
src/web-check-live/components/Results/SocialTags.tsx
Normal 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;
|
||||
99
src/web-check-live/components/Results/SslCert.tsx
Normal file
99
src/web-check-live/components/Results/SslCert.tsx
Normal 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;
|
||||
114
src/web-check-live/components/Results/TechStack.tsx
Normal file
114
src/web-check-live/components/Results/TechStack.tsx
Normal 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;
|
||||
88
src/web-check-live/components/Results/Threats.tsx
Normal file
88
src/web-check-live/components/Results/Threats.tsx
Normal 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;
|
||||
70
src/web-check-live/components/Results/TlsCipherSuites.tsx
Normal file
70
src/web-check-live/components/Results/TlsCipherSuites.tsx
Normal 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;
|
||||
84
src/web-check-live/components/Results/TlsClientSupport.tsx
Normal file
84
src/web-check-live/components/Results/TlsClientSupport.tsx
Normal 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;
|
||||
131
src/web-check-live/components/Results/TlsIssueAnalysis.tsx
Normal file
131
src/web-check-live/components/Results/TlsIssueAnalysis.tsx
Normal 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;
|
||||
64
src/web-check-live/components/Results/TraceRoute.tsx
Normal file
64
src/web-check-live/components/Results/TraceRoute.tsx
Normal 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;
|
||||
25
src/web-check-live/components/Results/TxtRecords.tsx
Normal file
25
src/web-check-live/components/Results/TxtRecords.tsx
Normal 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;
|
||||
69
src/web-check-live/components/Results/WhoIs.tsx
Normal file
69
src/web-check-live/components/Results/WhoIs.tsx
Normal 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;
|
||||
Reference in New Issue
Block a user