From c9c462f2a10981a0bb9a17ef88cba4b5d72a44c8 Mon Sep 17 00:00:00 2001 From: Alicia Sykes Date: Fri, 7 Jul 2023 20:58:04 +0100 Subject: [PATCH] Adds new Result cards, and updates params in old ones --- src/components/Results/CarbonFootprint.tsx | 49 +++++ src/components/Results/DnsRecords.tsx | 2 +- src/components/Results/DnsSec.tsx | 210 +++++++++++++++++++++ src/components/Results/HostNames.tsx | 2 +- src/components/Results/RobotsTxt.tsx | 3 +- src/components/Results/SiteFeatures.tsx | 58 ++++++ src/components/Results/TraceRoute.tsx | 8 +- 7 files changed, 326 insertions(+), 6 deletions(-) create mode 100644 src/components/Results/CarbonFootprint.tsx create mode 100644 src/components/Results/DnsSec.tsx create mode 100644 src/components/Results/SiteFeatures.tsx diff --git a/src/components/Results/CarbonFootprint.tsx b/src/components/Results/CarbonFootprint.tsx new file mode 100644 index 0000000..ead7b9c --- /dev/null +++ b/src/components/Results/CarbonFootprint.tsx @@ -0,0 +1,49 @@ +import { useEffect, useState } from 'react'; +import styled from 'styled-components'; +import { Card } from 'components/Form/Card'; +import Row from 'components/Form/Row'; +import colors from 'styles/colors'; + +const LearnMoreInfo = styled.p` +font-size: 0.8rem; +margin-top: 0.5rem; +opacity: 0.75; +a { color: ${colors.primary}; } +`; + +const CarbonCard = (props: { data: any, title: string, actionButtons: any }): JSX.Element => { + const carbons = props.data.statistics; + const initialUrl = props.data.scanUrl; + + const [carbonData, setCarbonData] = useState<{c?: number, p?: number}>({}); + + useEffect(() => { + const fetchCarbonData = async () => { + try { + const response = await fetch(`https://api.websitecarbon.com/b?url=${encodeURIComponent(initialUrl)}`); + const data = await response.json(); + setCarbonData(data); + } catch (error) { + console.error('Error fetching carbon data:', error); + } + }; + fetchCarbonData(); + }, [initialUrl]); + + return ( + + { (!carbons?.adjustedBytes && !carbonData.c) &&

Unable to calculate carbon footprint for host

} + { carbons?.adjustedBytes > 0 && <> + + + + } + {carbonData.c && } + {carbonData.p && } +
+ Learn more at websitecarbon.com +
+ ); +} + +export default CarbonCard; diff --git a/src/components/Results/DnsRecords.tsx b/src/components/Results/DnsRecords.tsx index b9018c9..6961325 100644 --- a/src/components/Results/DnsRecords.tsx +++ b/src/components/Results/DnsRecords.tsx @@ -3,7 +3,7 @@ import Row, { ListRow } from 'components/Form/Row'; const styles = ` .content { - max-height: 28rem; + max-height: 32rem; overflow-y: auto; } `; diff --git a/src/components/Results/DnsSec.tsx b/src/components/Results/DnsSec.tsx new file mode 100644 index 0000000..5a49cf8 --- /dev/null +++ b/src/components/Results/DnsSec.tsx @@ -0,0 +1,210 @@ +import { Card } from 'components/Form/Card'; +import Row, { ExpandableRow, RowProps } from 'components/Form/Row'; +import Heading from 'components/Form/Heading'; +import colors from 'styles/colors'; + + + +const parseDNSKeyData = (data: string) => { + const dnsKey = data.split(' '); + + const flags = parseInt(dnsKey[0]); + const protocol = parseInt(dnsKey[1]); + const algorithm = parseInt(dnsKey[2]); + + let flagMeaning = ''; + let protocolMeaning = ''; + let algorithmMeaning = ''; + + // Flags + if (flags === 256) { + flagMeaning = 'Zone Signing Key (ZSK)'; + } else if (flags === 257) { + flagMeaning = 'Key Signing Key (KSK)'; + } else { + flagMeaning = 'Unknown'; + } + + // Protocol + protocolMeaning = protocol === 3 ? 'DNSSEC' : 'Unknown'; + + // Algorithm + switch (algorithm) { + case 5: + algorithmMeaning = 'RSA/SHA-1'; + break; + case 7: + algorithmMeaning = 'RSASHA1-NSEC3-SHA1'; + break; + case 8: + algorithmMeaning = 'RSA/SHA-256'; + break; + case 10: + algorithmMeaning = 'RSA/SHA-512'; + break; + case 13: + algorithmMeaning = 'ECDSA Curve P-256 with SHA-256'; + break; + case 14: + algorithmMeaning = 'ECDSA Curve P-384 with SHA-384'; + break; + case 15: + algorithmMeaning = 'Ed25519'; + break; + case 16: + algorithmMeaning = 'Ed448'; + break; + default: + algorithmMeaning = 'Unknown'; + break; + } + + return { + flags: flagMeaning, + protocol: protocolMeaning, + algorithm: algorithmMeaning, + publicKey: dnsKey[3] + }; +} + +const getRecordTypeName = (typeCode: number): string => { + switch(typeCode) { + case 1: return 'A'; + case 2: return 'NS'; + case 5: return 'CNAME'; + case 6: return 'SOA'; + case 12: return 'PTR'; + case 13: return 'HINFO'; + case 15: return 'MX'; + case 16: return 'TXT'; + case 28: return 'AAAA'; + case 33: return 'SRV'; + case 35: return 'NAPTR'; + case 39: return 'DNAME'; + case 41: return 'OPT'; + case 43: return 'DS'; + case 46: return 'RRSIG'; + case 47: return 'NSEC'; + case 48: return 'DNSKEY'; + case 50: return 'NSEC3'; + case 51: return 'NSEC3PARAM'; + case 52: return 'TLSA'; + case 53: return 'SMIMEA'; + case 55: return 'HIP'; + case 56: return 'NINFO'; + case 57: return 'RKEY'; + case 58: return 'TALINK'; + case 59: return 'CDS'; + case 60: return 'CDNSKEY'; + case 61: return 'OPENPGPKEY'; + case 62: return 'CSYNC'; + case 63: return 'ZONEMD'; + default: return 'Unknown'; + } +} + +const parseDSData = (dsData: string) => { + const parts = dsData.split(' '); + + const keyTag = parts[0]; + const algorithm = getAlgorithmName(parseInt(parts[1], 10)); + const digestType = getDigestTypeName(parseInt(parts[2], 10)); + const digest = parts[3]; + + return { + keyTag, + algorithm, + digestType, + digest + }; +} + +const getAlgorithmName = (code: number) => { + switch(code) { + case 1: return 'RSA/MD5'; + case 2: return 'Diffie-Hellman'; + case 3: return 'DSA/SHA1'; + case 5: return 'RSA/SHA1'; + case 6: return 'DSA/NSEC3/SHA1'; + case 7: return 'RSASHA1/NSEC3/SHA1'; + case 8: return 'RSA/SHA256'; + case 10: return 'RSA/SHA512'; + case 12: return 'ECC/GOST'; + case 13: return 'ECDSA/CurveP256/SHA256'; + case 14: return 'ECDSA/CurveP384/SHA384'; + case 15: return 'Ed25519'; + case 16: return 'Ed448'; + default: return 'Unknown'; + } +} + +const getDigestTypeName = (code: number) => { + switch(code) { + case 1: return 'SHA1'; + case 2: return 'SHA256'; + case 3: return 'GOST R 34.11-94'; + case 4: return 'SHA384'; + default: return 'Unknown'; + } +} + +const makeResponseList = (response: any): RowProps[] => { + const result = [] as RowProps[]; + if (!response) return result; + if (typeof response.RD === 'boolean') result.push({ lbl: 'Recursion Desired (RD)', val: response.RD }); + if (typeof response.RA === 'boolean') result.push({ lbl: 'Recursion Available (RA)', val: response.RA }); + if (typeof response.TC === 'boolean') result.push({ lbl: 'TrunCation (TC)', val: response.TC }); + if (typeof response.AD === 'boolean') result.push({ lbl: 'Authentic Data (AD)', val: response.AD }); + if (typeof response.CD === 'boolean') result.push({ lbl: 'Checking Disabled (CD)', val: response.CD }); + return result; +}; + +const makeAnswerList = (recordData: any): RowProps[] => { + return [ + { lbl: 'Domain', val: recordData.name }, + { lbl: 'Type', val: `${getRecordTypeName(recordData.type)} (${recordData.type})` }, + { lbl: 'TTL', val: recordData.TTL }, + { lbl: 'Algorithm', val: recordData.algorithm }, + { lbl: 'Flags', val: recordData.flags }, + { lbl: 'Protocol', val: recordData.protocol }, + { lbl: 'Public Key', val: recordData.publicKey }, + { lbl: 'Key Tag', val: recordData.keyTag }, + { lbl: 'Digest Type', val: recordData.digestType }, + { lbl: 'Digest', val: recordData.digest }, + ].filter((rowData) => rowData.val && rowData.val !== 'Unknown'); +}; + +const DnsSecCard = (props: { data: any, title: string, actionButtons: any }): JSX.Element => { + const dnsSec = props.data; + return ( + + { + ['DNSKEY', 'DS', 'RRSIG'].map((key: string, index: number) => { + const record = dnsSec[key]; + return (
+ {key} + {(record.isFound && record.answer) && (<> + + { + 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 ( + + ); + }) + } + )} + + {(!record.isFound && record.response) && ( + + )} +
) + }) + } +
+ ); +} + +export default DnsSecCard; diff --git a/src/components/Results/HostNames.tsx b/src/components/Results/HostNames.tsx index 40cbe41..cc61ba9 100644 --- a/src/components/Results/HostNames.tsx +++ b/src/components/Results/HostNames.tsx @@ -28,7 +28,7 @@ const HostListSection = (props: { list: string[], title: string }) => { } const cardStyles = ` - max-height: 28rem; + max-height: 32rem; overflow: auto; `; diff --git a/src/components/Results/RobotsTxt.tsx b/src/components/Results/RobotsTxt.tsx index 8063506..c595021 100644 --- a/src/components/Results/RobotsTxt.tsx +++ b/src/components/Results/RobotsTxt.tsx @@ -3,8 +3,9 @@ import { Card } from 'components/Form/Card'; import Row, { RowProps } from 'components/Form/Row'; const cardStyles = ` + grid-row: span 2; .content { - max-height: 28rem; + max-height: 40rem; overflow-y: auto; } `; diff --git a/src/components/Results/SiteFeatures.tsx b/src/components/Results/SiteFeatures.tsx new file mode 100644 index 0000000..5cce10b --- /dev/null +++ b/src/components/Results/SiteFeatures.tsx @@ -0,0 +1,58 @@ +import { Card } from 'components/Form/Card'; +import colors from 'styles/colors'; +import Row, { ListRow } from 'components/Form/Row'; +import Heading from 'components/Form/Heading'; + +const styles = ` + .content { + max-height: 32rem; + overflow-y: auto; + } + + .scan-date { + font-size: 0.8rem; + margin-top: 0.5rem; + opacity: 0.75; + } +`; + +const formatDate = (timestamp: number): string => { + const date = new Date(timestamp * 1000); + 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 ( + +
+ { features.groups.filter((group: any) => group.categories.length > 0).map((group: any, index: number) => ( +
+ {group.name} + { group.categories.map((category: any, subIndex: number) => ( + // + + {category.name} + {category.live} Live {category.dead ? `(${category.dead} dead)` : ''} + + )) + } +
+ )) + } +
+

Last scanned on {formatDate(features.last)}

+
+ ); +} + +export default SiteFeaturesCard; diff --git a/src/components/Results/TraceRoute.tsx b/src/components/Results/TraceRoute.tsx index 2c6230c..bf4d41e 100644 --- a/src/components/Results/TraceRoute.tsx +++ b/src/components/Results/TraceRoute.tsx @@ -1,7 +1,6 @@ import styled from 'styled-components'; import colors from 'styles/colors'; import { Card } from 'components/Form/Card'; -import Heading from 'components/Form/Heading'; const RouteRow = styled.div` text-align: center; @@ -31,14 +30,17 @@ p { } `; +const cardStyles = ` + grid-row: span 2; +`; + const TraceRouteCard = (props: { data: any, title: string, actionButtons: any }): JSX.Element => { const traceRouteResponse = props.data; const routes = traceRouteResponse.result; return ( - + {routes.filter((x: any) => x).map((route: any, index: number) => ( - {/* {route} */} {Object.keys(route)[0]} {route[Object.keys(route)[0]].map((time: any, packetIndex: number) => (