Skip to content

Commit

Permalink
Add Ability to Tag Out-of-scope Domains (#4613)
Browse files Browse the repository at this point in the history
* add OUTSIDE tag to domain tag enums

* add OUTSIDE tag to frontend

* translations

* filter exports to only include blocked domains

* undo translations for merge

* add outsideComment to create-domain

* add outsideComment to update-domain

* add outside comment select box to modal

* fix OUTSIDE tag detection

* add outsideComment to mutations

* add disclaimer to outside domains

* add outsideComment as reason in activity logs instead of claim value

* move reason outside of target object in log

* update tests

* fix blocked conditional in domain export
  • Loading branch information
lcampbell2 authored Jun 29, 2023
1 parent 3d69224 commit a8dd2f9
Show file tree
Hide file tree
Showing 18 changed files with 218 additions and 38 deletions.
29 changes: 27 additions & 2 deletions api/src/domain/mutations/create-domain.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { createDomainUnion } from '../unions'
import { Domain, Selectors } from '../../scalars'
import { logActivity } from '../../audit-logs/mutations/log-activity'
import { inputTag } from '../inputs/domain-tag'
import { OutsideDomainCommentEnum } from '../../enums'

export const createDomain = new mutationWithClientMutationId({
name: 'CreateDomain',
Expand Down Expand Up @@ -35,6 +36,10 @@ export const createDomain = new mutationWithClientMutationId({
description: 'Value that determines if the domain is excluded from the scanning process.',
type: GraphQLBoolean,
},
outsideComment: {
description: 'Comment describing reason for adding out-of-scope domain.',
type: OutsideDomainCommentEnum,
},
}),
outputFields: () => ({
result: {
Expand Down Expand Up @@ -97,6 +102,24 @@ export const createDomain = new mutationWithClientMutationId({
hidden = false
}

let outsideComment
if (typeof args.outsideComment !== 'undefined') {
outsideComment = cleanseInput(args.outsideComment)
} else {
outsideComment = ''
}

if (tags?.find(({ en }) => en === 'OUTSIDE')) {
if (outsideComment === '') {
console.warn(`User: ${userKey} attempted to create a domain with the OUTSIDE tag without providing a comment.`)
return {
_type: 'error',
code: 400,
description: i18n._(t`Please provide a comment when adding an outside domain.`),
}
}
}

// Check to see if org exists
const org = await loadOrgByKey.load(orgId)

Expand Down Expand Up @@ -223,7 +246,8 @@ export const createDomain = new mutationWithClientMutationId({
_from: ${org._id},
_to: ${insertedDomain._id},
tags: ${tags},
hidden: ${hidden}
hidden: ${hidden},
outsideComment: ${outsideComment}
} INTO claims
`,
)
Expand Down Expand Up @@ -270,7 +294,7 @@ export const createDomain = new mutationWithClientMutationId({
_from: ${org._id},
_to: ${checkDomain._id},
tags: ${tags},
hidden: ${hidden}
hidden: ${hidden},
} INTO claims
`,
)
Expand Down Expand Up @@ -337,6 +361,7 @@ export const createDomain = new mutationWithClientMutationId({
}, // name of resource being acted upon
resourceType: 'domain', // user, org, domain
},
reason: outsideComment !== '' ? outsideComment : null,
})

await publish({
Expand Down
24 changes: 24 additions & 0 deletions api/src/domain/mutations/update-domain.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { updateDomainUnion } from '../unions'
import { Domain, Selectors } from '../../scalars'
import { logActivity } from '../../audit-logs/mutations/log-activity'
import { inputTag } from '../inputs/domain-tag'
import { OutsideDomainCommentEnum } from '../../enums'

export const updateDomain = new mutationWithClientMutationId({
name: 'UpdateDomain',
Expand Down Expand Up @@ -39,6 +40,10 @@ export const updateDomain = new mutationWithClientMutationId({
description: 'Value that determines if the domain is excluded from the scanning process.',
type: GraphQLBoolean,
},
outsideComment: {
description: 'Comment describing reason for adding out-of-scope domain.',
type: OutsideDomainCommentEnum,
},
}),
outputFields: () => ({
result: {
Expand Down Expand Up @@ -99,6 +104,24 @@ export const updateDomain = new mutationWithClientMutationId({
hidden = null
}

let outsideComment
if (typeof args.outsideComment !== 'undefined') {
outsideComment = cleanseInput(args.outsideComment)
} else {
outsideComment = ''
}

if (tags?.find(({ en }) => en === 'OUTSIDE')) {
if (outsideComment === '') {
console.warn(`User: ${userKey} attempted to create a domain with the OUTSIDE tag without providing a comment.`)
return {
_type: 'error',
code: 400,
description: i18n._(t`Please provide a comment when adding an outside domain.`),
}
}
}

// Check to see if domain exists
const domain = await loadDomainByKey.load(domainId)

Expand Down Expand Up @@ -309,6 +332,7 @@ export const updateDomain = new mutationWithClientMutationId({
resourceType: 'domain', // user, org, domain
updatedProperties,
},
reason: outsideComment !== '' ? outsideComment : null,
})
}

Expand Down
8 changes: 8 additions & 0 deletions api/src/enums/domain-tag-label.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,14 @@ export const DomainTagLabel = new GraphQLEnumType({
value: 'scan-pending',
description: 'Label for tagging domains that have a pending web scan.',
},
OUTSIDE: {
value: 'OUTSIDE',
description: 'English label for tagging domains that are outside the scope of the project.',
},
EXTERIEUR: {
value: 'EXTÉRIEUR',
description: 'French label for tagging domains that are outside the scope of the project.',
},
},
description: 'An enum used to assign and test user-generated domain tags',
})
1 change: 1 addition & 0 deletions api/src/enums/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,4 @@ export * from './tfa-send-method'
export * from './verified-domain-order-field'
export * from './verified-organization-order-field'
export * from './web-order-field'
export * from './outside-domain-comment'
20 changes: 20 additions & 0 deletions api/src/enums/outside-domain-comment.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { GraphQLEnumType } from 'graphql'

export const OutsideDomainCommentEnum = new GraphQLEnumType({
name: 'OutsideDomainCommentEnum',
values: {
INVESTMENT: {
value: 'investment',
description: 'Organization is invested in the outside domain.',
},
OWNERSHIP: {
value: 'ownership',
description: 'Organization owns this domain, but it is outside the allowed scope.',
},
OTHER: {
value: 'other',
description: 'Other reason.',
},
},
description: 'Reason why an outside domain was added to the organization.',
})
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,18 @@ import { t } from '@lingui/macro'

export const loadAllOrganizationDomainStatuses =
({ query, userKey, i18n }) =>
async () => {
async ({ blocked }) => {
let statuses

try {
statuses = (
await query`
if (blocked) {
statuses = (
await query`
WITH domains, organizations
FOR org IN organizations
FILTER org.orgDetails.en.acronym != "SA"
FOR domain, claim IN 1..1 OUTBOUND org._id claims
FILTER domain.blocked == true
RETURN {
"Organization name (English)": org.orgDetails.en.name,
"Nom de l'organisation (Français)": org.orgDetails.fr.name,
Expand All @@ -26,7 +28,30 @@ export const loadAllOrganizationDomainStatuses =
"DMARC": domain.status.dmarc
}
`
).all()
).all()
} else {
statuses = (
await query`
WITH domains, organizations
FOR org IN organizations
FILTER org.orgDetails.en.acronym != "SA"
FOR domain, claim IN 1..1 OUTBOUND org._id claims
RETURN {
"Organization name (English)": org.orgDetails.en.name,
"Nom de l'organisation (Français)": org.orgDetails.fr.name,
"Domain": domain.domain,
"HTTPS": domain.status.https,
"HSTS": domain.status.hsts,
"Ciphers": domain.status.ciphers,
"Curves": domain.status.curves,
"Protocols": domain.status.protocols,
"SPF": domain.status.spf,
"DKIM": domain.status.dkim,
"DMARC": domain.status.dmarc
}
`
).all()
}
} catch (err) {
console.error(`Database error occurred when user: ${userKey} running loadAllOrganizationDomainStatuses: ${err}`)
throw new Error(i18n._(t`Unable to load all organization domain statuses. Please try again.`))
Expand Down
36 changes: 25 additions & 11 deletions api/src/organization/loaders/load-organization-domain-statuses.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,34 @@ import { t } from '@lingui/macro'

export const loadOrganizationDomainStatuses =
({ query, userKey, i18n }) =>
async ({ orgId }) => {
async ({ orgId, blocked }) => {
let domains

try {
domains = (
await query`
WITH claims, domains, organizations
FOR v, e IN 1..1 OUTBOUND ${orgId} claims
RETURN {
domain: v.domain,
status: v.status
}
`
).all()
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`
WITH claims, domains, organizations
FOR v, e IN 1..1 OUTBOUND ${orgId} claims
RETURN {
domain: v.domain,
status: v.status
}
`
).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
9 changes: 8 additions & 1 deletion api/src/organization/objects/organization.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,15 @@ export const organizationType = new GraphQLObjectType({
type: GraphQLString,
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.',
},
},
resolve: async (
{ _id },
_args,
args,
{
i18n,
userKey,
Expand All @@ -100,6 +106,7 @@ export const organizationType = new GraphQLObjectType({

const domains = await loadOrganizationDomainStatuses({
orgId: _id,
...args,
})
const headers = [
'domain',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import { GraphQLString } from 'graphql'
import { GraphQLBoolean, GraphQLString } from 'graphql'

import { t } from '@lingui/macro'

export const getAllOrganizationDomainStatuses = {
type: GraphQLString,
description: 'CSV formatted output of all domains in all organizations including their email and web scan statuses.',
args: {
blocked: {
type: GraphQLBoolean,
description: 'Whether to include blocked domains in the output.',
},
},
resolve: async (
_,
_args,
args,
{
userKey,
i18n,
Expand All @@ -27,7 +33,7 @@ export const getAllOrganizationDomainStatuses = {
throw new Error(i18n._(t`Permissions error. You do not have sufficient permissions to access this data.`))
}

const domainStatuses = await loadAllOrganizationDomainStatuses()
const domainStatuses = await loadAllOrganizationDomainStatuses({ ...args })

console.info(`User ${userKey} successfully retrieved all domain statuses.`)

Expand Down
2 changes: 2 additions & 0 deletions frontend/mocking/faked_schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -572,6 +572,8 @@ export const getTypeNames = () => gql`
# Value that determines if a domain has a web scan pending.
webScanPending: Boolean
userHasPermission: Boolean
# The organization that this domain belongs to.
organizations(
# Ordering options for organization connections
Expand Down
Loading

0 comments on commit a8dd2f9

Please sign in to comment.