Skip to content

Commit 6adbd3d

Browse files
authored
Merge pull request #44 from secure-dashboards/feat/add-githubOrgMFA
2 parents 00bbab9 + 0e52dbd commit 6adbd3d

File tree

12 files changed

+574
-5
lines changed

12 files changed

+574
-5
lines changed

.github/ISSUE_TEMPLATE/add-a-new-compliance-check.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,4 @@ _Provide a clear definition_
3636
- [ ] Run the command `check run --name {check_code_name}` and verify the changes in the database. Update the seed script if needed (`npm run db:seed`)
3737
- [ ] **5. Update the website**
3838
- [ ] Review the current content it in `https://openjs-security-program-standards.netlify.app/details/{check_code_name}`
39-
- [ ] Create a PR in https://github.com/secure-dashboards/openjs-security-program-standards to include how we calculate this check and include additional information on the mitigation if needed.
39+
- [ ] Create a PR in https://github.com/secure-dashboards/openjs-security-program-standards to include how we calculate this check and include additional information on the mitigation if needed.

__tests__/checks/githubOrgMFA.test.js

+105
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
const knexInit = require('knex')
2+
const { getConfig } = require('../../src/config')
3+
const githubOrgMFA = require('../../src/checks/complianceChecks/githubOrgMFA')
4+
const {
5+
resetDatabase, addProject, addGithubOrg, getAllResults, getAllTasks, getAllAlerts,
6+
addAlert, addTask, addResult, getCheckByCodeName
7+
} = require('../../__utils__')
8+
const { sampleGithubOrg } = require('../../__fixtures__')
9+
10+
const { dbSettings } = getConfig('test')
11+
12+
let knex
13+
let project
14+
let check
15+
16+
beforeAll(async () => {
17+
knex = knexInit(dbSettings)
18+
check = await getCheckByCodeName(knex, 'githubOrgMFA')
19+
})
20+
21+
beforeEach(async () => {
22+
await resetDatabase(knex)
23+
project = await addProject(knex, { name: sampleGithubOrg.login, category: 'impact' })
24+
})
25+
26+
afterAll(async () => {
27+
await knex.destroy()
28+
})
29+
30+
describe('Integration: githubOrgMFA', () => {
31+
test('Should add results without alerts or tasks', async () => {
32+
// Add a passed check scenario
33+
await addGithubOrg(knex, { login: sampleGithubOrg.login, html_url: sampleGithubOrg.html_url, project_id: project.id, two_factor_requirement_enabled: true })
34+
// Check that the database is empty
35+
let results = await getAllResults(knex)
36+
expect(results.length).toBe(0)
37+
let alerts = await getAllAlerts(knex)
38+
expect(alerts.length).toBe(0)
39+
let tasks = await getAllTasks(knex)
40+
expect(tasks.length).toBe(0)
41+
// Run the check
42+
await expect(githubOrgMFA(knex)).resolves.toBeUndefined()
43+
// Check that the database has the expected results
44+
results = await getAllResults(knex)
45+
expect(results.length).toBe(1)
46+
expect(results[0].status).toBe('passed')
47+
expect(results[0].compliance_check_id).toBe(check.id)
48+
alerts = await getAllAlerts(knex)
49+
expect(alerts.length).toBe(0)
50+
tasks = await getAllTasks(knex)
51+
expect(tasks.length).toBe(0)
52+
})
53+
54+
test('Should delete (previous alerts and tasks) and add results', async () => {
55+
// Prepare the Scenario
56+
await addGithubOrg(knex, { login: sampleGithubOrg.login, html_url: sampleGithubOrg.html_url, project_id: project.id, two_factor_requirement_enabled: true })
57+
await addAlert(knex, { compliance_check_id: check.id, project_id: project.id, title: 'existing', description: 'existing', severity: 'critical' })
58+
await addTask(knex, { compliance_check_id: check.id, project_id: project.id, title: 'existing', description: 'existing', severity: 'critical' })
59+
// Check that the database has the expected results
60+
let results = await getAllResults(knex)
61+
expect(results.length).toBe(0)
62+
let alerts = await getAllAlerts(knex)
63+
expect(alerts.length).toBe(1)
64+
expect(alerts[0].compliance_check_id).toBe(check.id)
65+
let tasks = await getAllTasks(knex)
66+
expect(tasks.length).toBe(1)
67+
expect(tasks[0].compliance_check_id).toBe(check.id)
68+
// Run the check
69+
await githubOrgMFA(knex)
70+
// Check that the database has the expected results
71+
results = await getAllResults(knex)
72+
expect(results.length).toBe(1)
73+
expect(results[0].status).toBe('passed')
74+
alerts = await getAllAlerts(knex)
75+
expect(alerts.length).toBe(0)
76+
tasks = await getAllTasks(knex)
77+
expect(tasks.length).toBe(0)
78+
})
79+
test('Should add (alerts and tasks) and update results', async () => {
80+
// Prepare the Scenario
81+
await addGithubOrg(knex, { login: sampleGithubOrg.login, html_url: sampleGithubOrg.html_url, project_id: project.id, two_factor_requirement_enabled: false })
82+
await addResult(knex, { compliance_check_id: check.id, project_id: project.id, status: 'passed', rationale: 'failed previously', severity: 'critical' })
83+
// Check that the database has the expected results
84+
let results = await getAllResults(knex)
85+
expect(results.length).toBe(1)
86+
expect(results[0].compliance_check_id).toBe(check.id)
87+
let alerts = await getAllAlerts(knex)
88+
expect(alerts.length).toBe(0)
89+
let tasks = await getAllTasks(knex)
90+
expect(tasks.length).toBe(0)
91+
// Run the check
92+
await githubOrgMFA(knex)
93+
// Check that the database has the expected results
94+
results = await getAllResults(knex)
95+
expect(results.length).toBe(1)
96+
expect(results[0].status).toBe('failed')
97+
expect(results[0].rationale).not.toBe('failed previously')
98+
alerts = await getAllAlerts(knex)
99+
expect(alerts.length).toBe(1)
100+
expect(alerts[0].compliance_check_id).toBe(check.id)
101+
tasks = await getAllTasks(knex)
102+
expect(tasks.length).toBe(1)
103+
expect(tasks[0].compliance_check_id).toBe(check.id)
104+
})
105+
})

