Skip to content

Commit 0681e58

Browse files
authored
Merge pull request #167 from OpenPathfinder/feat/add-checklists
2 parents 2b8b0fc + ac74b36 commit 0681e58

15 files changed

+890
-103
lines changed

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

-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ You can find more details in [the contributing guide](/CONTRIBUTING.md#current-i
2626
- [ ] **3. Implement the Business Logic [Validator Example](https://github.com/OpenPathfinder/visionBoard/commit/44c41d119f0daefb7b2e496ba35d5ab65bcc319b) and [Check Example](https://github.com/OpenPathfinder/visionBoard/commit/6f1e16129ee0d01a1b9b536cd2dc6090b048b71f)**
2727
- [ ] Add the specific validator in `src/checks/validators/index.js`
2828
- [ ] Add the check logic in `src/checks/complianceChecks`
29-
- [ ] Ensure that the check is in scope for the organization (use `isCheckApplicableToProjectCategory`)
3029
- [ ] Ensure that the `severity` value is well calculated (use `getSeverityFromPriorityGroup`)
3130
- [ ] Add the alert row in the `compliance_checks_alerts` table when is needed.
3231
- [ ] Add the task row in the `compliance_checks_tasks` table when is needed.

.github/workflows/review-compliance-checks.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ jobs:
4545
"- [ ] Have you updated the compliance check in the `compliance_checks` table?\n" +
4646
"- [ ] Have you included a specific validator (`src/checks/validators/`) for this check with unit tests (`__tests__/checks/`)?\n" +
4747
"- [ ] Have you included a specific file in `src/checks/complianceChecks` with the integration tests (`__tests__/checks/`)?\n" +
48-
"- [ ] Have you included severity validation (`getSeverityFromPriorityGroup`) and checked applicability (`isCheckApplicableToProjectCategory`)?\n" +
48+
"- [ ] Have you included severity validation (`getSeverityFromPriorityGroup`)?\n" +
4949
"- [ ] Have you included the tasks, alerts, and results in the database tables?\n" +
5050
"- [ ] Have you tested the check with `check run --name {check_code_name}` using the seeded database (`npm run db:seed`)?\n" +
5151
"- [ ] Have you created a PR in [the website](https://github.com/OpenPathfinder/website) with the calculation details?\n" +

CONTRIBUTING.md

-1
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,6 @@ We are looking for contributors to implement compliance checks in the Dashboard.
143143
- **3. Implement the Business Logic ([Validator Example](https://github.com/OpenPathfinder/visionBoard/commit/44c41d119f0daefb7b2e496ba35d5ab65bcc319b) and [Check Example](https://github.com/OpenPathfinder/visionBoard/commit/6f1e16129ee0d01a1b9b536cd2dc6090b048b71f)):**
144144
- Add the specific validator in `src/checks/validators/index.js`.
145145
- Add the check logic in `src/checks/complianceChecks`.
146-
- Ensure the check is applicable to the organization (`isCheckApplicableToProjectCategory`).
147146
- Calculate `severity` accurately (`getSeverityFromPriorityGroup`).
148147
- Update relevant database tables (`compliance_checks_alerts`, `compliance_checks_tasks`, `compliance_checks_results`).
149148

__tests__/checks/validators.test.js

+4-33
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,8 @@ describe('githubOrgMFA', () => {
2323

2424
check = {
2525
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'
26+
default_priority_group: 'P1',
27+
details_url: 'https://example.com'
3128
}
3229

3330
projects = [
@@ -133,18 +130,6 @@ describe('githubOrgMFA', () => {
133130
tasks: []
134131
})
135132
})
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-
})
148133
})
149134

150135
describe('softwareDesignTraining', () => {
@@ -163,11 +148,8 @@ describe('softwareDesignTraining', () => {
163148

164149
check = {
165150
id: 1,
166-
priority_group: 'P1',
167-
details_url: 'https://example.com',
168-
level_incubating_status: 'expected',
169-
level_active_status: 'expected',
170-
level_retiring_status: 'expected'
151+
default_priority_group: 'P1',
152+
details_url: 'https://example.com'
171153
}
172154

173155
projects = [
@@ -299,15 +281,4 @@ describe('softwareDesignTraining', () => {
299281
]
300282
})
301283
})
302-
it('Should skip the check if it is not in scope for the project category', () => {
303-
check.level_active_status = 'n/a'
304-
check.level_incubating_status = 'n/a'
305-
check.level_retiring_status = 'n/a'
306-
const analysis = softwareDesignTraining({ trainings, check, projects })
307-
expect(analysis).toEqual({
308-
alerts: [],
309-
results: [],
310-
tasks: []
311-
})
312-
})
313284
})

__tests__/utils.test.js

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

33
describe('ensureGithubToken', () => {
44
let originalGithubToken
@@ -66,31 +66,6 @@ describe('groupArrayItemsByCriteria', () => {
6666
})
6767
})
6868

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-
9469
describe('getSeverityFromPriorityGroup', () => {
9570
it('should return the correct severity based on the priority group', () => {
9671
expect(getSeverityFromPriorityGroup('P0')).toBe('critical')

src/checks/validators/githubOrgMFA.js

+2-8
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
const debug = require('debug')('checks:validator:githubOrgMFA')
2-
const { getSeverityFromPriorityGroup, isCheckApplicableToProjectCategory, groupArrayItemsByCriteria } = require('../../utils')
2+
const { getSeverityFromPriorityGroup, groupArrayItemsByCriteria } = require('../../utils')
33

44
const groupByProject = groupArrayItemsByCriteria('project_id')
55

@@ -17,17 +17,11 @@ module.exports = ({ organizations = [], check, projects = [] }) => {
1717
organizationsGroupedByProject.forEach((projectOrgs) => {
1818
debug(`Processing project (${projectOrgs[0].project_id})`)
1919
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-
}
2620

2721
const baseData = {
2822
project_id: projectOrgs[0].project_id,
2923
compliance_check_id: check.id,
30-
severity: getSeverityFromPriorityGroup(check.priority_group)
24+
severity: getSeverityFromPriorityGroup(check.default_priority_group)
3125
}
3226

3327
const result = { ...baseData }

src/checks/validators/softwareDesignTraining.js

+2-9
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
const debug = require('debug')('checks:validator:softwareDesignTraining')
2-
const { isCheckApplicableToProjectCategory, getSeverityFromPriorityGroup, isDateWithinPolicy } = require('../../utils')
2+
const { getSeverityFromPriorityGroup, isDateWithinPolicy } = require('../../utils')
33

44
const expirationPolicy = '6m'
55

@@ -11,17 +11,10 @@ module.exports = ({ trainings = [], check, projects = [] }) => {
1111

1212
debug('Processing Projects...')
1313
projects.forEach(project => {
14-
const isInScope = isCheckApplicableToProjectCategory(check, project)
15-
// If the check is not in scope, skip it.
16-
if (!isInScope) {
17-
debug(`This check is not in scope for project (${project.id})`)
18-
return
19-
}
20-
2114
const baseData = {
2215
project_id: project.id,
2316
compliance_check_id: check.id,
24-
severity: getSeverityFromPriorityGroup(check.priority_group)
17+
severity: getSeverityFromPriorityGroup(check.default_priority_group)
2518
}
2619

2720
const result = { ...baseData }

src/database/migrations/1733494222119_add_compliance_checks.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
const statusLevels = ['n/a', 'deferrable', 'expected', 'recommended']
2+
const priorityGroupOptions = [
3+
'P0', 'P1', 'P2', 'P3', 'P4', 'P5', 'P6', 'P7', 'P8', 'P9', 'P10', 'P11', 'P12', 'P13', 'P14',
4+
'R0', 'R1', 'R2', 'R3', 'R4', 'R5', 'R6', 'R7', 'R8', 'R9', 'R10', 'R11', 'R12', 'R13', 'R14'
5+
]
26

37
exports.up = async (knex) => {
48
await knex.schema.createTable('compliance_checks', (table) => {
@@ -8,7 +12,7 @@ exports.up = async (knex) => {
812
table.string('section_number').notNullable()
913
table.string('section_name').notNullable()
1014
table.string('code_name').unique().notNullable()
11-
table.string('priority_group').notNullable()
15+
table.enum('priority_group', priorityGroupOptions).notNullable()
1216
table.boolean('is_c_scrm').notNullable().defaultTo(false)
1317
table.enum('level_incubating_status', statusLevels).notNullable()
1418
table.enum('level_active_status', statusLevels).notNullable()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
const statusLevels = ['n/a', 'deferrable', 'expected', 'recommended']
2+
3+
exports.up = async (knex) => {
4+
await knex.schema.alterTable('compliance_checks', (table) => {
5+
// Drop old fields
6+
table.dropColumn('level_incubating_status')
7+
table.dropColumn('level_active_status')
8+
table.dropColumn('level_retiring_status')
9+
10+
// Rename fields
11+
table.renameColumn('priority_group', 'default_priority_group')
12+
table.renameColumn('section_number', 'default_section_number')
13+
table.renameColumn('section_name', 'default_section_name')
14+
})
15+
}
16+
17+
exports.down = async (knex) => {
18+
await knex.schema.alterTable('compliance_checks', (table) => {
19+
// IMPORTANT: Re-add dropped fields but without the original values and nullable.
20+
table.enum('level_incubating_status', statusLevels).nullable()
21+
table.enum('level_active_status', statusLevels).nullable()
22+
table.enum('level_retiring_status', statusLevels).nullable()
23+
24+
// Rename fields back
25+
table.renameColumn('default_priority_group', 'priority_group')
26+
table.renameColumn('default_section_number', 'section_number')
27+
table.renameColumn('default_section_name', 'section_name')
28+
})
29+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
exports.up = async (knex) => {
2+
await knex.schema.createTable('compliance_checklists', (table) => {
3+
table.increments('id').primary() // Primary key
4+
table.text('author').notNullable()
5+
table.string('title').notNullable()
6+
table.text('description').notNullable()
7+
table.string('code_name').notNullable()
8+
table.text('url').notNullable()
9+
10+
// Timestamps
11+
table.timestamp('created_at').defaultTo(knex.fn.now()).notNullable()
12+
table.timestamp('updated_at').defaultTo(knex.fn.now()).notNullable()
13+
})
14+
15+
// Add trigger to automatically update the 'updated_at' column
16+
await knex.raw(`
17+
CREATE TRIGGER set_updated_at_compliance_checklists
18+
BEFORE UPDATE ON compliance_checklists
19+
FOR EACH ROW
20+
EXECUTE FUNCTION update_updated_at_column();
21+
`)
22+
}
23+
24+
exports.down = async (knex) => {
25+
// Drop trigger
26+
await knex.raw('DROP TRIGGER IF EXISTS set_updated_at_compliance_checklists ON compliance_checklists;')
27+
// Drop table
28+
await knex.schema.dropTableIfExists('compliance_checklists')
29+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
const list = [
2+
{
3+
id: 1,
4+
author: 'OpenJS Foundation',
5+
title: 'Security Compliance Guide v1.0 - Incubating',
6+
description: 'This checklist is for projects that are in the incubating phase and have multiple maintainers.',
7+
url: 'https://openpathfinder.com/docs/checklists/openjsSCGv1.0-incubating',
8+
code_name: 'OpenJS-SCGv1.0-incubating'
9+
}, {
10+
id: 2,
11+
author: 'OpenJS Foundation',
12+
title: 'Security Compliance Guide v1.0 - Active',
13+
description: 'This checklist is for projects that are in the active phase and have multiple maintainers.',
14+
url: 'https://openpathfinder.com/docs/checklists/openjsSCGv1.0-active',
15+
code_name: 'OpenJS-SCGv1.0-active'
16+
}, {
17+
id: 3,
18+
author: 'OpenJS Foundation',
19+
title: 'Security Compliance Guide v1.0 - Retiring',
20+
description: 'This checklist is for projects that are in the retiring phase and have multiple maintainers.',
21+
url: 'https://openpathfinder.com/docs/checklists/openjsSCGv1.0-retiring',
22+
code_name: 'OpenJS-SCGv1.0-retiring'
23+
}, {
24+
id: 4,
25+
author: 'OpenJS Foundation',
26+
title: 'Security Compliance Guide v1.0 - Solo Maintainers incubating',
27+
description: 'This checklist is for projects that are in the incubating phase and have a solo maintainer.',
28+
url: 'https://openpathfinder.com/docs/checklists/openjsSCGv1.0-solo-incubating',
29+
code_name: 'OpenJS-SCGv1.0-solo-incubating'
30+
}, {
31+
id: 5,
32+
author: 'OpenJS Foundation',
33+
title: 'Security Compliance Guide v1.0 - Solo Maintainers Active',
34+
description: 'This checklist is for projects that are in the active phase and have a solo maintainer.',
35+
url: 'https://openpathfinder.com/docs/checklists/openjsSCGv1.0-solo-active',
36+
code_name: 'OpenJS-SCGv1.0-solo-active'
37+
}, {
38+
id: 6,
39+
author: 'OpenJS Foundation',
40+
title: 'Security Compliance Guide v1.0 - Solo Maintainers Retiring',
41+
description: 'This checklist is for projects that are in the retiring phase and have a solo maintainer.',
42+
url: 'https://openpathfinder.com/docs/checklists/openjsSCGv1.0-solo-retiring',
43+
code_name: 'OpenJS-SCGv1.0-solo-retiring'
44+
}
45+
]
46+
47+
exports.up = async (knex) => {
48+
await knex('compliance_checklists').insert(list)
49+
}
50+
51+
exports.down = async (knex) => {
52+
await knex('compliance_checklists').del()
53+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
const priorityGroupOptions = [
2+
'P0', 'P1', 'P2', 'P3', 'P4', 'P5', 'P6', 'P7', 'P8', 'P9', 'P10', 'P11', 'P12', 'P13', 'P14',
3+
'R0', 'R1', 'R2', 'R3', 'R4', 'R5', 'R6', 'R7', 'R8', 'R9', 'R10', 'R11', 'R12', 'R13', 'R14'
4+
]
5+
6+
exports.up = async (knex) => {
7+
await knex.schema.createTable('checklist_items', (table) => {
8+
table.increments('id').primary() // Primary key
9+
table.integer('checklist_id').unsigned().notNullable() // Foreign key to compliance_checklists
10+
table.foreign('checklist_id').references('id').inTable('compliance_checklists').onDelete('CASCADE')
11+
12+
table.integer('compliance_check_id').unsigned().notNullable() // Foreign key to compliance_checks
13+
table.foreign('compliance_check_id').references('id').inTable('compliance_checks').onDelete('CASCADE')
14+
15+
// Overrides for the association if needed
16+
table.enum('priority_group', priorityGroupOptions).nullable()
17+
table.string('section_number').nullable()
18+
table.string('section_name').nullable()
19+
20+
// Timestamps
21+
table.timestamp('created_at').defaultTo(knex.fn.now()).notNullable()
22+
table.timestamp('updated_at').defaultTo(knex.fn.now()).notNullable()
23+
})
24+
25+
// Add trigger to automatically update the 'updated_at' column
26+
await knex.raw(`
27+
CREATE TRIGGER set_updated_at_checklist_items
28+
BEFORE UPDATE ON checklist_items
29+
FOR EACH ROW
30+
EXECUTE FUNCTION update_updated_at_column();
31+
`)
32+
}
33+
34+
exports.down = async (knex) => {
35+
// Drop trigger
36+
await knex.raw('DROP TRIGGER IF EXISTS set_updated_at_checklist_items ON checklist_items;')
37+
// Drop table
38+
await knex.schema.dropTableIfExists('checklist_items')
39+
}

0 commit comments

Comments
 (0)