From edb2e559019fdef79b5ee7d82623181d794a7f6b Mon Sep 17 00:00:00 2001 From: lcampbell Date: Thu, 13 Apr 2023 16:40:40 -0300 Subject: [PATCH 01/40] create summaries for web connections(https+hsts), ssl(TLS), spf, and dkim --- services/summaries/summaries.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/services/summaries/summaries.py b/services/summaries/summaries.py index 4e87fdd830..33379765d2 100644 --- a/services/summaries/summaries.py +++ b/services/summaries/summaries.py @@ -18,8 +18,15 @@ DB_URL = os.getenv("DB_URL") SCAN_TYPES = ["https", "ssl", "dkim", "spf", "dmarc"] -CHARTS = {"mail": ["dmarc", "spf", "dkim"], "web": ["https", "ssl"], - "https": ["https"]} +CHARTS = { + "mail": ["dmarc", "spf", "dkim"], + "web": ["https", "ssl"], + "https": ["https"], + "webConnections": ["https", "hsts"], + "tls": ["ssl"], + "spf": ["spf"], + "dkim": ["dkim"], +} logging.basicConfig(stream=sys.stdout, level=logging.INFO) From 1dda1cc7d54a6774ce6924421f4ad71cf18187e5 Mon Sep 17 00:00:00 2001 From: lcampbell Date: Thu, 13 Apr 2023 16:42:02 -0300 Subject: [PATCH 02/40] create api queries --- api/src/summaries/queries/dkim-summary.js | 33 +++++++++++++++++++ api/src/summaries/queries/index.js | 4 +++ api/src/summaries/queries/spf-summary.js | 33 +++++++++++++++++++ api/src/summaries/queries/ssl-summary.js | 33 +++++++++++++++++++ .../queries/web-connections-summary.js | 33 +++++++++++++++++++ 5 files changed, 136 insertions(+) create mode 100644 api/src/summaries/queries/dkim-summary.js create mode 100644 api/src/summaries/queries/spf-summary.js create mode 100644 api/src/summaries/queries/ssl-summary.js create mode 100644 api/src/summaries/queries/web-connections-summary.js diff --git a/api/src/summaries/queries/dkim-summary.js b/api/src/summaries/queries/dkim-summary.js new file mode 100644 index 0000000000..9a91978ce4 --- /dev/null +++ b/api/src/summaries/queries/dkim-summary.js @@ -0,0 +1,33 @@ +import { categorizedSummaryType } from '../objects' +import { t } from '@lingui/macro' + +export const dkimSummary = { + type: categorizedSummaryType, + description: 'DKIM summary computed values, used to build summary cards.', + resolve: async (_, __, { i18n, loaders: { loadChartSummaryByKey } }) => { + const summary = await loadChartSummaryByKey.load('dkim') + + if (typeof summary === 'undefined') { + console.warn(`User could not retrieve DKIM summary.`) + throw new Error(i18n._(t`Unable to load DKIM summary. Please try again.`)) + } + + const categories = [ + { + name: 'pass', + count: summary.pass, + percentage: Number(((summary.pass / summary.total) * 100).toFixed(1)), + }, + { + name: 'fail', + count: summary.fail, + percentage: Number(((summary.fail / summary.total) * 100).toFixed(1)), + }, + ] + + return { + categories, + total: summary.total, + } + }, +} diff --git a/api/src/summaries/queries/index.js b/api/src/summaries/queries/index.js index 6b51658f0f..6437741cf7 100644 --- a/api/src/summaries/queries/index.js +++ b/api/src/summaries/queries/index.js @@ -2,3 +2,7 @@ export * from './mail-summary' export * from './web-summary' export * from './dmarc-phase-summary' export * from './https-summary' +export * from './ssl-summary' +export * from './web-connections-summary' +export * from './spf-summary' +export * from './dkim-summary' diff --git a/api/src/summaries/queries/spf-summary.js b/api/src/summaries/queries/spf-summary.js new file mode 100644 index 0000000000..c320ff40ff --- /dev/null +++ b/api/src/summaries/queries/spf-summary.js @@ -0,0 +1,33 @@ +import { categorizedSummaryType } from '../objects' +import { t } from '@lingui/macro' + +export const spfSummary = { + type: categorizedSummaryType, + description: 'SPF summary computed values, used to build summary cards.', + resolve: async (_, __, { i18n, loaders: { loadChartSummaryByKey } }) => { + const summary = await loadChartSummaryByKey.load('spf') + + if (typeof summary === 'undefined') { + console.warn(`User could not retrieve SPF summary.`) + throw new Error(i18n._(t`Unable to load SPF summary. Please try again.`)) + } + + const categories = [ + { + name: 'pass', + count: summary.pass, + percentage: Number(((summary.pass / summary.total) * 100).toFixed(1)), + }, + { + name: 'fail', + count: summary.fail, + percentage: Number(((summary.fail / summary.total) * 100).toFixed(1)), + }, + ] + + return { + categories, + total: summary.total, + } + }, +} diff --git a/api/src/summaries/queries/ssl-summary.js b/api/src/summaries/queries/ssl-summary.js new file mode 100644 index 0000000000..ceafecefde --- /dev/null +++ b/api/src/summaries/queries/ssl-summary.js @@ -0,0 +1,33 @@ +import { categorizedSummaryType } from '../objects' +import { t } from '@lingui/macro' + +export const sslSummary = { + type: categorizedSummaryType, + description: 'SSL summary computed values, used to build summary cards.', + resolve: async (_, __, { i18n, loaders: { loadChartSummaryByKey } }) => { + const summary = await loadChartSummaryByKey.load('ssl') + + if (typeof summary === 'undefined') { + console.warn(`User could not retrieve SSL summary.`) + throw new Error(i18n._(t`Unable to load SSL summary. Please try again.`)) + } + + const categories = [ + { + name: 'pass', + count: summary.pass, + percentage: Number(((summary.pass / summary.total) * 100).toFixed(1)), + }, + { + name: 'fail', + count: summary.fail, + percentage: Number(((summary.fail / summary.total) * 100).toFixed(1)), + }, + ] + + return { + categories, + total: summary.total, + } + }, +} diff --git a/api/src/summaries/queries/web-connections-summary.js b/api/src/summaries/queries/web-connections-summary.js new file mode 100644 index 0000000000..96e56ee887 --- /dev/null +++ b/api/src/summaries/queries/web-connections-summary.js @@ -0,0 +1,33 @@ +import { categorizedSummaryType } from '../objects' +import { t } from '@lingui/macro' + +export const webConnectionsSummary = { + type: categorizedSummaryType, + description: 'SSL summary computed values, used to build summary cards.', + resolve: async (_, __, { i18n, loaders: { loadChartSummaryByKey } }) => { + const summary = await loadChartSummaryByKey.load('webConnections') + + if (typeof summary === 'undefined') { + console.warn(`User could not retrieve web connections summary.`) + throw new Error(i18n._(t`Unable to load web connections summary. Please try again.`)) + } + + const categories = [ + { + name: 'pass', + count: summary.pass, + percentage: Number(((summary.pass / summary.total) * 100).toFixed(1)), + }, + { + name: 'fail', + count: summary.fail, + percentage: Number(((summary.fail / summary.total) * 100).toFixed(1)), + }, + ] + + return { + categories, + total: summary.total, + } + }, +} From 80e45d0eb86f9b047b98d1832caba18330f9709c Mon Sep 17 00:00:00 2001 From: lcampbell Date: Thu, 13 Apr 2023 16:42:27 -0300 Subject: [PATCH 03/40] create frontend queries for tiered summaries --- frontend/src/graphql/fragments.js | 15 ++++++++++ frontend/src/graphql/queries.js | 50 +++++++++++++++++++++++-------- 2 files changed, 52 insertions(+), 13 deletions(-) diff --git a/frontend/src/graphql/fragments.js b/frontend/src/graphql/fragments.js index 0f52dec2bf..4869ba9eb7 100644 --- a/frontend/src/graphql/fragments.js +++ b/frontend/src/graphql/fragments.js @@ -18,6 +18,21 @@ export const Authorization = { }, } +export const Summary = { + fragments: { + requiredFields: gql` + fragment RequiredSummaryFields on CategorizedSummary { + total + categories { + name + count + percentage + } + } + `, + }, +} + export const Guidance = { fragments: { requiredFields: gql` diff --git a/frontend/src/graphql/queries.js b/frontend/src/graphql/queries.js index e0533af7f8..68070ae2a2 100644 --- a/frontend/src/graphql/queries.js +++ b/frontend/src/graphql/queries.js @@ -1,5 +1,5 @@ import { gql } from '@apollo/client' -import { Guidance } from './fragments' +import { Guidance, Summary } from './fragments' export const PAGINATED_ORGANIZATIONS = gql` query PaginatedOrganizations( @@ -59,22 +59,46 @@ export const PAGINATED_ORGANIZATIONS = gql` export const HTTPS_AND_DMARC_SUMMARY = gql` query LandingPageSummaries { httpsSummary { - total - categories { - name - count - percentage - } + ...RequiredSummaryFields } dmarcPhaseSummary { - total - categories { - name - count - percentage - } + ...RequiredSummaryFields + } + } + ${Summary.fragments.requiredFields} +` + +export const TIER_TWO_SUMMARY = gql` + query TierTwoSummary { + connectionsSummary { + ...RequiredSummaryFields + } + sslSummary { + ...RequiredSummaryFields + } + spfSummary { + ...RequiredSummaryFields + } + dkimSummary { + ...RequiredSummaryFields + } + dmarcPhaseSummary { + ...RequiredSummaryFields + } + } + ${Summary.fragments.requiredFields} +` + +export const TIER_THREE_SUMMARY = gql` + query TierThreeSummary { + webSummary { + ...RequiredSummaryFields + } + mailSummary { + ...RequiredSummaryFields } } + ${Summary.fragments.requiredFields} ` export const GET_ORGANIZATION_DOMAINS_STATUSES_CSV = gql` From ee730524db2709ff02a12cb11b25496d6e3a098d Mon Sep 17 00:00:00 2001 From: lcampbell Date: Fri, 14 Apr 2023 10:40:28 -0300 Subject: [PATCH 04/40] fix ssl summary name --- services/summaries/summaries.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/summaries/summaries.py b/services/summaries/summaries.py index 33379765d2..a13b70038b 100644 --- a/services/summaries/summaries.py +++ b/services/summaries/summaries.py @@ -23,7 +23,7 @@ "web": ["https", "ssl"], "https": ["https"], "webConnections": ["https", "hsts"], - "tls": ["ssl"], + "ssl": ["ssl"], "spf": ["spf"], "dkim": ["dkim"], } From caedfb53d72182c70ab898708183b7829cd6b868 Mon Sep 17 00:00:00 2001 From: lcampbell Date: Fri, 14 Apr 2023 15:54:33 -0300 Subject: [PATCH 05/40] update faked schema --- frontend/mocking/faked_schema.js | 348 ++++++++++++++++++++++++------- 1 file changed, 268 insertions(+), 80 deletions(-) diff --git a/frontend/mocking/faked_schema.js b/frontend/mocking/faked_schema.js index 279560e685..dc68626204 100644 --- a/frontend/mocking/faked_schema.js +++ b/frontend/mocking/faked_schema.js @@ -167,6 +167,18 @@ export const getTypeNames = () => gql` # HTTPS summary computed values, used to build summary cards. httpsSummary: CategorizedSummary + # SSL summary computed values, used to build summary cards. + sslSummary: CategorizedSummary + + # SSL summary computed values, used to build summary cards. + webConnectionsSummary: CategorizedSummary + + # SPF summary computed values, used to build summary cards. + spfSummary: CategorizedSummary + + # DKIM summary computed values, used to build summary cards. + dkimSummary: CategorizedSummary + # Query the currently logged in user. findMe: PersonalUser @@ -308,8 +320,8 @@ export const getTypeNames = () => gql` # The ID of an object id: ID! - # Datetime string the activity occured. - timestamp: String + # Datetime string the activity occurred. + timestamp: DateTime # Username of admin that initiated the activity. initiatedBy: InitiatedBy @@ -324,6 +336,9 @@ export const getTypeNames = () => gql` reason: DomainRemovalReasonEnum } + # A date-time string at UTC, such as 2007-12-03T10:15:30Z, compliant with the 'date-time' format outlined in section 5.6 of the RFC 3339 profile of the ISO 8601 standard for representation of dates and times using the Gregorian calendar. + scalar DateTime + # Information on the user that initiated the logged action type InitiatedBy { # The ID of an object @@ -450,12 +465,12 @@ export const getTypeNames = () => gql` RESOURCE_NAME } - # Possible directions in which to order a list of items when provided an "orderBy" argument. + # Possible directions in which to order a list of items when provided an 'orderBy' argument. enum OrderDirection { - # Specifies an ascending order for a given "orderBy" argument. + # Specifies an ascending order for a given 'orderBy' argument. ASC - # Specifies a descending order for a given "orderBy" argument. + # Specifies a descending order for a given 'orderBy' argument. DESC } @@ -539,6 +554,15 @@ export const getTypeNames = () => gql` # The domains scan status, based on the latest scan data. status: DomainStatus + # Value that determines if a domain is excluded from any results and scans. + archived: Boolean + + # Value that determines if a domain is possibly blocked. + blocked: Boolean + + # Value that determines if a domain has a web scan pending. + webScanPending: Boolean + # The organization that this domain belongs to. organizations( # Ordering options for organization connections @@ -569,10 +593,10 @@ export const getTypeNames = () => gql` # DNS scan results. dnsScan( # Start date for date filter. - startDate: Date + startDate: DateTime # End date for date filter. - endDate: Date + endDate: DateTime # Ordering options for DNS connections. orderBy: DNSOrder @@ -596,10 +620,10 @@ export const getTypeNames = () => gql` # HTTPS, and TLS scan results. web( # Start date for date filter. - startDate: Date + startDate: DateTime # End date for date filter. - endDate: Date + endDate: DateTime # Ordering options for web connections. orderBy: WebOrder @@ -607,6 +631,9 @@ export const getTypeNames = () => gql` # Number of web scans to retrieve. limit: Int + # Exclude web scans which have pending status. + excludePending: Boolean + # Returns the items in the list that come after the specified cursor. after: String @@ -637,9 +664,6 @@ export const getTypeNames = () => gql` # Value that determines if a domain is excluded from an organization's results. hidden: Boolean - - # Value that determines if a domain is excluded from any results and scans. - archived: Boolean } # String that conforms to a domain structure. @@ -650,6 +674,9 @@ export const getTypeNames = () => gql` # This object contains how the domain is doing on the various scans we preform, based on the latest scan data. type DomainStatus { + # Certificates Status + certificates: StatusEnum + # Ciphers Status ciphers: StatusEnum @@ -766,6 +793,9 @@ export const getTypeNames = () => gql` # String used to search for domains. search: String + # Filters used to limit domains returned. + filters: [DomainFilter] + # Returns the items in the list that come after the specified cursor. after: String @@ -909,6 +939,75 @@ export const getTypeNames = () => gql` # Order domains by spf status. SPF_STATUS + + # Order domains by tags. + TAGS + } + + # This object is used to provide filtering options when querying org-claimed domains. + input DomainFilter { + # Category of filter to be applied. + filterCategory: DomainOrderField + + # First value equals or does not equal second value. + comparison: ComparisonEnums + + # Status type or tag label. + filterValue: filterValueEnums + } + + # + enum ComparisonEnums { + # + EQUAL + + # + NOT_EQUAL + } + + # + enum filterValueEnums { + # If the given check meets the passing requirements. + PASS + + # If the given check has flagged something that can provide information on the domain that aren't scan related. + INFO + + # If the given check does not meet the passing requirements + FAIL + + # English label for tagging domains as new to the system. + NEW + + # French label for tagging domains as new to the system. + NOUVEAU + + # Bilingual Label for tagging domains as a production environment. + PROD + + # English label for tagging domains as a staging environment. + STAGING + + # French label for tagging domains as a staging environment. + DEV + + # Bilingual label for tagging domains as a test environment. + TEST + + # Bilingual label for tagging domains as web-hosting. + WEB + + # English label for tagging domains that are not active. + INACTIVE + + # French label for tagging domains that are not active. + INACTIF + + # English label for tagging domains that are hidden. + HIDDEN + + # English label for tagging domains that are archived. + ARCHIVED } # A connection to a list of items. @@ -1164,7 +1263,7 @@ export const getTypeNames = () => gql` domain: String # The time when the scan was initiated. - timestamp: Date + timestamp: DateTime # String of the base domain the scan was run on. baseDomain: String @@ -1194,9 +1293,6 @@ export const getTypeNames = () => gql` dkim: DKIM } - # A date string, such as 2007-12-03, compliant with the "full-date" format outlined in section 5.6 of the RFC 3339 profile of the ISO 8601 standard for representation of dates and times using the Gregorian calendar. - scalar Date - type MXRecord { # Hosts listed in the domain's MX record. hosts: [MXHost] @@ -1330,6 +1426,15 @@ export const getTypeNames = () => gql` # The compliance status for DKIM for the scanned domain. status: String + # List of positive tags for the scanned domain from this scan. + positiveTags: [GuidanceTag] + + # List of neutral tags for the scanned domain from this scan. + neutralTags: [GuidanceTag] + + # List of negative tags for the scanned domain from this scan. + negativeTags: [GuidanceTag] + # Individual scans results for each DKIM selector. selectors: [DKIMSelectorResult] } @@ -1416,7 +1521,7 @@ export const getTypeNames = () => gql` domain: String # The time when the scan was initiated. - timestamp: Date + timestamp: DateTime # Results of the web scan at each IP address. results: [WebScan] @@ -1424,9 +1529,6 @@ export const getTypeNames = () => gql` # Information for the TLS and HTTP connection scans on the given domain. type WebScan { - # The time when the scan was initiated. - timestamp: Date - # IP address for scan target. ipAddress: String @@ -1440,7 +1542,7 @@ export const getTypeNames = () => gql` # Results of TLS and HTTP connection scans on the given domain. type WebScanResult { # The time when the scan was initiated. - timestamp: Date + timestamp: DateTime # The result for the TLS scan for the scanned server. tlsResult: TLSResult @@ -1466,6 +1568,9 @@ export const getTypeNames = () => gql` # Whether or not the scanned server is vulnerable to heartbleed. heartbleedVulnerable: Boolean + # Whether or not the scanned server is vulnerable to heartbleed. + robotVulnerable: String + # Whether or not the scanned server is vulnerable to CCS injection. ccsInjectionVulnerable: Boolean @@ -1484,6 +1589,9 @@ export const getTypeNames = () => gql` # List of negative tags for the scanned server from this scan. negativeTags: [GuidanceTag] + # The compliance status of the certificate bundle for the scanned server from this scan. + certificateStatus: String + # The compliance status for TLS for the scanned server from this scan. sslStatus: String @@ -1745,7 +1853,7 @@ export const getTypeNames = () => gql` HSTS: Boolean } - # The "JSONObject" scalar type represents JSON objects as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). + # The 'JSONObject' scalar type represents JSON objects as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). scalar JSONObject # Ordering options for web connections. @@ -2413,7 +2521,7 @@ export const getTypeNames = () => gql` domain: DomainScalar # The last time that a scan was ran on this domain. - lastRan: Date + lastRan: DateTime # The domains scan status, based on the latest scan data. status: DomainStatus @@ -2637,6 +2745,9 @@ export const getTypeNames = () => gql` # given organization. updateUserRole(input: UpdateUserRoleInput!): UpdateUserRolePayload + # Mutation used to create multiple new domains for an organization. + addOrganizationsDomains(input: AddOrganizationsDomainsInput!): AddOrganizationsDomainsPayload + # Mutation used to create a new domain for an organization. createDomain(input: CreateDomainInput!): CreateDomainPayload @@ -2646,6 +2757,9 @@ export const getTypeNames = () => gql` # This mutation allows the removal of unused domains. removeDomain(input: RemoveDomainInput!): RemoveDomainPayload + # This mutation allows the removal of unused domains. + removeOrganizationsDomains(input: RemoveOrganizationsDomainsInput!): RemoveOrganizationsDomainsPayload + # This mutation is used to step a manual scan on a requested domain. requestScan(input: RequestScanInput!): RequestScanPayload @@ -2714,12 +2828,12 @@ export const getTypeNames = () => gql` } type InviteUserToOrgPayload { - # "InviteUserToOrgUnion" returning either a "InviteUserToOrgResult", or "InviteUserToOrgError" object. + # 'InviteUserToOrgUnion' returning either a 'InviteUserToOrgResult', or 'InviteUserToOrgError' object. result: InviteUserToOrgUnion clientMutationId: String } - # This union is used with the "InviteUserToOrg" mutation, allowing for users to invite user to their org, and support any errors that may occur + # This union is used with the 'InviteUserToOrg' mutation, allowing for users to invite user to their org, and support any errors that may occur union InviteUserToOrgUnion = AffiliationError | InviteUserToOrgResult # This object is used to inform the user if any errors occurred while executing affiliation mutations. @@ -2753,12 +2867,12 @@ export const getTypeNames = () => gql` } type LeaveOrganizationPayload { - # "LeaveOrganizationUnion" resolving to either a "LeaveOrganizationResult" or "AffiliationError". + # 'LeaveOrganizationUnion' resolving to either a 'LeaveOrganizationResult' or 'AffiliationError'. result: LeaveOrganizationUnion clientMutationId: String } - # This union is used with the "leaveOrganization" mutation, allowing for users to leave a given organization, and support any errors that may occur. + # This union is used with the 'leaveOrganization' mutation, allowing for users to leave a given organization, and support any errors that may occur. union LeaveOrganizationUnion = AffiliationError | LeaveOrganizationResult # This object is used to inform the user that they successful left a given organization. @@ -2774,12 +2888,12 @@ export const getTypeNames = () => gql` } type RemoveUserFromOrgPayload { - # "RemoveUserFromOrgUnion" returning either a "RemoveUserFromOrgResult", or "RemoveUserFromOrgError" object. + # 'RemoveUserFromOrgUnion' returning either a 'RemoveUserFromOrgResult', or 'RemoveUserFromOrgError' object. result: RemoveUserFromOrgUnion clientMutationId: String } - # This union is used with the "RemoveUserFromOrg" mutation, allowing for users to remove a user from their org, and support any errors that may occur + # This union is used with the 'RemoveUserFromOrg' mutation, allowing for users to remove a user from their org, and support any errors that may occur union RemoveUserFromOrgUnion = AffiliationError | RemoveUserFromOrgResult # This object is used to inform the user of the removal status. @@ -2801,12 +2915,12 @@ export const getTypeNames = () => gql` } type TransferOrgOwnershipPayload { - # "TransferOrgOwnershipUnion" resolving to either a "TransferOrgOwnershipResult" or "AffiliationError". + # 'TransferOrgOwnershipUnion' resolving to either a 'TransferOrgOwnershipResult' or 'AffiliationError'. result: TransferOrgOwnershipUnion clientMutationId: String } - # This union is used with the "transferOrgOwnership" mutation, allowing for + # This union is used with the 'transferOrgOwnership' mutation, allowing for # users to transfer ownership of a given organization, and support any errors that may occur. union TransferOrgOwnershipUnion = AffiliationError | TransferOrgOwnershipResult @@ -2826,12 +2940,12 @@ export const getTypeNames = () => gql` } type UpdateUserRolePayload { - # "UpdateUserRoleUnion" returning either a "UpdateUserRoleResult", or "UpdateUserRoleError" object. + # 'UpdateUserRoleUnion' returning either a 'UpdateUserRoleResult', or 'UpdateUserRoleError' object. result: UpdateUserRoleUnion clientMutationId: String } - # This union is used with the "UpdateUserRole" mutation, allowing for users to update a users role in an org, and support any errors that may occur + # This union is used with the 'UpdateUserRole' mutation, allowing for users to update a users role in an org, and support any errors that may occur union UpdateUserRoleUnion = AffiliationError | UpdateUserRoleResult # This object is used to inform the user of the status of the role update. @@ -2855,16 +2969,16 @@ export const getTypeNames = () => gql` clientMutationId: String } - type CreateDomainPayload { - # "CreateDomainUnion" returning either a "Domain", or "CreateDomainError" object. - result: CreateDomainUnion + type AddOrganizationsDomainsPayload { + # 'BulkModifyDomainsUnion' returning either a 'DomainBulkResult', or 'DomainErrorType' object. + result: BulkModifyDomainsUnion clientMutationId: String } - # This union is used with the "CreateDomain" mutation, - # allowing for users to create a domain and add it to their org, - # and support any errors that may occur - union CreateDomainUnion = DomainError | Domain + # This union is used with the 'AddOrganizationsDomains' and 'RemoveOrganizationsDomains' mutation, + # allowing for users to add/remove multiple domains belonging to their org, + # and support any errors that may occur + union BulkModifyDomainsUnion = DomainError | DomainBulkResult # This object is used to inform the user if any errors occurred while using a domain mutation. type DomainError { @@ -2875,6 +2989,41 @@ export const getTypeNames = () => gql` description: String } + # This object is used to inform the user that no errors were encountered while mutating a domain. + type DomainBulkResult { + # Informs the user if the domain mutation was successful. + status: String + } + + input AddOrganizationsDomainsInput { + # The global id of the organization you wish to assign this domain to. + orgId: ID! + + # Url that you would like to be added to the database. + domains: [DomainScalar]! + + # New domains will be hidden. + hideNewDomains: Boolean + + # New domains will be tagged with NEW. + tagNewDomains: Boolean + + # Audit logs will be created. + audit: Boolean + clientMutationId: String + } + + type CreateDomainPayload { + # 'CreateDomainUnion' returning either a 'Domain', or 'CreateDomainError' object. + result: CreateDomainUnion + clientMutationId: String + } + + # This union is used with the 'CreateDomain' mutation, + # allowing for users to create a domain and add it to their org, + # and support any errors that may occur + union CreateDomainUnion = DomainError | Domain + input CreateDomainInput { # The global id of the organization you wish to assign this domain to. orgId: ID! @@ -2887,6 +3036,12 @@ export const getTypeNames = () => gql` # List of labelled tags users have applied to the domain. tags: [InputTag] + + # Value that determines if the domain is excluded from an organization's score. + hidden: Boolean + + # Value that determines if the domain is excluded from the scanning process. + archived: Boolean clientMutationId: String } @@ -2927,10 +3082,16 @@ export const getTypeNames = () => gql` # French label for tagging domains that are not active. INACTIF + + # English label for tagging domains that are hidden. + HIDDEN + + # English label for tagging domains that are archived. + ARCHIVED } type FavouriteDomainPayload { - # "CreateDomainUnion" returning either a "Domain", or "CreateDomainError" object. + # 'CreateDomainUnion' returning either a 'Domain', or 'CreateDomainError' object. result: CreateDomainUnion clientMutationId: String } @@ -2942,19 +3103,19 @@ export const getTypeNames = () => gql` } type RemoveDomainPayload { - # "RemoveDomainUnion" returning either a "DomainResultType", or "DomainErrorType" object. + # 'RemoveDomainUnion' returning either a 'DomainResultType', or 'DomainErrorType' object. result: RemoveDomainUnion! clientMutationId: String } - # This union is used with the "RemoveDomain" mutation, + # This union is used with the 'RemoveDomain' mutation, # allowing for users to remove a domain belonging to their org, # and support any errors that may occur union RemoveDomainUnion = DomainError | DomainResult - # This object is used to inform the user that no errors were encountered while removing a domain. + # This object is used to inform the user that no errors were encountered while mutating a domain. type DomainResult { - # Informs the user if the domain removal was successful. + # Informs the user if the domain mutation was successful. status: String # The domain that is being mutated. @@ -2973,6 +3134,27 @@ export const getTypeNames = () => gql` clientMutationId: String } + type RemoveOrganizationsDomainsPayload { + # 'BulkModifyDomainsUnion' returning either a 'DomainBulkResult', or 'DomainErrorType' object. + result: BulkModifyDomainsUnion! + clientMutationId: String + } + + input RemoveOrganizationsDomainsInput { + # Domains you wish to remove from the organization. + domains: [DomainScalar]! + + # The organization you wish to remove the domain from. + orgId: ID! + + # Domains will be archived. + archiveDomains: Boolean + + # Audit logs will be created. + audit: Boolean + clientMutationId: String + } + type RequestScanPayload { # Informs the user if the scan was dispatched successfully. status: String @@ -2986,7 +3168,7 @@ export const getTypeNames = () => gql` } type UnfavouriteDomainPayload { - # "RemoveDomainUnion" returning either a "DomainResultType", or "DomainErrorType" object. + # 'RemoveDomainUnion' returning either a 'DomainResultType', or 'DomainErrorType' object. result: RemoveDomainUnion! clientMutationId: String } @@ -2998,12 +3180,12 @@ export const getTypeNames = () => gql` } type UpdateDomainPayload { - # "UpdateDomainUnion" returning either a "Domain", or "DomainError" object. + # 'UpdateDomainUnion' returning either a 'Domain', or 'DomainError' object. result: UpdateDomainUnion clientMutationId: String } - # This union is used with the "UpdateDomain" mutation, + # This union is used with the 'UpdateDomain' mutation, # allowing for users to update a domain belonging to their org, # and support any errors that may occur union UpdateDomainUnion = DomainError | Domain @@ -3023,16 +3205,22 @@ export const getTypeNames = () => gql` # List of labelled tags users have applied to the domain. tags: [InputTag] + + # Value that determines if the domain is excluded from an organization's score. + hidden: Boolean + + # Value that determines if the domain is excluded from the scanning process. + archived: Boolean clientMutationId: String } type CreateOrganizationPayload { - # "CreateOrganizationUnion" returning either an "Organization", or "OrganizationError" object. + # 'CreateOrganizationUnion' returning either an 'Organization', or 'OrganizationError' object. result: CreateOrganizationUnion clientMutationId: String } - # This union is used with the "CreateOrganization" mutation, + # This union is used with the 'CreateOrganization' mutation, # allowing for users to create an organization, and support any errors that may occur union CreateOrganizationUnion = OrganizationError | Organization @@ -3091,12 +3279,12 @@ export const getTypeNames = () => gql` } type RemoveOrganizationPayload { - # "RemoveOrganizationUnion" returning either an "OrganizationResult", or "OrganizationError" object. + # 'RemoveOrganizationUnion' returning either an 'OrganizationResult', or 'OrganizationError' object. result: RemoveOrganizationUnion! clientMutationId: String } - # This union is used with the "RemoveOrganization" mutation, + # This union is used with the 'RemoveOrganization' mutation, # allowing for users to remove an organization they belong to, # and support any errors that may occur union RemoveOrganizationUnion = OrganizationError | OrganizationResult @@ -3117,12 +3305,12 @@ export const getTypeNames = () => gql` } type UpdateOrganizationPayload { - # "UpdateOrganizationUnion" returning either an "Organization", or "OrganizationError" object. + # 'UpdateOrganizationUnion' returning either an 'Organization', or 'OrganizationError' object. result: UpdateOrganizationUnion! clientMutationId: String } - # This union is used with the "UpdateOrganization" mutation, + # This union is used with the 'UpdateOrganization' mutation, # allowing for users to update an organization, and support any errors that may occur union UpdateOrganizationUnion = OrganizationError | Organization @@ -3175,12 +3363,12 @@ export const getTypeNames = () => gql` } type VerifyOrganizationPayload { - # "VerifyOrganizationUnion" returning either an "OrganizationResult", or "OrganizationError" object. + # 'VerifyOrganizationUnion' returning either an 'OrganizationResult', or 'OrganizationError' object. result: VerifyOrganizationUnion clientMutationId: String } - # This union is used with the "VerifyOrganization" mutation, + # This union is used with the 'VerifyOrganization' mutation, # allowing for super admins to verify an organization, # and support any errors that may occur union VerifyOrganizationUnion = OrganizationError | OrganizationResult @@ -3192,12 +3380,12 @@ export const getTypeNames = () => gql` } type AuthenticatePayload { - # Authenticate union returning either a "authResult" or "authenticateError" object. + # Authenticate union returning either a 'authResult' or 'authenticateError' object. result: AuthenticateUnion clientMutationId: String } - # This union is used with the "authenticate" mutation, allowing for the user to authenticate, and support any errors that may occur + # This union is used with the 'authenticate' mutation, allowing for the user to authenticate, and support any errors that may occur union AuthenticateUnion = AuthResult | AuthenticateError # An object used to return information when users sign up or authenticate. @@ -3228,12 +3416,12 @@ export const getTypeNames = () => gql` } type CloseAccountPayload { - # "CloseAccountUnion" returning either a "CloseAccountResult", or "CloseAccountError" object. + # 'CloseAccountUnion' returning either a 'CloseAccountResult', or 'CloseAccountError' object. result: CloseAccountUnion clientMutationId: String } - # This union is used for the "closeAccount" mutation, to support successful or errors that may occur. + # This union is used for the 'closeAccount' mutation, to support successful or errors that may occur. union CloseAccountUnion = CloseAccountResult | CloseAccountError # This object is used to inform the user of the status of closing their account. @@ -3258,12 +3446,12 @@ export const getTypeNames = () => gql` } type RefreshTokensPayload { - # Refresh tokens union returning either a "authResult" or "authenticateError" object. + # Refresh tokens union returning either a 'authResult' or 'authenticateError' object. result: RefreshTokensUnion clientMutationId: String } - # This union is used with the "refreshTokens" mutation, allowing for the user to refresh their tokens, and support any errors that may occur + # This union is used with the 'refreshTokens' mutation, allowing for the user to refresh their tokens, and support any errors that may occur union RefreshTokensUnion = AuthResult | AuthenticateError input RefreshTokensInput { @@ -3271,12 +3459,12 @@ export const getTypeNames = () => gql` } type RemovePhoneNumberPayload { - # "RemovePhoneNumberUnion" returning either a "RemovePhoneNumberResult", or "RemovePhoneNumberError" object. + # 'RemovePhoneNumberUnion' returning either a 'RemovePhoneNumberResult', or 'RemovePhoneNumberError' object. result: RemovePhoneNumberUnion clientMutationId: String } - # This union is used with the "RemovePhoneNumber" mutation, allowing for users to remove their phone number, and support any errors that may occur + # This union is used with the 'RemovePhoneNumber' mutation, allowing for users to remove their phone number, and support any errors that may occur union RemovePhoneNumberUnion = RemovePhoneNumberError | RemovePhoneNumberResult # This object is used to inform the user if any errors occurred while removing their phone number. @@ -3299,12 +3487,12 @@ export const getTypeNames = () => gql` } type ResetPasswordPayload { - # "ResetPasswordUnion" returning either a "ResetPasswordResult", or "ResetPasswordError" object. + # 'ResetPasswordUnion' returning either a 'ResetPasswordResult', or 'ResetPasswordError' object. result: ResetPasswordUnion clientMutationId: String } - # This union is used with the "ResetPassword" mutation, allowing for users to reset their password, and support any errors that may occur + # This union is used with the 'ResetPassword' mutation, allowing for users to reset their password, and support any errors that may occur union ResetPasswordUnion = ResetPasswordError | ResetPasswordResult # This object is used to inform the user if any errors occurred while resetting their password. @@ -3359,12 +3547,12 @@ export const getTypeNames = () => gql` } type SetPhoneNumberPayload { - # "SetPhoneNumberUnion" returning either a "SetPhoneNumberResult", or "SetPhoneNumberError" object. + # 'SetPhoneNumberUnion' returning either a 'SetPhoneNumberResult', or 'SetPhoneNumberError' object. result: SetPhoneNumberUnion clientMutationId: String } - # This union is used with the "setPhoneNumber" mutation, allowing for users to send a verification code to their phone, and support any errors that may occur + # This union is used with the 'setPhoneNumber' mutation, allowing for users to send a verification code to their phone, and support any errors that may occur union SetPhoneNumberUnion = SetPhoneNumberError | SetPhoneNumberResult # This object is used to inform the user if any errors occurred while setting a new phone number. @@ -3392,12 +3580,12 @@ export const getTypeNames = () => gql` } type SignInPayload { - # "SignInUnion" returning either a "regularSignInResult", "tfaSignInResult", or "signInError" object. + # 'SignInUnion' returning either a 'regularSignInResult', 'tfaSignInResult', or 'signInError' object. result: SignInUnion clientMutationId: String } - # This union is used with the "SignIn" mutation, allowing for multiple styles of logging in, and support any errors that may occur + # This union is used with the 'SignIn' mutation, allowing for multiple styles of logging in, and support any errors that may occur union SignInUnion = AuthResult | SignInError | TFASignInResult # This object is used to inform the user if any errors occurred during sign in. @@ -3441,12 +3629,12 @@ export const getTypeNames = () => gql` } type SignUpPayload { - # "SignUpUnion" returning either a "AuthResult", or "SignUpError" object. + # 'SignUpUnion' returning either a 'AuthResult', or 'SignUpError' object. result: SignUpUnion clientMutationId: String } - # This union is used with the "signUp" mutation, allowing for the user to sign up, and support any errors that may occur. + # This union is used with the 'signUp' mutation, allowing for the user to sign up, and support any errors that may occur. union SignUpUnion = AuthResult | SignUpError # This object is used to inform the user if any errors occurred during sign up. @@ -3483,12 +3671,12 @@ export const getTypeNames = () => gql` } type UpdateUserPasswordPayload { - # "UpdateUserPasswordUnion" returning either a "UpdateUserPasswordResultType", or "UpdateUserPasswordError" object. + # 'UpdateUserPasswordUnion' returning either a 'UpdateUserPasswordResultType', or 'UpdateUserPasswordError' object. result: UpdateUserPasswordUnion clientMutationId: String } - # This union is used with the "updateUserPassword" mutation, allowing for users to update their password, and support any errors that may occur + # This union is used with the 'updateUserPassword' mutation, allowing for users to update their password, and support any errors that may occur union UpdateUserPasswordUnion = UpdateUserPasswordError | UpdateUserPasswordResultType # This object is used to inform the user if any errors occurred while updating their password. @@ -3519,12 +3707,12 @@ export const getTypeNames = () => gql` } type UpdateUserProfilePayload { - # "UpdateUserProfileUnion" returning either a "UpdateUserProfileResult", or "UpdateUserProfileError" object. + # 'UpdateUserProfileUnion' returning either a 'UpdateUserProfileResult', or 'UpdateUserProfileError' object. result: UpdateUserProfileUnion clientMutationId: String } - # This union is used with the "updateUserProfile" mutation, allowing for users to update their profile, and support any errors that may occur + # This union is used with the 'updateUserProfile' mutation, allowing for users to update their profile, and support any errors that may occur union UpdateUserProfileUnion = UpdateUserProfileError | UpdateUserProfileResult # This object is used to inform the user if any errors occurred while updating their profile. @@ -3564,12 +3752,12 @@ export const getTypeNames = () => gql` } type VerifyAccountPayload { - # "VerifyAccountUnion" returning either a "VerifyAccountResult", or "VerifyAccountError" object. + # 'VerifyAccountUnion' returning either a 'VerifyAccountResult', or 'VerifyAccountError' object. result: VerifyAccountUnion clientMutationId: String } - # This union is used with the "verifyAccount" mutation, allowing for users to verify their account, and support any errors that may occur + # This union is used with the 'verifyAccount' mutation, allowing for users to verify their account, and support any errors that may occur union VerifyAccountUnion = VerifyAccountError | VerifyAccountResult # This object is used to inform the user if any errors occurred while verifying their account. @@ -3594,12 +3782,12 @@ export const getTypeNames = () => gql` } type verifyPhoneNumberPayload { - # "VerifyPhoneNumberUnion" returning either a "VerifyPhoneNumberResult", or "VerifyPhoneNumberError" object. + # 'VerifyPhoneNumberUnion' returning either a 'VerifyPhoneNumberResult', or 'VerifyPhoneNumberError' object. result: VerifyPhoneNumberUnion clientMutationId: String } - # This union is used with the "verifyPhoneNumber" mutation, allowing for users to verify their phone number, and support any errors that may occur + # This union is used with the 'verifyPhoneNumber' mutation, allowing for users to verify their phone number, and support any errors that may occur union VerifyPhoneNumberUnion = VerifyPhoneNumberError | VerifyPhoneNumberResult # This object is used to inform the user if any errors occurred while verifying their phone number. From 9235dba65a08f4134f0aefb45e3e66e11aff4788 Mon Sep 17 00:00:00 2001 From: lcampbell Date: Fri, 14 Apr 2023 15:54:58 -0300 Subject: [PATCH 06/40] create components for tiered summaries --- frontend/src/graphql/queries.js | 2 +- frontend/src/landing/LandingPage.js | 18 ++- frontend/src/landing/LandingPageSummaries.js | 7 +- frontend/src/summaries/TierThreeSummaries.js | 63 ++++++++ frontend/src/summaries/TierTwoSummaries.js | 143 +++++++++++++++++++ 5 files changed, 222 insertions(+), 11 deletions(-) create mode 100644 frontend/src/summaries/TierThreeSummaries.js create mode 100644 frontend/src/summaries/TierTwoSummaries.js diff --git a/frontend/src/graphql/queries.js b/frontend/src/graphql/queries.js index 68070ae2a2..817d27de0c 100644 --- a/frontend/src/graphql/queries.js +++ b/frontend/src/graphql/queries.js @@ -70,7 +70,7 @@ export const HTTPS_AND_DMARC_SUMMARY = gql` export const TIER_TWO_SUMMARY = gql` query TierTwoSummary { - connectionsSummary { + webConnectionsSummary { ...RequiredSummaryFields } sslSummary { diff --git a/frontend/src/landing/LandingPage.js b/frontend/src/landing/LandingPage.js index 810a44efa4..b76494fd81 100644 --- a/frontend/src/landing/LandingPage.js +++ b/frontend/src/landing/LandingPage.js @@ -5,6 +5,8 @@ import { Trans } from '@lingui/macro' import { LandingPageSummaries } from './LandingPageSummaries' import { useLingui } from '@lingui/react' import { bool } from 'prop-types' +import { TierTwoSummaries } from '../summaries/TierTwoSummaries' +import { TierThreeSummaries } from '../summaries/TierThreeSummaries' const emailUrlEn = 'https://www.canada.ca/en/government/system/digital-government/policies-standards/enterprise-it-service-common-configurations/email.html' @@ -26,10 +28,8 @@ export function LandingPage({ loginRequired, isLoggedIn }) { - Canadians rely on the Government of Canada to provide secure digital - services. The Policy on Service and Digital guides government online - services to adopt good security practices for practices outlined in - the{' '} + Canadians rely on the Government of Canada to provide secure digital services. The Policy on Service and + Digital guides government online services to adopt good security practices for practices outlined in the{' '} - {(!loginRequired || isLoggedIn) && } + {(!loginRequired || isLoggedIn) && ( + + + + + + + + )} ) } diff --git a/frontend/src/landing/LandingPageSummaries.js b/frontend/src/landing/LandingPageSummaries.js index b2bfbdeda9..d233e94006 100644 --- a/frontend/src/landing/LandingPageSummaries.js +++ b/frontend/src/landing/LandingPageSummaries.js @@ -12,13 +12,10 @@ export function LandingPageSummaries() { if (loading) return if (error) return - + // console.log(JSON.stringify(data)) return ( - + ) } diff --git a/frontend/src/summaries/TierThreeSummaries.js b/frontend/src/summaries/TierThreeSummaries.js new file mode 100644 index 0000000000..e79eb58535 --- /dev/null +++ b/frontend/src/summaries/TierThreeSummaries.js @@ -0,0 +1,63 @@ +import React from 'react' +import { Box, Flex } from '@chakra-ui/react' +import { useQuery } from '@apollo/client' +import { SummaryCard } from './SummaryCard' + +// import theme from '../theme/canada' +import { LoadingMessage } from '../components/LoadingMessage' +import { ErrorFallbackMessage } from '../components/ErrorFallbackMessage' +import { TIER_THREE_SUMMARY } from '../graphql/queries' +import { t } from '@lingui/macro' + +export function TierThreeSummaries() { + // const { colors } = theme + const failColour = '#22485B' + const passColour = '#F15E6B' + const { loading, error, data } = useQuery(TIER_THREE_SUMMARY) + if (loading) return + if (error) return + console.log(JSON.stringify(data)) + const { webSummary, mailSummary } = data + + return ( + + + + + + + + ) +} diff --git a/frontend/src/summaries/TierTwoSummaries.js b/frontend/src/summaries/TierTwoSummaries.js new file mode 100644 index 0000000000..e3ea521e65 --- /dev/null +++ b/frontend/src/summaries/TierTwoSummaries.js @@ -0,0 +1,143 @@ +import React from 'react' +import { Box, Flex } from '@chakra-ui/react' +import { useQuery } from '@apollo/client' +import { SummaryCard } from './SummaryCard' + +// import theme from '../theme/canada' +import { LoadingMessage } from '../components/LoadingMessage' +import { ErrorFallbackMessage } from '../components/ErrorFallbackMessage' +import { TIER_TWO_SUMMARY } from '../graphql/queries' +import { t } from '@lingui/macro' + +export function TierTwoSummaries() { + // const { colors } = theme + const failColour = '#22485B' + const passColour = '#F15E6B' + const { loading, error, data } = useQuery(TIER_TWO_SUMMARY) + if (loading) return + if (error) return + console.log(JSON.stringify(data)) + const { webConnectionsSummary, sslSummary, spfSummary, dkimSummary, dmarcPhaseSummary } = data + + const dmarcPhases = () => { + let dmarcFailCount = 0 + let dmarcFailPercentage = 0 + dmarcPhaseSummary.categories.forEach(({ name, count, percentage }) => { + if (name !== 'maintain') { + dmarcFailCount += count + dmarcFailPercentage += percentage + } + }) + const maintain = dmarcPhaseSummary.categories.find(({ name }) => name === 'maintain') + return { + categories: [ + { + name: 'fail', + count: dmarcFailCount, + percentage: dmarcFailPercentage, + }, + { + name: 'pass', + count: maintain.count, + percentage: maintain.percentage, + }, + ], + total: dmarcPhaseSummary.total, + } + } + + return ( + + + + + + + + + + + + + ) +} From f453fa3a6a4ab4040a74bb13fd8854549ee1f75a Mon Sep 17 00:00:00 2001 From: lcampbell Date: Mon, 17 Apr 2023 16:08:12 -0300 Subject: [PATCH 07/40] add new summaries to org summaries --- services/summaries/summaries.py | 52 ++++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/services/summaries/summaries.py b/services/summaries/summaries.py index a13b70038b..594ef073f7 100644 --- a/services/summaries/summaries.py +++ b/services/summaries/summaries.py @@ -246,6 +246,14 @@ def update_org_summaries(host=DB_URL, name=DB_NAME, user=DB_USER, web_pass = 0 mail_fail = 0 mail_pass = 0 + web_connections_fail = 0 + web_connections_pass = 0 + ssl_fail = 0 + ssl_pass = 0 + spf_fail = 0 + spf_pass = 0 + dkim_fail = 0 + dkim_pass = 0 dmarc_phase_not_implemented = 0 dmarc_phase_assess = 0 dmarc_phase_deploy = 0 @@ -290,6 +298,26 @@ def update_org_summaries(host=DB_URL, name=DB_NAME, user=DB_USER, else: mail_fail = mail_fail + 1 + if domain.get("status", {}).get("spf") == "pass": + spf_pass = spf_pass + 1 + else: + spf_fail = spf_fail + 1 + + if domain.get("status", {}).get("dkim") == "pass": + dkim_pass = dkim_pass + 1 + else: + dkim_fail = dkim_fail + 1 + + if domain.get("status", {}).get("ssl") == "pass": + ssl_pass = ssl_pass + 1 + elif domain.get("status", {}).get("ssl") == "fail": + ssl_fail = ssl_fail + 1 + + if (domain.get("status", {}).get("https") == "pass" and domain.get("status", {}).get("hsts") == "pass"): + web_connections_pass = web_connections_pass + 1 + elif (domain.get("status", {}).get("https") == "fail" or domain.get("status", {}).get("hsts") == "fail"): + web_connections_fail = web_connections_fail + 1 + phase = domain.get("phase") if phase is None: @@ -339,7 +367,29 @@ def update_org_summaries(host=DB_URL, name=DB_NAME, user=DB_USER, "fail": https_fail, "total": https_pass + https_fail # Don't count non web-hosting domains - } + }, + "ssl": { + "pass": ssl_pass, + "fail": ssl_fail, + "total": ssl_pass + ssl_fail + # Don't count non web-hosting domains + }, + "spf": { + "pass": spf_pass, + "fail": spf_fail, + "total": spf_pass + spf_fail + }, + "dkim": { + "pass": dkim_pass, + "fail": dkim_fail, + "total": dkim_pass + dkim_fail + }, + "web_connections": { + "pass": web_connections_pass, + "fail": web_connections_fail, + "total": web_connections_pass + web_connections_fail + # Don't count non web-hosting domains + }, } } From ceb9bdf25b322662169c5c5cb08c17a9c9527632 Mon Sep 17 00:00:00 2001 From: lcampbell Date: Mon, 17 Apr 2023 16:10:18 -0300 Subject: [PATCH 08/40] make tabbed summary component --- frontend/src/landing/LandingPage.js | 6 -- frontend/src/landing/LandingPageSummaries.js | 2 +- frontend/src/summaries/SummaryGroup.js | 12 ++-- frontend/src/summaries/TierThreeSummaries.js | 21 +++--- frontend/src/summaries/TierTwoSummaries.js | 73 +++++--------------- frontend/src/summaries/TieredSummaries.js | 63 +++++++++++++++++ frontend/src/theme/canada.js | 4 ++ 7 files changed, 100 insertions(+), 81 deletions(-) create mode 100644 frontend/src/summaries/TieredSummaries.js diff --git a/frontend/src/landing/LandingPage.js b/frontend/src/landing/LandingPage.js index b76494fd81..cb40e23fbb 100644 --- a/frontend/src/landing/LandingPage.js +++ b/frontend/src/landing/LandingPage.js @@ -5,8 +5,6 @@ import { Trans } from '@lingui/macro' import { LandingPageSummaries } from './LandingPageSummaries' import { useLingui } from '@lingui/react' import { bool } from 'prop-types' -import { TierTwoSummaries } from '../summaries/TierTwoSummaries' -import { TierThreeSummaries } from '../summaries/TierThreeSummaries' const emailUrlEn = 'https://www.canada.ca/en/government/system/digital-government/policies-standards/enterprise-it-service-common-configurations/email.html' @@ -52,10 +50,6 @@ export function LandingPage({ loginRequired, isLoggedIn }) { {(!loginRequired || isLoggedIn) && ( - - - - )} diff --git a/frontend/src/landing/LandingPageSummaries.js b/frontend/src/landing/LandingPageSummaries.js index d233e94006..bd7e712b2e 100644 --- a/frontend/src/landing/LandingPageSummaries.js +++ b/frontend/src/landing/LandingPageSummaries.js @@ -12,7 +12,7 @@ export function LandingPageSummaries() { if (loading) return if (error) return - // console.log(JSON.stringify(data)) + return ( diff --git a/frontend/src/summaries/SummaryGroup.js b/frontend/src/summaries/SummaryGroup.js index c8bf476e93..b1731773ac 100644 --- a/frontend/src/summaries/SummaryGroup.js +++ b/frontend/src/summaries/SummaryGroup.js @@ -18,11 +18,11 @@ export function SummaryGroup({ https, dmarcPhases }) { categoryDisplay={{ fail: { name: t`Non-compliant`, - color: '#22485B', + color: colors.summaries.fail, }, pass: { name: t`Compliant`, - color: '#F15E6B', + color: colors.summaries.pass, }, unscanned: { name: t`Unscanned`, @@ -34,9 +34,7 @@ export function SummaryGroup({ https, dmarcPhases }) { /> ) : ( - - No HTTPS configuration information available for this organization. - + No HTTPS configuration information available for this organization. ) @@ -48,7 +46,7 @@ export function SummaryGroup({ https, dmarcPhases }) { categoryDisplay={{ 'not implemented': { name: t`Not Implemented`, - color: '#22485B', + color: colors.summaries.fail, }, unscanned: { name: t`Unscanned`, @@ -56,7 +54,7 @@ export function SummaryGroup({ https, dmarcPhases }) { }, implemented: { name: t`Implemented`, - color: '#F15E6B', + color: colors.summaries.pass, }, }} data={dmarcPhases} diff --git a/frontend/src/summaries/TierThreeSummaries.js b/frontend/src/summaries/TierThreeSummaries.js index e79eb58535..7c86478907 100644 --- a/frontend/src/summaries/TierThreeSummaries.js +++ b/frontend/src/summaries/TierThreeSummaries.js @@ -3,37 +3,34 @@ import { Box, Flex } from '@chakra-ui/react' import { useQuery } from '@apollo/client' import { SummaryCard } from './SummaryCard' -// import theme from '../theme/canada' +import theme from '../theme/canada' import { LoadingMessage } from '../components/LoadingMessage' import { ErrorFallbackMessage } from '../components/ErrorFallbackMessage' import { TIER_THREE_SUMMARY } from '../graphql/queries' import { t } from '@lingui/macro' export function TierThreeSummaries() { - // const { colors } = theme - const failColour = '#22485B' - const passColour = '#F15E6B' + const { colors } = theme const { loading, error, data } = useQuery(TIER_THREE_SUMMARY) if (loading) return if (error) return - console.log(JSON.stringify(data)) const { webSummary, mailSummary } = data return ( - + if (error) return - console.log(JSON.stringify(data)) const { webConnectionsSummary, sslSummary, spfSummary, dkimSummary, dmarcPhaseSummary } = data + const categoryDisplay = { + fail: { + name: t`Non-compliant`, + color: colors.summaries.fail, + }, + pass: { + name: t`Compliant`, + color: colors.summaries.pass, + }, + } + const dmarcPhases = () => { let dmarcFailCount = 0 let dmarcFailPercentage = 0 @@ -53,16 +61,7 @@ export function TierTwoSummaries() { id="webConnectionsSummary" title={t`Web Connections Summary`} description={t`Web connections are configured to use HTTPS and valid HSTS`} - categoryDisplay={{ - fail: { - name: t`Non-compliant`, - color: failColour, - }, - pass: { - name: t`Compliant`, - color: passColour, - }, - }} + categoryDisplay={categoryDisplay} data={webConnectionsSummary} mb={{ base: 6, md: 0 }} /> @@ -71,16 +70,7 @@ export function TierTwoSummaries() { id="sslSummary" title={t`TLS Summary`} description={t`TLS certificate is valid and configured to use strong ciphers`} - categoryDisplay={{ - fail: { - name: t`Non-compliant`, - color: failColour, - }, - pass: { - name: t`Compliant`, - color: passColour, - }, - }} + categoryDisplay={categoryDisplay} data={sslSummary} mb={{ base: 6, md: 0 }} /> @@ -90,16 +80,7 @@ export function TierTwoSummaries() { id="spfSummary" title={t`SPF Summary`} description={t`SPF record is configured and valid`} - categoryDisplay={{ - fail: { - name: t`Non-compliant`, - color: failColour, - }, - pass: { - name: t`Compliant`, - color: passColour, - }, - }} + categoryDisplay={categoryDisplay} data={spfSummary} mb={{ base: 6, md: 0 }} /> @@ -107,16 +88,7 @@ export function TierTwoSummaries() { id="dkimSummary" title={t`DKIM Summary`} description={t`DKIM record is configured and valid`} - categoryDisplay={{ - fail: { - name: t`Non-compliant`, - color: failColour, - }, - pass: { - name: t`Compliant`, - color: passColour, - }, - }} + categoryDisplay={categoryDisplay} data={dkimSummary} mb={{ base: 6, md: 0 }} /> @@ -124,16 +96,7 @@ export function TierTwoSummaries() { id="dmarcPhaseSummary" title={t`DMARC Summary`} description={t`A DMARC phase of maintain is configured`} - categoryDisplay={{ - fail: { - name: t`Non-compliant`, - color: failColour, - }, - pass: { - name: t`Compliant`, - color: passColour, - }, - }} + categoryDisplay={categoryDisplay} data={dmarcPhases()} mb={{ base: 6, md: 0 }} /> diff --git a/frontend/src/summaries/TieredSummaries.js b/frontend/src/summaries/TieredSummaries.js new file mode 100644 index 0000000000..776dc3970b --- /dev/null +++ b/frontend/src/summaries/TieredSummaries.js @@ -0,0 +1,63 @@ +import React, { useState } from 'react' +import { Flex, Tabs, TabPanels, TabPanel, IconButton, Text } from '@chakra-ui/react' +import { ArrowLeftIcon, ArrowRightIcon } from '@chakra-ui/icons' + +import { LandingPageSummaries } from '../landing/LandingPageSummaries' +import { TierTwoSummaries } from './TierTwoSummaries' +import { TierThreeSummaries } from './TierThreeSummaries' +import { Trans } from '@lingui/macro' + +export function TieredSummaries() { + const [tabIndex, setTabIndex] = useState(0) + + const handleBackBtn = () => { + tabIndex === 0 ? setTabIndex(2) : setTabIndex(tabIndex - 1) + } + + const handleFwdBtn = () => { + tabIndex === 2 ? setTabIndex(0) : setTabIndex(tabIndex + 1) + } + + return ( + + } + onClick={handleBackBtn} + /> + + + + + Tier 1 + + + + + + Tier 2 + + + + + + Tier 3 + + + + + + } + onClick={handleFwdBtn} + /> + + ) +} diff --git a/frontend/src/theme/canada.js b/frontend/src/theme/canada.js index 379732b7bc..fca31a860f 100644 --- a/frontend/src/theme/canada.js +++ b/frontend/src/theme/canada.js @@ -59,6 +59,10 @@ export default extendTheme({ medium: '#F7FFDA', high: '#FFE1B0', critical: '#FFB3BC', + summaries: { + fail: '#22485B', + pass: '#F15E6B', + }, green: { 50: '#F2FFF0', 100: '#C3EEBF', From d5c25902870227cdb5d5409423e3ee152b5fd3de Mon Sep 17 00:00:00 2001 From: lcampbell Date: Tue, 18 Apr 2023 09:09:45 -0300 Subject: [PATCH 09/40] give orgs summary stats for hidden domains --- services/summaries/summaries.py | 163 +++++++++++++++++++------------- 1 file changed, 97 insertions(+), 66 deletions(-) diff --git a/services/summaries/summaries.py b/services/summaries/summaries.py index 594ef073f7..e222bd142c 100644 --- a/services/summaries/summaries.py +++ b/services/summaries/summaries.py @@ -259,6 +259,12 @@ def update_org_summaries(host=DB_URL, name=DB_NAME, user=DB_USER, dmarc_phase_deploy = 0 dmarc_phase_enforce = 0 dmarc_phase_maintain = 0 + + hidden_https_pass = 0 + hidden_https_fail = 0 + hidden_dmarc_pass = 0 + hidden_dmarc_fail = 0 + domain_total = 0 claims = db.collection("claims").find({"_from": org["_id"]}) @@ -266,75 +272,88 @@ def update_org_summaries(host=DB_URL, name=DB_NAME, user=DB_USER, domain = db.collection("domains").get({"_id": claim["_to"]}) archived = domain.get("archived") hidden = claim.get("hidden") - if hidden != True and archived != True: - domain_total = domain_total + 1 - if domain.get("status", {}).get("dmarc") == "pass": - dmarc_pass = dmarc_pass + 1 - else: - dmarc_fail = dmarc_fail + 1 - - if ( - domain.get("status", {}).get("ssl") == "pass" - and domain.get("status", {}).get("https") == "pass" - ): - web_pass = web_pass + 1 - elif ( - domain.get("status", {}).get("ssl") == "fail" - or domain.get("status", {}).get("https") == "fail" - ): - web_fail = web_fail + 1 - - if domain.get("status", {}).get("https") == "pass": - https_pass = https_pass + 1 - if domain.get("status", {}).get("https") == "fail": - https_fail = https_fail + 1 - - if ( - domain.get("status", {}).get("dmarc") == "pass" - and domain.get("status", {}).get("spf") == "pass" - and domain.get("status", {}).get("dkim") == "pass" - ): - mail_pass = mail_pass + 1 + if archived != True: + if hidden != True: + domain_total = domain_total + 1 + if domain.get("status", {}).get("dmarc") == "pass": + dmarc_pass = dmarc_pass + 1 + else: + dmarc_fail = dmarc_fail + 1 + + if ( + domain.get("status", {}).get("ssl") == "pass" + and domain.get("status", {}).get("https") == "pass" + ): + web_pass = web_pass + 1 + elif ( + domain.get("status", {}).get("ssl") == "fail" + or domain.get("status", {}).get("https") == "fail" + ): + web_fail = web_fail + 1 + + if domain.get("status", {}).get("https") == "pass": + https_pass = https_pass + 1 + if domain.get("status", {}).get("https") == "fail": + https_fail = https_fail + 1 + + if ( + domain.get("status", {}).get("dmarc") == "pass" + and domain.get("status", {}).get("spf") == "pass" + and domain.get("status", {}).get("dkim") == "pass" + ): + mail_pass = mail_pass + 1 + else: + mail_fail = mail_fail + 1 + + if domain.get("status", {}).get("spf") == "pass": + spf_pass = spf_pass + 1 + else: + spf_fail = spf_fail + 1 + + if domain.get("status", {}).get("dkim") == "pass": + dkim_pass = dkim_pass + 1 + else: + dkim_fail = dkim_fail + 1 + + if domain.get("status", {}).get("ssl") == "pass": + ssl_pass = ssl_pass + 1 + elif domain.get("status", {}).get("ssl") == "fail": + ssl_fail = ssl_fail + 1 + + if (domain.get("status", {}).get("https") == "pass" and domain.get("status", {}).get("hsts") == "pass"): + web_connections_pass = web_connections_pass + 1 + elif (domain.get("status", {}).get("https") == "fail" or domain.get("status", {}).get("hsts") == "fail"): + web_connections_fail = web_connections_fail + 1 + + phase = domain.get("phase") + + if phase is None: + logging.info( + f"Property \"phase\" does not exist for domain \"${domain['domain']}\".") + continue + + if phase == "not implemented": + dmarc_phase_not_implemented = dmarc_phase_not_implemented + 1 + elif phase == "assess": + dmarc_phase_assess = dmarc_phase_assess + 1 + elif phase == "deploy": + dmarc_phase_deploy = dmarc_phase_deploy + 1 + elif phase == "enforce": + dmarc_phase_enforce = dmarc_phase_enforce + 1 + elif phase == "maintain": + dmarc_phase_maintain = dmarc_phase_maintain + 1 else: - mail_fail = mail_fail + 1 + if domain.get("status", {}).get("dmarc") == "pass": + hidden_dmarc_pass = hidden_dmarc_pass + 1 + else: + hidden_dmarc_fail = hidden_dmarc_fail + 1 - if domain.get("status", {}).get("spf") == "pass": - spf_pass = spf_pass + 1 - else: - spf_fail = spf_fail + 1 + if domain.get("status", {}).get("https") == "pass": + hidden_https_pass = hidden_https_pass + 1 + elif domain.get("status", {}).get("https") == "fail": + hidden_https_fail = hidden_https_fail + 1 - if domain.get("status", {}).get("dkim") == "pass": - dkim_pass = dkim_pass + 1 - else: - dkim_fail = dkim_fail + 1 - - if domain.get("status", {}).get("ssl") == "pass": - ssl_pass = ssl_pass + 1 - elif domain.get("status", {}).get("ssl") == "fail": - ssl_fail = ssl_fail + 1 - - if (domain.get("status", {}).get("https") == "pass" and domain.get("status", {}).get("hsts") == "pass"): - web_connections_pass = web_connections_pass + 1 - elif (domain.get("status", {}).get("https") == "fail" or domain.get("status", {}).get("hsts") == "fail"): - web_connections_fail = web_connections_fail + 1 - - phase = domain.get("phase") - - if phase is None: - logging.info( - f"Property \"phase\" does not exist for domain \"${domain['domain']}\".") - continue - - if phase == "not implemented": - dmarc_phase_not_implemented = dmarc_phase_not_implemented + 1 - elif phase == "assess": - dmarc_phase_assess = dmarc_phase_assess + 1 - elif phase == "deploy": - dmarc_phase_deploy = dmarc_phase_deploy + 1 - elif phase == "enforce": - dmarc_phase_enforce = dmarc_phase_enforce + 1 - elif phase == "maintain": - dmarc_phase_maintain = dmarc_phase_maintain + 1 + summary_data = { "summaries": { @@ -390,6 +409,18 @@ def update_org_summaries(host=DB_URL, name=DB_NAME, user=DB_USER, "total": web_connections_pass + web_connections_fail # Don't count non web-hosting domains }, + "hidden": { + "dmarc": { + "pass": hidden_dmarc_pass, + "fail": hidden_dmarc_fail, + "total": hidden_dmarc_pass + hidden_dmarc_fail + }, + "https": { + "pass": hidden_https_pass, + "fail": hidden_https_fail, + "total": hidden_https_pass + hidden_https_fail + } + } } } From d913f0d11f5cd984382d457a6ef13d27ff52744d Mon Sep 17 00:00:00 2001 From: lcampbell Date: Tue, 18 Apr 2023 09:57:32 -0300 Subject: [PATCH 10/40] move arrow btns to top of TieredSummaries component --- frontend/src/summaries/TieredSummaries.js | 52 +++++++++++------------ 1 file changed, 24 insertions(+), 28 deletions(-) diff --git a/frontend/src/summaries/TieredSummaries.js b/frontend/src/summaries/TieredSummaries.js index 776dc3970b..72ca3a57c2 100644 --- a/frontend/src/summaries/TieredSummaries.js +++ b/frontend/src/summaries/TieredSummaries.js @@ -1,5 +1,5 @@ import React, { useState } from 'react' -import { Flex, Tabs, TabPanels, TabPanel, IconButton, Text } from '@chakra-ui/react' +import { Flex, Tabs, TabPanels, TabPanel, IconButton, Text, Box } from '@chakra-ui/react' import { ArrowLeftIcon, ArrowRightIcon } from '@chakra-ui/icons' import { LandingPageSummaries } from '../landing/LandingPageSummaries' @@ -19,45 +19,41 @@ export function TieredSummaries() { } return ( - - } - onClick={handleBackBtn} - /> + + + } + onClick={handleBackBtn} + /> + + Tier {tabIndex + 1} + + } + onClick={handleFwdBtn} + /> + - - Tier 1 - - - Tier 2 - - - Tier 3 - - } - onClick={handleFwdBtn} - /> - + ) } From b4368834307a368653f95653cfefb6729b7a0b53 Mon Sep 17 00:00:00 2001 From: lcampbell Date: Tue, 18 Apr 2023 09:58:15 -0300 Subject: [PATCH 11/40] add new org summaries to api --- .../objects/organization-summary.js | 242 +++++++++++++++--- frontend/mocking/faked_schema.js | 18 ++ frontend/src/graphql/queries.js | 42 ++- 3 files changed, 260 insertions(+), 42 deletions(-) diff --git a/api/src/organization/objects/organization-summary.js b/api/src/organization/objects/organization-summary.js index 68e00334f6..4593fce600 100644 --- a/api/src/organization/objects/organization-summary.js +++ b/api/src/organization/objects/organization-summary.js @@ -8,8 +8,7 @@ export const organizationSummaryType = new GraphQLObjectType({ fields: () => ({ dmarc: { type: categorizedSummaryType, - description: - 'Summary based on DMARC scan results for a given organization.', + description: 'Summary based on DMARC scan results for a given organization.', resolve: ({ dmarc }, _) => { let percentPass, percentageFail if (dmarc.total <= 0) { @@ -41,8 +40,7 @@ export const organizationSummaryType = new GraphQLObjectType({ }, https: { type: categorizedSummaryType, - description: - 'Summary based on HTTPS scan results for a given organization.', + description: 'Summary based on HTTPS scan results for a given organization.', resolve: ({ https }, _) => { let percentPass, percentageFail if (https.total <= 0) { @@ -74,8 +72,7 @@ export const organizationSummaryType = new GraphQLObjectType({ }, mail: { type: categorizedSummaryType, - description: - 'Summary based on mail scan results for a given organization.', + description: 'Summary based on mail scan results for a given organization.', resolve: ({ mail }, _) => { let percentPass, percentageFail if (mail.total <= 0) { @@ -107,8 +104,7 @@ export const organizationSummaryType = new GraphQLObjectType({ }, web: { type: categorizedSummaryType, - description: - 'Summary based on web scan results for a given organization.', + description: 'Summary based on web scan results for a given organization.', resolve: ({ web }, _) => { let percentPass, percentageFail if (web.total <= 0) { @@ -142,11 +138,7 @@ export const organizationSummaryType = new GraphQLObjectType({ type: categorizedSummaryType, description: 'Summary based on DMARC phases for a given organization.', resolve: ({ dmarc_phase }, _) => { - let percentNotImplemented, - percentAsses, - percentDeploy, - percentEnforce, - percentMaintain + let percentNotImplemented, percentAsses, percentDeploy, percentEnforce, percentMaintain if (dmarc_phase.total <= 0) { percentNotImplemented = 0 percentAsses = 0 @@ -154,23 +146,11 @@ export const organizationSummaryType = new GraphQLObjectType({ percentEnforce = 0 percentMaintain = 0 } else { - percentNotImplemented = Number( - ((dmarc_phase.not_implemented / dmarc_phase.total) * 100).toFixed( - 1, - ), - ) - percentAsses = Number( - ((dmarc_phase.assess / dmarc_phase.total) * 100).toFixed(1), - ) - percentDeploy = Number( - ((dmarc_phase.deploy / dmarc_phase.total) * 100).toFixed(1), - ) - percentEnforce = Number( - ((dmarc_phase.enforce / dmarc_phase.total) * 100).toFixed(1), - ) - percentMaintain = Number( - ((dmarc_phase.maintain / dmarc_phase.total) * 100).toFixed(1), - ) + percentNotImplemented = Number(((dmarc_phase.not_implemented / dmarc_phase.total) * 100).toFixed(1)) + percentAsses = Number(((dmarc_phase.assess / dmarc_phase.total) * 100).toFixed(1)) + percentDeploy = Number(((dmarc_phase.deploy / dmarc_phase.total) * 100).toFixed(1)) + percentEnforce = Number(((dmarc_phase.enforce / dmarc_phase.total) * 100).toFixed(1)) + percentMaintain = Number(((dmarc_phase.maintain / dmarc_phase.total) * 100).toFixed(1)) } const categories = [ @@ -207,5 +187,207 @@ export const organizationSummaryType = new GraphQLObjectType({ } }, }, + ssl: { + type: categorizedSummaryType, + description: 'Summary based on SSL scan results for a given organization.', + resolve: ({ ssl }, _) => { + let percentPass, percentageFail + if (ssl.total <= 0) { + percentPass = 0 + percentageFail = 0 + } else { + percentPass = Number(((ssl.pass / ssl.total) * 100).toFixed(1)) + percentageFail = Number(((ssl.fail / ssl.total) * 100).toFixed(1)) + } + + const categories = [ + { + name: 'pass', + count: ssl.pass, + percentage: percentPass, + }, + { + name: 'fail', + count: ssl.fail, + percentage: percentageFail, + }, + ] + + return { + categories, + total: ssl.total, + } + }, + }, + webConnections: { + type: categorizedSummaryType, + description: 'Summary based on HTTPS and HSTS scan results for a given organization.', + resolve: ({ web_connections }, _) => { + let percentPass, percentageFail + if (web_connections.total <= 0) { + percentPass = 0 + percentageFail = 0 + } else { + percentPass = Number(((web_connections.pass / web_connections.total) * 100).toFixed(1)) + percentageFail = Number(((web_connections.fail / web_connections.total) * 100).toFixed(1)) + } + + const categories = [ + { + name: 'pass', + count: web_connections.pass, + percentage: percentPass, + }, + { + name: 'fail', + count: web_connections.fail, + percentage: percentageFail, + }, + ] + + return { + categories, + total: web_connections.total, + } + }, + }, + spf: { + type: categorizedSummaryType, + description: 'Summary based on SPF scan results for a given organization.', + resolve: ({ spf }, _) => { + let percentPass, percentageFail + if (spf.total <= 0) { + percentPass = 0 + percentageFail = 0 + } else { + percentPass = Number(((spf.pass / spf.total) * 100).toFixed(1)) + percentageFail = Number(((spf.fail / spf.total) * 100).toFixed(1)) + } + + const categories = [ + { + name: 'pass', + count: spf.pass, + percentage: percentPass, + }, + { + name: 'fail', + count: spf.fail, + percentage: percentageFail, + }, + ] + + return { + categories, + total: spf.total, + } + }, + }, + dkim: { + type: categorizedSummaryType, + description: 'Summary based on DKIM scan results for a given organization.', + resolve: ({ dkim }, _) => { + let percentPass, percentageFail + if (dkim.total <= 0) { + percentPass = 0 + percentageFail = 0 + } else { + percentPass = Number(((dkim.pass / dkim.total) * 100).toFixed(1)) + percentageFail = Number(((dkim.fail / dkim.total) * 100).toFixed(1)) + } + + const categories = [ + { + name: 'pass', + count: dkim.pass, + percentage: percentPass, + }, + { + name: 'fail', + count: dkim.fail, + percentage: percentageFail, + }, + ] + + return { + categories, + total: dkim.total, + } + }, + }, + httpsIncludeHidden: { + type: categorizedSummaryType, + description: + 'Summary based on HTTPS scan results for a given organization that includes domains marked as hidden.', + resolve: ({ https, hidden }, _) => { + const pass = https.pass + hidden.https.pass + const fail = https.fail + hidden.https.fail + const total = https.total + hidden.https.total + + let percentPass, percentageFail + if (total <= 0) { + percentPass = 0 + percentageFail = 0 + } else { + percentPass = Number(((pass / total) * 100).toFixed(1)) + percentageFail = Number(((fail / total) * 100).toFixed(1)) + } + + const categories = [ + { + name: 'pass', + count: pass, + percentage: percentPass, + }, + { + name: 'fail', + count: fail, + percentage: percentageFail, + }, + ] + + return { + categories, + total, + } + }, + }, + dmarcIncludeHidden: { + type: categorizedSummaryType, + description: + 'Summary based on HTTPS scan results for a given organization that includes domains marked as hidden.', + resolve: ({ dmarc, hidden }, _) => { + const pass = dmarc.pass + hidden.dmarc.pass + const fail = dmarc.fail + hidden.dmarc.fail + const total = dmarc.total + hidden.https.total + + let percentPass, percentageFail + if (total <= 0) { + percentPass = 0 + percentageFail = 0 + } else { + percentPass = Number(((pass / total) * 100).toFixed(1)) + percentageFail = Number(((fail / total) * 100).toFixed(1)) + } + + const categories = [ + { + name: 'pass', + count: pass, + percentage: percentPass, + }, + { + name: 'fail', + count: fail, + percentage: percentageFail, + }, + ] + + return { + categories, + total, + } + }, + }, }), }) diff --git a/frontend/mocking/faked_schema.js b/frontend/mocking/faked_schema.js index dc68626204..07c7e05449 100644 --- a/frontend/mocking/faked_schema.js +++ b/frontend/mocking/faked_schema.js @@ -853,6 +853,24 @@ export const getTypeNames = () => gql` # Summary based on DMARC phases for a given organization. dmarcPhase: CategorizedSummary + + # Summary based on SSL scan results for a given organization. + ssl: CategorizedSummary + + # Summary based on HTTPS and HSTS scan results for a given organization. + webConnections: CategorizedSummary + + # Summary based on SPF scan results for a given organization. + spf: CategorizedSummary + + # Summary based on DKIM scan results for a given organization. + dkim: CategorizedSummary + + # Summary based on HTTPS scan results for a given organization that includes domains marked as hidden. + httpsIncludeHidden: CategorizedSummary + + # Summary based on HTTPS scan results for a given organization that includes domains marked as hidden. + dmarcIncludeHidden: CategorizedSummary } # This object contains the list of different categories for pre-computed diff --git a/frontend/src/graphql/queries.js b/frontend/src/graphql/queries.js index 817d27de0c..fbd6582c95 100644 --- a/frontend/src/graphql/queries.js +++ b/frontend/src/graphql/queries.js @@ -487,24 +487,42 @@ export const ORG_DETAILS_PAGE = gql` verified summaries { https { - total - categories { - name - count - percentage - } + ...RequiredSummaryFields + } + dmarc { + ...RequiredSummaryFields + } + httpsIncludeHidden { + ...RequiredSummaryFields + } + dmarcIncludeHidden { + ...RequiredSummaryFields + } + dkim { + ...RequiredSummaryFields + } + spf { + ...RequiredSummaryFields + } + ssl { + ...RequiredSummaryFields + } + webConnections { + ...RequiredSummaryFields } dmarcPhase { - total - categories { - name - count - percentage - } + ...RequiredSummaryFields + } + web { + ...RequiredSummaryFields + } + mail { + ...RequiredSummaryFields } } } } + ${Summary.fragments.requiredFields} ` export const PAGINATED_ORG_DOMAINS = gql` From f3af39ceca100c5da89cff9d93bdcaea13258346 Mon Sep 17 00:00:00 2001 From: lcampbell Date: Tue, 18 Apr 2023 12:57:20 -0300 Subject: [PATCH 12/40] add chart dmarc summary --- api/src/summaries/queries/dmarc-summary.js | 33 ++++++++++++++++++++++ api/src/summaries/queries/index.js | 9 +++--- frontend/mocking/faked_schema.js | 23 ++++++++------- frontend/src/graphql/queries.js | 2 +- services/summaries/summaries.py | 1 + 5 files changed, 53 insertions(+), 15 deletions(-) create mode 100644 api/src/summaries/queries/dmarc-summary.js diff --git a/api/src/summaries/queries/dmarc-summary.js b/api/src/summaries/queries/dmarc-summary.js new file mode 100644 index 0000000000..e7ca64b3c8 --- /dev/null +++ b/api/src/summaries/queries/dmarc-summary.js @@ -0,0 +1,33 @@ +import { categorizedSummaryType } from '../objects' +import { t } from '@lingui/macro' + +export const dmarcSummary = { + type: categorizedSummaryType, + description: 'DMARC summary computed values, used to build summary cards.', + resolve: async (_, __, { i18n, loaders: { loadChartSummaryByKey } }) => { + const summary = await loadChartSummaryByKey.load('dmarc') + + if (typeof summary === 'undefined') { + console.warn(`User could not retrieve DMARC summary.`) + throw new Error(i18n._(t`Unable to load DMARC summary. Please try again.`)) + } + + const categories = [ + { + name: 'pass', + count: summary.pass, + percentage: Number(((summary.pass / summary.total) * 100).toFixed(1)), + }, + { + name: 'fail', + count: summary.fail, + percentage: Number(((summary.fail / summary.total) * 100).toFixed(1)), + }, + ] + + return { + categories, + total: summary.total, + } + }, +} diff --git a/api/src/summaries/queries/index.js b/api/src/summaries/queries/index.js index 6437741cf7..c4a8068b9a 100644 --- a/api/src/summaries/queries/index.js +++ b/api/src/summaries/queries/index.js @@ -1,8 +1,9 @@ -export * from './mail-summary' -export * from './web-summary' +export * from './dkim-summary' export * from './dmarc-phase-summary' +export * from './dmarc-summary' export * from './https-summary' +export * from './mail-summary' +export * from './spf-summary' export * from './ssl-summary' export * from './web-connections-summary' -export * from './spf-summary' -export * from './dkim-summary' +export * from './web-summary' diff --git a/frontend/mocking/faked_schema.js b/frontend/mocking/faked_schema.js index 07c7e05449..21945836ca 100644 --- a/frontend/mocking/faked_schema.js +++ b/frontend/mocking/faked_schema.js @@ -155,29 +155,32 @@ export const getTypeNames = () => gql` # CSV formatted output of all domains in all organizations including their email and web scan statuses. getAllOrganizationDomainStatuses: String - # Email summary computed values, used to build summary cards. - mailSummary: CategorizedSummary - - # Web summary computed values, used to build summary cards. - webSummary: CategorizedSummary + # DKIM summary computed values, used to build summary cards. + dkimSummary: CategorizedSummary # DMARC phase summary computed values, used to build summary cards. dmarcPhaseSummary: CategorizedSummary + # DMARC summary computed values, used to build summary cards. + dmarcSummary: CategorizedSummary + # HTTPS summary computed values, used to build summary cards. httpsSummary: CategorizedSummary + # Email summary computed values, used to build summary cards. + mailSummary: CategorizedSummary + + # SPF summary computed values, used to build summary cards. + spfSummary: CategorizedSummary + # SSL summary computed values, used to build summary cards. sslSummary: CategorizedSummary # SSL summary computed values, used to build summary cards. webConnectionsSummary: CategorizedSummary - # SPF summary computed values, used to build summary cards. - spfSummary: CategorizedSummary - - # DKIM summary computed values, used to build summary cards. - dkimSummary: CategorizedSummary + # Web summary computed values, used to build summary cards. + webSummary: CategorizedSummary # Query the currently logged in user. findMe: PersonalUser diff --git a/frontend/src/graphql/queries.js b/frontend/src/graphql/queries.js index fbd6582c95..57f69725ff 100644 --- a/frontend/src/graphql/queries.js +++ b/frontend/src/graphql/queries.js @@ -61,7 +61,7 @@ export const HTTPS_AND_DMARC_SUMMARY = gql` httpsSummary { ...RequiredSummaryFields } - dmarcPhaseSummary { + dmarcSummary { ...RequiredSummaryFields } } diff --git a/services/summaries/summaries.py b/services/summaries/summaries.py index e222bd142c..19e113033d 100644 --- a/services/summaries/summaries.py +++ b/services/summaries/summaries.py @@ -26,6 +26,7 @@ "ssl": ["ssl"], "spf": ["spf"], "dkim": ["dkim"], + "dmarc": ["dmarc"], } logging.basicConfig(stream=sys.stdout, level=logging.INFO) From 4c24c04c2feacb6aafcf8d4b7fb6554e92c92ecf Mon Sep 17 00:00:00 2001 From: lcampbell Date: Tue, 18 Apr 2023 13:11:00 -0300 Subject: [PATCH 13/40] replace dmarcPhase summary with dmarcSummary on landing page --- frontend/src/landing/LandingPageSummaries.js | 45 +++++++++++++++++- frontend/src/summaries/SummaryCard.js | 49 ++------------------ 2 files changed, 48 insertions(+), 46 deletions(-) diff --git a/frontend/src/landing/LandingPageSummaries.js b/frontend/src/landing/LandingPageSummaries.js index bd7e712b2e..2a1c7a2e68 100644 --- a/frontend/src/landing/LandingPageSummaries.js +++ b/frontend/src/landing/LandingPageSummaries.js @@ -1,11 +1,16 @@ import React from 'react' import { useQuery } from '@apollo/client' -import { SummaryGroup } from '../summaries/SummaryGroup' import { HTTPS_AND_DMARC_SUMMARY } from '../graphql/queries' import { Box } from '@chakra-ui/react' import { LoadingMessage } from '../components/LoadingMessage' import { ErrorFallbackMessage } from '../components/ErrorFallbackMessage' +import { t } from '@lingui/macro' +import { Flex } from '@chakra-ui/react' + +import { SummaryCard } from '../summaries/SummaryCard' + +import theme from '../theme/canada' export function LandingPageSummaries() { const { loading, error, data } = useQuery(HTTPS_AND_DMARC_SUMMARY) @@ -13,9 +18,45 @@ export function LandingPageSummaries() { if (loading) return if (error) return + const { colors } = theme + return ( - + + + + ) } diff --git a/frontend/src/summaries/SummaryCard.js b/frontend/src/summaries/SummaryCard.js index 69c2b4af50..8d49906147 100644 --- a/frontend/src/summaries/SummaryCard.js +++ b/frontend/src/summaries/SummaryCard.js @@ -5,34 +5,10 @@ import { arrayOf, number, objectOf, shape, string } from 'prop-types' import { Doughnut, Segment } from './Doughnut' import { useLingui } from '@lingui/react' -export function SummaryCard({ - id, - title, - categoryDisplay, - description, - data, - ...props -}) { +export function SummaryCard({ id, title, categoryDisplay, description, data, ...props }) { const { i18n } = useLingui() - let dmarcCompliantCount = 0 - let dmarcCompliantPercentage = 0.0 + let { categories } = data - if (id === 'dmarcPhases') { - data.categories.forEach(({ name, count, percentage }) => { - if (name !== 'not implemented') { - dmarcCompliantCount += count - dmarcCompliantPercentage += percentage - } - }) - categories = [ - { - name: 'implemented', - count: dmarcCompliantCount, - percentage: dmarcCompliantPercentage, - }, - categories[0], - ] - } return ( - - + + {title} @@ -81,9 +44,7 @@ export function SummaryCard({ width={320} valueAccessor={(d) => d.count} > - {(segmentProps, index) => ( - - )} + {(segmentProps, index) => } From 5752ddd5cbfdf9685afecb2bd6206a4383c94cee Mon Sep 17 00:00:00 2001 From: lcampbell Date: Wed, 19 Apr 2023 09:17:33 -0300 Subject: [PATCH 14/40] refactor to use tiered summaries on landing page and org pages --- frontend/src/graphql/queries.js | 17 ++--- frontend/src/landing/LandingPage.js | 6 +- frontend/src/landing/LandingPageSummaries.js | 64 +++++-------------- .../OrganizationDetails.js | 49 ++++---------- .../src/organizations/OrganizationCard.js | 57 ++--------------- frontend/src/summaries/TierOneSummaries.js | 63 ++++++++++++++++++ frontend/src/summaries/TierThreeSummaries.js | 20 +++--- frontend/src/summaries/TierTwoSummaries.js | 48 ++++++++------ frontend/src/summaries/TieredSummaries.js | 22 +++++-- 9 files changed, 159 insertions(+), 187 deletions(-) create mode 100644 frontend/src/summaries/TierOneSummaries.js diff --git a/frontend/src/graphql/queries.js b/frontend/src/graphql/queries.js index 57f69725ff..c476b115ed 100644 --- a/frontend/src/graphql/queries.js +++ b/frontend/src/graphql/queries.js @@ -56,20 +56,16 @@ export const PAGINATED_ORGANIZATIONS = gql` } ` -export const HTTPS_AND_DMARC_SUMMARY = gql` +export const LANDING_PAGE_SUMMARIES = gql` query LandingPageSummaries { + # Tier 1 httpsSummary { ...RequiredSummaryFields } dmarcSummary { ...RequiredSummaryFields } - } - ${Summary.fragments.requiredFields} -` - -export const TIER_TWO_SUMMARY = gql` - query TierTwoSummary { + # Tier 2 webConnectionsSummary { ...RequiredSummaryFields } @@ -85,12 +81,7 @@ export const TIER_TWO_SUMMARY = gql` dmarcPhaseSummary { ...RequiredSummaryFields } - } - ${Summary.fragments.requiredFields} -` - -export const TIER_THREE_SUMMARY = gql` - query TierThreeSummary { + # Tier 3 webSummary { ...RequiredSummaryFields } diff --git a/frontend/src/landing/LandingPage.js b/frontend/src/landing/LandingPage.js index cb40e23fbb..e1d5177866 100644 --- a/frontend/src/landing/LandingPage.js +++ b/frontend/src/landing/LandingPage.js @@ -47,11 +47,7 @@ export function LandingPage({ loginRequired, isLoggedIn }) { - {(!loginRequired || isLoggedIn) && ( - - - - )} + {(!loginRequired || isLoggedIn) && } ) } diff --git a/frontend/src/landing/LandingPageSummaries.js b/frontend/src/landing/LandingPageSummaries.js index 2a1c7a2e68..eaaaa2270a 100644 --- a/frontend/src/landing/LandingPageSummaries.js +++ b/frontend/src/landing/LandingPageSummaries.js @@ -1,62 +1,28 @@ import React from 'react' import { useQuery } from '@apollo/client' -import { HTTPS_AND_DMARC_SUMMARY } from '../graphql/queries' -import { Box } from '@chakra-ui/react' +import { LANDING_PAGE_SUMMARIES } from '../graphql/queries' import { LoadingMessage } from '../components/LoadingMessage' import { ErrorFallbackMessage } from '../components/ErrorFallbackMessage' -import { t } from '@lingui/macro' -import { Flex } from '@chakra-ui/react' - -import { SummaryCard } from '../summaries/SummaryCard' - -import theme from '../theme/canada' +import { TieredSummaries } from '../summaries/TieredSummaries' export function LandingPageSummaries() { - const { loading, error, data } = useQuery(HTTPS_AND_DMARC_SUMMARY) + const { loading, error, data } = useQuery(LANDING_PAGE_SUMMARIES) if (loading) return if (error) return - const { colors } = theme + const summaries = { + https: data?.httpsSummary, + dmarc: data?.dmarcSummary, + webConnections: data?.webConnectionsSummary, + ssl: data?.sslSummary, + spf: data?.spfSummary, + dkim: data?.dkimSummary, + dmarcPhase: data?.dmarcPhaseSummary, + web: data?.webSummary, + mail: data?.mailSummary, + } - return ( - - - - - - - ) + return } diff --git a/frontend/src/organizationDetails/OrganizationDetails.js b/frontend/src/organizationDetails/OrganizationDetails.js index 72c67877b4..5d0cf1494b 100644 --- a/frontend/src/organizationDetails/OrganizationDetails.js +++ b/frontend/src/organizationDetails/OrganizationDetails.js @@ -1,33 +1,19 @@ import React, { useEffect } from 'react' import { useLazyQuery, useQuery } from '@apollo/client' import { Trans } from '@lingui/macro' -import { - Box, - Flex, - Heading, - IconButton, - Tab, - TabList, - TabPanel, - TabPanels, - Tabs, - Text, -} from '@chakra-ui/react' +import { Box, Flex, Heading, IconButton, Tab, TabList, TabPanel, TabPanels, Tabs, Text } from '@chakra-ui/react' import { ArrowLeftIcon, CheckCircleIcon } from '@chakra-ui/icons' import { Link as RouteLink, useParams, useHistory } from 'react-router-dom' import { ErrorBoundary } from 'react-error-boundary' import { OrganizationDomains } from './OrganizationDomains' import { OrganizationAffiliations } from './OrganizationAffiliations' -import { OrganizationSummary } from './OrganizationSummary' +import { TieredSummaries } from '../summaries/TieredSummaries' import { ErrorFallbackMessage } from '../components/ErrorFallbackMessage' import { LoadingMessage } from '../components/LoadingMessage' import { useDocumentTitle } from '../utilities/useDocumentTitle' -import { - GET_ORGANIZATION_DOMAINS_STATUSES_CSV, - ORG_DETAILS_PAGE, -} from '../graphql/queries' +import { GET_ORGANIZATION_DOMAINS_STATUSES_CSV, ORG_DETAILS_PAGE } from '../graphql/queries' import { RadialBarChart } from '../summaries/RadialBarChart' import { ExportButton } from '../components/ExportButton' @@ -44,12 +30,12 @@ export default function OrganizationDetails() { // errorPolicy: 'ignore', // allow partial success }) - const [ - getOrgDomainStatuses, - { loading: orgDomainStatusesLoading, _error, _data }, - ] = useLazyQuery(GET_ORGANIZATION_DOMAINS_STATUSES_CSV, { - variables: { orgSlug: orgSlug }, - }) + const [getOrgDomainStatuses, { loading: orgDomainStatusesLoading, _error, _data }] = useLazyQuery( + GET_ORGANIZATION_DOMAINS_STATUSES_CSV, + { + variables: { orgSlug: orgSlug }, + }, + ) useEffect(() => { if (!activeTab) { @@ -77,14 +63,11 @@ export default function OrganizationDetails() { } } + // const { httpsIncludeHidden, dmarcIncludeHidden, ...rest } = data?.organization?.summaries ?? {} + return ( - + } as={RouteLink} @@ -148,18 +131,14 @@ export default function OrganizationDetails() { - + DMARC Phases - + diff --git a/frontend/src/organizations/OrganizationCard.js b/frontend/src/organizations/OrganizationCard.js index db6a2738e6..7b81c30827 100644 --- a/frontend/src/organizations/OrganizationCard.js +++ b/frontend/src/organizations/OrganizationCard.js @@ -1,28 +1,11 @@ import React from 'react' -import { - Box, - Button, - Flex, - ListItem, - Progress, - Stack, - Text, - useBreakpointValue, -} from '@chakra-ui/react' +import { Box, Button, Flex, ListItem, Progress, Stack, Text, useBreakpointValue } from '@chakra-ui/react' import { CheckCircleIcon } from '@chakra-ui/icons' import { Link as RouteLink, useRouteMatch } from 'react-router-dom' import { bool, number, object, string } from 'prop-types' import { Trans } from '@lingui/macro' -export function OrganizationCard({ - name, - acronym, - slug, - domainCount, - verified, - summaries, - ...rest -}) { +export function OrganizationCard({ name, acronym, slug, domainCount, verified, summaries, ...rest }) { const { path, _url } = useRouteMatch() let httpsValue = 0 let dmarcValue = 0 @@ -75,24 +58,13 @@ export function OrganizationCard({ maxWidth="100%" > - + {name} ({acronym}) - {verified && ( - - )} + {verified && } - + HTTPS Configured @@ -121,11 +88,7 @@ export function OrganizationCard({ - + DMARC Configured @@ -133,13 +96,7 @@ export function OrganizationCard({ {hasButton && ( -