__tests__/checks/validators.test.js

+148
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+
})

__tests__/utils.test.js

+66-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
const { validateGithubUrl, ensureGithubToken } = require('../src/utils/index')
1+
const { validateGithubUrl, ensureGithubToken, groupArrayItemsByCriteria, isCheckApplicableToProjectCategory, getSeverityFromPriorityGroup } = require('../src/utils/index')
22

33
describe('ensureGithubToken', () => {
44
let originalGithubToken
@@ -43,3 +43,68 @@ describe('validateGithubUrl', () => {
4343
expect(validateGithubUrl(url)).toBe(false)
4444
})
4545
})
46+
47+
describe('groupArrayItemsByCriteria', () => {
48+
const groupByProject = groupArrayItemsByCriteria('project_id')
49+
50+
it('should group array items by criteria', () => {
51+
const items = [
52+
{ project_id: 1, name: 'item1' },
53+
{ project_id: 1, name: 'item2' },
54+
{ project_id: 2, name: 'item3' }
55+
]
56+
const expected = [
57+
[
58+
{ project_id: 1, name: 'item1' },
59+
{ project_id: 1, name: 'item2' }
60+
],
61+
[
62+
{ project_id: 2, name: 'item3' }
63+
]
64+
]
65+
expect(groupByProject(items)).toEqual(expected)
66+
})
67+
})
68+
69+
describe('isCheckApplicableToProjectCategory', () => {
70+
const disabledCheck = {
71+
level_active_status: 'n/a',
72+
level_incubating_status: 'n/a',
73+
level_retiring_status: 'n/a'
74+
}
75+
76+
it('should return false if the check is not applicable to the project category', () => {
77+
let project = { category: 'impact' }
78+
expect(isCheckApplicableToProjectCategory(disabledCheck, project)).toBe(false)
79+
project = { category: 'incubation' }
80+
expect(isCheckApplicableToProjectCategory(disabledCheck, project)).toBe(false)
81+
project = { category: 'emeritus' }
82+
expect(isCheckApplicableToProjectCategory(disabledCheck, project)).toBe(false)
83+
project = { category: 'at-large' }
84+
expect(isCheckApplicableToProjectCategory(disabledCheck, project)).toBe(false)
85+
})
86+
87+
it('should return true if the check is applicable to the project category', () => {
88+
const project = { category: 'impact' }
89+
const check = { ...disabledCheck, level_active_status: 'recommended' }
90+
expect(isCheckApplicableToProjectCategory(check, project)).toBe(true)
91+
})
92+
})
93+
94+
describe('getSeverityFromPriorityGroup', () => {
95+
it('should return the correct severity based on the priority group', () => {
96+
expect(getSeverityFromPriorityGroup('P0')).toBe('critical')
97+
expect(getSeverityFromPriorityGroup('P1')).toBe('critical')
98+
expect(getSeverityFromPriorityGroup('P2')).toBe('critical')
99+
expect(getSeverityFromPriorityGroup('P3')).toBe('high')
100+
expect(getSeverityFromPriorityGroup('P4')).toBe('high')
101+
expect(getSeverityFromPriorityGroup('P5')).toBe('medium')
102+
expect(getSeverityFromPriorityGroup('P6')).toBe('medium')
103+
expect(getSeverityFromPriorityGroup('P7')).toBe('medium')
104+
expect(getSeverityFromPriorityGroup('P8')).toBe('low')
105+
expect(getSeverityFromPriorityGroup('P20')).toBe('low')
106+
// Recommendations always have 'info' severity
107+
expect(getSeverityFromPriorityGroup('R1')).toBe('info')
108+
expect(getSeverityFromPriorityGroup('R11')).toBe('info')
109+
})
110+
})

