Skip to content

Commit 44c41d1

Browse files
committed
feat: add check validator for githubOrgMFA
Related: #43
1 parent b93f20b commit 44c41d1

File tree

2 files changed

+228
-0
lines changed

2 files changed

+228
-0
lines changed

__tests__/checks/validators.test.js

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
const { githubOrgMFA } = require('../../src/checks/validators')
2+
// @see: https://github.com/secure-dashboards/openjs-foundation-dashboard/issues/43
3+
describe('githubOrgMFA', () => {
4+
let organizations, check, projects
5+
beforeEach(() => {
6+
organizations = [
7+
{
8+
project_id: 1,
9+
login: 'org1',
10+
two_factor_requirement_enabled: true
11+
},
12+
{
13+
project_id: 1,
14+
login: 'org2',
15+
two_factor_requirement_enabled: true
16+
},
17+
{
18+
project_id: 2,
19+
login: 'org3',
20+
two_factor_requirement_enabled: true
21+
}
22+
]
23+
24+
check = {
25+
id: 1,
26+
priority_group: 'P1',
27+
details_url: 'https://example.com',
28+
level_incubating_status: 'expected',
29+
level_active_status: 'expected',
30+
level_retiring_status: 'expected'
31+
}
32+
33+
projects = [
34+
{
35+
id: 1,
36+
category: 'impact'
37+
},
38+
{
39+
id: 2,
40+
category: 'at-large'
41+
}
42+
]
43+
})
44+
it('Should generate a passed result if all organizations have 2FA enabled', () => {
45+
const analysis = githubOrgMFA({ organizations, check, projects })
46+
expect(analysis).toEqual({
47+
alerts: [],
48+
results: [
49+
{
50+
project_id: 1,
51+
compliance_check_id: 1,
52+
severity: 'critical',
53+
status: 'passed',
54+
rationale: 'The organization(s) have 2FA enabled'
55+
},
56+
{
57+
compliance_check_id: 1,
58+
project_id: 2,
59+
rationale: 'The organization(s) have 2FA enabled',
60+
severity: 'critical',
61+
status: 'passed'
62+
}
63+
],
64+
tasks: []
65+
})
66+
})
67+
68+
it('should generate a failed result if some organizations do not have 2FA enabled', () => {
69+
organizations[0].two_factor_requirement_enabled = false
70+
// IMPORTANT: If one organization fails, the whole project fails no matter how other organizations are in the project
71+
organizations[1].two_factor_requirement_enabled = null
72+
73+
const analysis = githubOrgMFA({ organizations, check, projects })
74+
expect(analysis).toEqual({
75+
alerts: [
76+
{
77+
project_id: 1,
78+
compliance_check_id: 1,
79+
severity: 'critical',
80+
title: 'The organization(s) (org1) do not have 2FA enabled',
81+
description: 'Check the details on https://example.com'
82+
}
83+
],
84+
results: [
85+
{
86+
project_id: 1,
87+
compliance_check_id: 1,
88+
severity: 'critical',
89+
status: 'failed',
90+
rationale: 'The organization(s) (org1) do not have 2FA enabled'
91+
},
92+
{
93+
project_id: 2,
94+
compliance_check_id: 1,
95+
severity: 'critical',
96+
status: 'passed',
97+
rationale: 'The organization(s) have 2FA enabled'
98+
}
99+
],
100+
tasks: [
101+
{
102+
project_id: 1,
103+
compliance_check_id: 1,
104+
severity: 'critical',
105+
title: 'Enable 2FA for the organization(s) (org1)',
106+
description: 'Check the details on https://example.com'
107+
}
108+
]
109+
})
110+
})
111+
112+
it('should generate an unknown result if some organizations have unknown 2FA status', () => {
113+
organizations[1].two_factor_requirement_enabled = null
114+
const analysis = githubOrgMFA({ organizations, check, projects })
115+
expect(analysis).toEqual({
116+
alerts: [],
117+
results: [
118+
{
119+
project_id: 1,
120+
compliance_check_id: 1,
121+
severity: 'critical',
122+
status: 'unknown',
123+
rationale: 'The organization(s) (org2) have 2FA status unknown'
124+
},
125+
{
126+
project_id: 2,
127+
compliance_check_id: 1,
128+
severity: 'critical',
129+
status: 'passed',
130+
rationale: 'The organization(s) have 2FA enabled'
131+
}
132+
],
133+
tasks: []
134+
})
135+
})
136+
137+
it('Should skip the check if it is not in scope for the project category', () => {
138+
check.level_active_status = 'n/a'
139+
check.level_incubating_status = 'n/a'
140+
check.level_retiring_status = 'n/a'
141+
const analysis = githubOrgMFA({ organizations, check, projects })
142+
expect(analysis).toEqual({
143+
alerts: [],
144+
results: [],
145+
tasks: []
146+
})
147+
})
148+
})

