Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature export filtered org domains #4707

Merged
merged 8 commits into from
Jul 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 97 additions & 20 deletions api/src/organization/loaders/load-organization-domain-statuses.js
Original file line number Diff line number Diff line change
@@ -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.`))
Expand Down
13 changes: 9 additions & 4 deletions api/src/organization/objects/organization.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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}`
})

Expand Down
4 changes: 2 additions & 2 deletions frontend/src/graphql/queries.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
`
Expand Down
31 changes: 7 additions & 24 deletions frontend/src/organizationDetails/OrganizationDetails.js
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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'

Expand All @@ -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}`)
Expand Down Expand Up @@ -160,20 +152,11 @@ export default function OrganizationDetails() {
</TabPanel>
<TabPanel>
<ErrorBoundary FallbackComponent={ErrorFallbackMessage}>
{data?.organization?.userHasPermission && (
<ExportButton
ml="auto"
my="2"
mt={{ base: '4', md: 0 }}
fileName={`${orgName}_${new Date().toLocaleDateString()}_Tracker`}
dataFunction={async () => {
const result = await getOrgDomainStatuses()
return result.data?.findOrganizationBySlug?.toCsv
}}
isLoading={orgDomainStatusesLoading}
/>
)}
<OrganizationDomains orgSlug={orgSlug} />
<OrganizationDomains
orgSlug={orgSlug}
orgName={orgName}
userHasPermission={data?.organization?.userHasPermission}
/>
</ErrorBoundary>
</TabPanel>
{!isNaN(data?.organization?.affiliations?.totalCount) && (
Expand Down
34 changes: 30 additions & 4 deletions frontend/src/organizationDetails/OrganizationDomains.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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('')
Expand Down Expand Up @@ -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 <ErrorFallbackMessage error={error} />
Expand Down Expand Up @@ -274,6 +287,19 @@ export function OrganizationDomains({ orgSlug }) {

return (
<Box>
{userHasPermission && (
<ExportButton
ml="auto"
my="2"
mt={{ base: '4', md: 0 }}
fileName={`${orgName}_${new Date().toLocaleDateString()}_Tracker`}
dataFunction={async () => {
const result = await getOrgDomainStatuses()
return result.data?.findOrganizationBySlug?.toCsv
}}
isLoading={orgDomainStatusesLoading}
/>
)}
<InfoPanel isOpen={isOpen} onToggle={onToggle}>
<InfoBox title={t`Domain`} info={t`The domain address.`} />
{/* Web statuses */}
Expand Down Expand Up @@ -401,4 +427,4 @@ export function OrganizationDomains({ orgSlug }) {
)
}

OrganizationDomains.propTypes = { domainsPerPage: number, orgSlug: string }
OrganizationDomains.propTypes = { orgSlug: string, orgName: string, userHasPermission: bool }