__utils__/index.js

+33-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
const resetDatabase = async (knex) => {
2+
await knex.raw('TRUNCATE TABLE compliance_checks_results RESTART IDENTITY CASCADE')
3+
await knex.raw('TRUNCATE TABLE compliance_checks_tasks RESTART IDENTITY CASCADE')
4+
await knex.raw('TRUNCATE TABLE compliance_checks_alerts RESTART IDENTITY CASCADE')
25
await knex.raw('TRUNCATE TABLE github_repositories RESTART IDENTITY CASCADE')
36
await knex.raw('TRUNCATE TABLE github_organizations RESTART IDENTITY CASCADE')
47
await knex.raw('TRUNCATE TABLE projects RESTART IDENTITY CASCADE')
@@ -23,7 +26,29 @@ const addGithubRepo = async (knex, data) => {
2326
return githubRepo
2427
}
2528

29+
const getAllResults = (knex) => knex('compliance_checks_results').select('*')
30+
const getAllTasks = (knex) => knex('compliance_checks_tasks').select('*')
31+
const getAllAlerts = (knex) => knex('compliance_checks_alerts').select('*')
32+
const addAlert = async (knex, alert) => {
33+
const [newAlert] = await knex('compliance_checks_alerts').insert(alert).returning('*')
34+
return newAlert
35+
}
36+
37+
const addTask = async (knex, task) => {
38+
const [newTask] = await knex('compliance_checks_tasks').insert(task).returning('*')
39+
return newTask
40+
}
41+
42+
const addResult = async (knex, result) => {
43+
const [newResult] = await knex('compliance_checks_results').insert(result).returning('*')
44+
return newResult
45+
}
46+
2647
const getAllComplianceChecks = (knex) => knex('compliance_checks').select('*')
48+
const getCheckByCodeName = async (knex, codeName) => {
49+
const check = await knex('compliance_checks').where({ code_name: codeName }).first()
50+
return check
51+
}
2752

2853
module.exports = {
2954
getAllComplianceChecks,
@@ -33,5 +58,12 @@ module.exports = {
3358
addProject,
3459
addGithubOrg,
3560
getAllGithubRepos,
36-
addGithubRepo
61+
addGithubRepo,
62+
getAllResults,
63+
getAllTasks,
64+
getAllAlerts,
65+
addAlert,
66+
addTask,
67+
addResult,
68+
getCheckByCodeName
3769
}

src/checks/complianceChecks/.placeholder

Whitespace-only changes.

0 commit comments

Comments
 (0)