src/checks/validators/index.js

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
const debug = require('debug')('checks:validator:githubOrgMFA')
2+
const { getSeverityFromPriorityGroup, isCheckApplicableToProjectCategory, groupArrayItemsByCriteria } = require('../../utils')
3+
4+
const groupByProject = groupArrayItemsByCriteria('project_id')
5+
6+
// @see: https://github.com/secure-dashboards/openjs-foundation-dashboard/issues/43
7+
const githubOrgMFA = ({ organizations, check, projects }) => {
8+
debug('Validating GitHub organizations MFA...')
9+
debug('Grouping organizations by project...')
10+
const organizationsGroupedByProject = groupByProject(organizations)
11+
12+
const alerts = []
13+
const results = []
14+
const tasks = []
15+
16+
debug('Processing organizations...')
17+
organizationsGroupedByProject.forEach((projectOrgs) => {
18+
debug(`Processing project (${projectOrgs[0].project_id})`)
19+
const project = projects.find(p => p.id === projectOrgs[0].project_id)
20+
const isInScope = isCheckApplicableToProjectCategory(check, project)
21+
// If the check is not in scope, skip it.
22+
if (!isInScope) {
23+
debug(`This check is not in scope for project (${project.id})`)
24+
return
25+
}
26+
27+
const baseData = {
28+
project_id: projectOrgs[0].project_id,
29+
compliance_check_id: check.id,
30+
severity: getSeverityFromPriorityGroup(check.priority_group)
31+
}
32+
33+
const result = { ...baseData }
34+
const task = { ...baseData }
35+
const alert = { ...baseData }
36+
37+
const failedOrgs = projectOrgs.filter(org => org.two_factor_requirement_enabled === false).map(org => org.login)
38+
const unknownOrgs = projectOrgs.filter(org => org.two_factor_requirement_enabled === null).map(org => org.login)
39+
40+
if (projectOrgs.every(org => org.two_factor_requirement_enabled === true)) {
41+
result.status = 'passed'
42+
result.rationale = 'The organization(s) have 2FA enabled'
43+
} else if (failedOrgs.length) {
44+
result.status = 'failed'
45+
result.rationale = `The organization(s) (${failedOrgs.join(',')}) do not have 2FA enabled`
46+
alert.title = `The organization(s) (${failedOrgs.join(',')}) do not have 2FA enabled`
47+
alert.description = `Check the details on ${check.details_url}`
48+
task.title = `Enable 2FA for the organization(s) (${failedOrgs.join(',')})`
49+
task.description = `Check the details on ${check.details_url}`
50+
} else if (unknownOrgs.length) {
51+
result.status = 'unknown'
52+
result.rationale = `The organization(s) (${unknownOrgs.join(',')}) have 2FA status unknown`
53+
}
54+
// Include only the task if was populated
55+
if (Object.keys(task).length > Object.keys(baseData).length) {
56+
debug(`Adding task for project (${project.id})`)
57+
tasks.push(task)
58+
}
59+
// Include only the alert if was populated
60+
if (Object.keys(alert).length > Object.keys(baseData).length) {
61+
debug(`Adding alert for project (${project.id})`)
62+
alerts.push(alert)
63+
}
64+
// Always include the result
65+
results.push(result)
66+
debug(`Processed project (${project.id})`)
67+
})
68+
69+
return {
70+
alerts,
71+
results,
72+
tasks
73+
}
74+
}
75+
76+
const validators = {
77+
githubOrgMFA
78+
}
79+
80+
module.exports = validators

0 commit comments

Comments
 (0)