diff --git a/api/src/organization/loaders/load-organization-domain-statuses.js b/api/src/organization/loaders/load-organization-domain-statuses.js index dda3bde6a..62d070ef4 100644 --- a/api/src/organization/loaders/load-organization-domain-statuses.js +++ b/api/src/organization/loaders/load-organization-domain-statuses.js @@ -1,35 +1,112 @@ import { t } from '@lingui/macro' +import { aql } from 'arangojs' export const loadOrganizationDomainStatuses = - ({ query, userKey, i18n }) => - async ({ orgId, blocked }) => { + ({ query, userKey, i18n, language }) => + async ({ orgId, filters }) => { let domains + let domainFilters = aql`FILTER v.archived != true` + if (typeof filters !== 'undefined') { + filters.forEach(({ filterCategory, comparison, filterValue }) => { + if (comparison === '==') { + comparison = aql`==` + } else { + comparison = aql`!=` + } + if (filterCategory === 'dmarc-status') { + domainFilters = aql` + ${domainFilters} + FILTER v.status.dmarc ${comparison} ${filterValue} + ` + } else if (filterCategory === 'dkim-status') { + domainFilters = aql` + ${domainFilters} + FILTER v.status.dkim ${comparison} ${filterValue} + ` + } else if (filterCategory === 'https-status') { + domainFilters = aql` + ${domainFilters} + FILTER v.status.https ${comparison} ${filterValue} + ` + } else if (filterCategory === 'spf-status') { + domainFilters = aql` + ${domainFilters} + FILTER v.status.spf ${comparison} ${filterValue} + ` + } else if (filterCategory === 'ciphers-status') { + domainFilters = aql` + ${domainFilters} + FILTER v.status.ciphers ${comparison} ${filterValue} + ` + } else if (filterCategory === 'curves-status') { + domainFilters = aql` + ${domainFilters} + FILTER v.status.curves ${comparison} ${filterValue} + ` + } else if (filterCategory === 'hsts-status') { + domainFilters = aql` + ${domainFilters} + FILTER v.status.hsts ${comparison} ${filterValue} + ` + } else if (filterCategory === 'protocols-status') { + domainFilters = aql` + ${domainFilters} + FILTER v.status.protocols ${comparison} ${filterValue} + ` + } else if (filterCategory === 'certificates-status') { + domainFilters = aql` + ${domainFilters} + FILTER v.status.certificates ${comparison} ${filterValue} + ` + } else if (filterCategory === 'tags') { + if (filterValue === 'hidden') { + domainFilters = aql` + ${domainFilters} + FILTER e.hidden ${comparison} true + ` + } else if (filterValue === 'nxdomain') { + domainFilters = aql` + ${domainFilters} + FILTER v.rcode ${comparison} "NXDOMAIN" + ` + } else if (filterValue === 'blocked') { + domainFilters = aql` + ${domainFilters} + FILTER v.blocked ${comparison} true + ` + } else { + domainFilters = aql` + ${domainFilters} + FILTER POSITION(claimTags, ${filterValue}) ${comparison} true + ` + } + } + }) + } try { - if (blocked) { - domains = ( - await query` - WITH claims, domains, organizations - FOR v, e IN 1..1 OUTBOUND ${orgId} claims - FILTER v.blocked == true - RETURN { - domain: v.domain, - status: v.status - } - ` - ).all() - } else { - domains = ( - await query` + domains = ( + await query` WITH claims, domains, organizations FOR v, e IN 1..1 OUTBOUND ${orgId} claims + LET claimTags = ( + LET translatedTags = ( + FOR tag IN e.tags || [] + RETURN TRANSLATE(${language}, tag) + ) + RETURN translatedTags + )[0] + ${domainFilters} RETURN { domain: v.domain, - status: v.status + status: v.status, + tags: claimTags, + hidden: e.hidden, + rcode: v.rcode, + blocked: v.blocked, } ` - ).all() - } + ).all() } catch (err) { console.error(`Database error occurred when user: ${userKey} running loadOrganizationDomainStatuses: ${err}`) throw new Error(i18n._(t`Unable to load organization domain statuses. Please try again.`)) diff --git a/api/src/organization/objects/organization.js b/api/src/organization/objects/organization.js index ab10fc726..162ca5b86 100644 --- a/api/src/organization/objects/organization.js +++ b/api/src/organization/objects/organization.js @@ -75,9 +75,9 @@ export const organizationType = new GraphQLObjectType({ description: 'CSV formatted output of all domains in the organization including their email and web scan statuses.', args: { - blocked: { - type: GraphQLBoolean, - description: 'Filters domains by blocked status.', + filters: { + type: new GraphQLList(domainFilter), + description: 'Filters used to limit domains returned.', }, }, resolve: async ( @@ -119,13 +119,18 @@ export const organizationType = new GraphQLObjectType({ 'spf', 'dkim', 'dmarc', + 'tags', + 'hidden', + 'rcode', + 'blocked', ] let csvOutput = headers.join(',') domains.forEach((domain) => { let csvLine = `${domain.domain}` - csvLine += headers.slice(1).reduce((previousValue, currentHeader) => { + csvLine += headers.slice(1, 10).reduce((previousValue, currentHeader) => { return `${previousValue},${domain.status[currentHeader]}` }, '') + csvLine += `,${domain.tags.join('|')},${domain.hidden},${domain.rcode},${domain.blocked}` csvOutput += `\n${csvLine}` }) diff --git a/frontend/src/graphql/queries.js b/frontend/src/graphql/queries.js index 3c5d781d6..7b4de63a6 100644 --- a/frontend/src/graphql/queries.js +++ b/frontend/src/graphql/queries.js @@ -95,9 +95,9 @@ export const LANDING_PAGE_SUMMARIES = gql` ` export const GET_ORGANIZATION_DOMAINS_STATUSES_CSV = gql` - query GetOrganizationDomainsStatusesCSV($orgSlug: Slug!) { + query GetOrganizationDomainsStatusesCSV($orgSlug: Slug!, $filters: [DomainFilter]) { findOrganizationBySlug(orgSlug: $orgSlug) { - toCsv + toCsv(filters: $filters) } } ` diff --git a/frontend/src/organizationDetails/OrganizationDetails.js b/frontend/src/organizationDetails/OrganizationDetails.js index 92d0a02c9..33e020066 100644 --- a/frontend/src/organizationDetails/OrganizationDetails.js +++ b/frontend/src/organizationDetails/OrganizationDetails.js @@ -1,5 +1,5 @@ import React, { useEffect } from 'react' -import { useLazyQuery, useQuery } from '@apollo/client' +import { useQuery } from '@apollo/client' import { Trans } from '@lingui/macro' import { Box, @@ -27,9 +27,8 @@ 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 { ORG_DETAILS_PAGE } from '../graphql/queries' import { RadialBarChart } from '../summaries/RadialBarChart' -import { ExportButton } from '../components/ExportButton' import { RequestOrgInviteModal } from '../organizations/RequestOrgInviteModal' import { useUserVar } from '../utilities/userState' @@ -48,13 +47,6 @@ export default function OrganizationDetails() { // errorPolicy: 'ignore', // allow partial success }) - const [getOrgDomainStatuses, { loading: orgDomainStatusesLoading, _error, _data }] = useLazyQuery( - GET_ORGANIZATION_DOMAINS_STATUSES_CSV, - { - variables: { orgSlug: orgSlug }, - }, - ) - useEffect(() => { if (!activeTab) { history.replace(`/organizations/${orgSlug}/${defaultActiveTab}`) @@ -160,20 +152,11 @@ export default function OrganizationDetails() { - {data?.organization?.userHasPermission && ( - { - const result = await getOrgDomainStatuses() - return result.data?.findOrganizationBySlug?.toCsv - }} - isLoading={orgDomainStatusesLoading} - /> - )} - + {!isNaN(data?.organization?.affiliations?.totalCount) && ( diff --git a/frontend/src/organizationDetails/OrganizationDomains.js b/frontend/src/organizationDetails/OrganizationDomains.js index 054788814..87e74ee05 100644 --- a/frontend/src/organizationDetails/OrganizationDomains.js +++ b/frontend/src/organizationDetails/OrganizationDomains.js @@ -13,7 +13,7 @@ import { useDisclosure, } from '@chakra-ui/react' import { ErrorBoundary } from 'react-error-boundary' -import { number, string } from 'prop-types' +import { bool, string } from 'prop-types' import { DomainCard } from '../domains/DomainCard' import { ListOf } from '../components/ListOf' @@ -23,13 +23,19 @@ import { RelayPaginationControls } from '../components/RelayPaginationControls' import { InfoBox, InfoPanel } from '../components/InfoPanel' import { usePaginatedCollection } from '../utilities/usePaginatedCollection' import { useDebouncedFunction } from '../utilities/useDebouncedFunction' -import { PAGINATED_ORG_DOMAINS as FORWARD, MY_TRACKER_DOMAINS } from '../graphql/queries' +import { + PAGINATED_ORG_DOMAINS as FORWARD, + GET_ORGANIZATION_DOMAINS_STATUSES_CSV, + MY_TRACKER_DOMAINS, +} from '../graphql/queries' import { SearchBox } from '../components/SearchBox' import { Formik } from 'formik' import { getRequirement, schemaToValidation } from '../utilities/fieldRequirements' import { CheckCircleIcon, InfoIcon, WarningIcon } from '@chakra-ui/icons' +import { useLazyQuery } from '@apollo/client' +import { ExportButton } from '../components/ExportButton' -export function OrganizationDomains({ orgSlug }) { +export function OrganizationDomains({ orgSlug, orgName, userHasPermission }) { const [orderDirection, setOrderDirection] = useState('ASC') const [orderField, setOrderField] = useState('DOMAIN') const [searchTerm, setSearchTerm] = useState('') @@ -72,6 +78,13 @@ export function OrganizationDomains({ orgSlug }) { nextFetchPolicy: 'cache-first', }) + const [getOrgDomainStatuses, { loading: orgDomainStatusesLoading, _error, _data }] = useLazyQuery( + GET_ORGANIZATION_DOMAINS_STATUSES_CSV, + { + variables: { orgSlug, filters }, + }, + ) + const { isOpen, onToggle } = useDisclosure() if (error) return @@ -274,6 +287,19 @@ export function OrganizationDomains({ orgSlug }) { return ( + {userHasPermission && ( + { + const result = await getOrgDomainStatuses() + return result.data?.findOrganizationBySlug?.toCsv + }} + isLoading={orgDomainStatusesLoading} + /> + )} {/* Web statuses */} @@ -401,4 +427,4 @@ export function OrganizationDomains({ orgSlug }) { ) } -OrganizationDomains.propTypes = { domainsPerPage: number, orgSlug: string } +OrganizationDomains.propTypes = { orgSlug: string, orgName: string, userHasPermission: bool }