diff --git a/.github/ISSUE_TEMPLATE/add-a-new-compliance-check.md b/.github/ISSUE_TEMPLATE/add-a-new-compliance-check.md index 05ab506..841f2b6 100644 --- a/.github/ISSUE_TEMPLATE/add-a-new-compliance-check.md +++ b/.github/ISSUE_TEMPLATE/add-a-new-compliance-check.md @@ -26,7 +26,6 @@ You can find more details in [the contributing guide](/CONTRIBUTING.md#current-i - [ ] **3. Implement the Business Logic [Validator Example](https://github.com/OpenPathfinder/visionBoard/commit/44c41d119f0daefb7b2e496ba35d5ab65bcc319b) and [Check Example](https://github.com/OpenPathfinder/visionBoard/commit/6f1e16129ee0d01a1b9b536cd2dc6090b048b71f)** - [ ] Add the specific validator in `src/checks/validators/index.js` - [ ] Add the check logic in `src/checks/complianceChecks` - - [ ] Ensure that the check is in scope for the organization (use `isCheckApplicableToProjectCategory`) - [ ] Ensure that the `severity` value is well calculated (use `getSeverityFromPriorityGroup`) - [ ] Add the alert row in the `compliance_checks_alerts` table when is needed. - [ ] Add the task row in the `compliance_checks_tasks` table when is needed. diff --git a/.github/workflows/review-compliance-checks.yml b/.github/workflows/review-compliance-checks.yml index 65b3196..8b5d43c 100644 --- a/.github/workflows/review-compliance-checks.yml +++ b/.github/workflows/review-compliance-checks.yml @@ -45,7 +45,7 @@ jobs: "- [ ] Have you updated the compliance check in the `compliance_checks` table?\n" + "- [ ] Have you included a specific validator (`src/checks/validators/`) for this check with unit tests (`__tests__/checks/`)?\n" + "- [ ] Have you included a specific file in `src/checks/complianceChecks` with the integration tests (`__tests__/checks/`)?\n" + - "- [ ] Have you included severity validation (`getSeverityFromPriorityGroup`) and checked applicability (`isCheckApplicableToProjectCategory`)?\n" + + "- [ ] Have you included severity validation (`getSeverityFromPriorityGroup`)?\n" + "- [ ] Have you included the tasks, alerts, and results in the database tables?\n" + "- [ ] Have you tested the check with `check run --name {check_code_name}` using the seeded database (`npm run db:seed`)?\n" + "- [ ] Have you created a PR in [the website](https://github.com/OpenPathfinder/website) with the calculation details?\n" + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8c082a3..9912157 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -143,7 +143,6 @@ We are looking for contributors to implement compliance checks in the Dashboard. - **3. Implement the Business Logic ([Validator Example](https://github.com/OpenPathfinder/visionBoard/commit/44c41d119f0daefb7b2e496ba35d5ab65bcc319b) and [Check Example](https://github.com/OpenPathfinder/visionBoard/commit/6f1e16129ee0d01a1b9b536cd2dc6090b048b71f)):** - Add the specific validator in `src/checks/validators/index.js`. - Add the check logic in `src/checks/complianceChecks`. - - Ensure the check is applicable to the organization (`isCheckApplicableToProjectCategory`). - Calculate `severity` accurately (`getSeverityFromPriorityGroup`). - Update relevant database tables (`compliance_checks_alerts`, `compliance_checks_tasks`, `compliance_checks_results`). diff --git a/__tests__/checks/validators.test.js b/__tests__/checks/validators.test.js index 3f286d5..94265fc 100644 --- a/__tests__/checks/validators.test.js +++ b/__tests__/checks/validators.test.js @@ -23,11 +23,8 @@ describe('githubOrgMFA', () => { check = { id: 1, - priority_group: 'P1', - details_url: 'https://example.com', - level_incubating_status: 'expected', - level_active_status: 'expected', - level_retiring_status: 'expected' + default_priority_group: 'P1', + details_url: 'https://example.com' } projects = [ @@ -133,18 +130,6 @@ describe('githubOrgMFA', () => { tasks: [] }) }) - - it('Should skip the check if it is not in scope for the project category', () => { - check.level_active_status = 'n/a' - check.level_incubating_status = 'n/a' - check.level_retiring_status = 'n/a' - const analysis = githubOrgMFA({ organizations, check, projects }) - expect(analysis).toEqual({ - alerts: [], - results: [], - tasks: [] - }) - }) }) describe('softwareDesignTraining', () => { @@ -163,11 +148,8 @@ describe('softwareDesignTraining', () => { check = { id: 1, - priority_group: 'P1', - details_url: 'https://example.com', - level_incubating_status: 'expected', - level_active_status: 'expected', - level_retiring_status: 'expected' + default_priority_group: 'P1', + details_url: 'https://example.com' } projects = [ @@ -299,15 +281,4 @@ describe('softwareDesignTraining', () => { ] }) }) - it('Should skip the check if it is not in scope for the project category', () => { - check.level_active_status = 'n/a' - check.level_incubating_status = 'n/a' - check.level_retiring_status = 'n/a' - const analysis = softwareDesignTraining({ trainings, check, projects }) - expect(analysis).toEqual({ - alerts: [], - results: [], - tasks: [] - }) - }) }) diff --git a/__tests__/utils.test.js b/__tests__/utils.test.js index 68ef298..d2b704a 100644 --- a/__tests__/utils.test.js +++ b/__tests__/utils.test.js @@ -1,4 +1,4 @@ -const { validateGithubUrl, ensureGithubToken, groupArrayItemsByCriteria, isCheckApplicableToProjectCategory, getSeverityFromPriorityGroup, isDateWithinPolicy, redactSensitiveData } = require('../src/utils/index') +const { validateGithubUrl, ensureGithubToken, groupArrayItemsByCriteria, getSeverityFromPriorityGroup, isDateWithinPolicy, redactSensitiveData } = require('../src/utils/index') describe('ensureGithubToken', () => { let originalGithubToken @@ -66,31 +66,6 @@ describe('groupArrayItemsByCriteria', () => { }) }) -describe('isCheckApplicableToProjectCategory', () => { - const disabledCheck = { - level_active_status: 'n/a', - level_incubating_status: 'n/a', - level_retiring_status: 'n/a' - } - - it('should return false if the check is not applicable to the project category', () => { - let project = { category: 'impact' } - expect(isCheckApplicableToProjectCategory(disabledCheck, project)).toBe(false) - project = { category: 'incubation' } - expect(isCheckApplicableToProjectCategory(disabledCheck, project)).toBe(false) - project = { category: 'emeritus' } - expect(isCheckApplicableToProjectCategory(disabledCheck, project)).toBe(false) - project = { category: 'at-large' } - expect(isCheckApplicableToProjectCategory(disabledCheck, project)).toBe(false) - }) - - it('should return true if the check is applicable to the project category', () => { - const project = { category: 'impact' } - const check = { ...disabledCheck, level_active_status: 'recommended' } - expect(isCheckApplicableToProjectCategory(check, project)).toBe(true) - }) -}) - describe('getSeverityFromPriorityGroup', () => { it('should return the correct severity based on the priority group', () => { expect(getSeverityFromPriorityGroup('P0')).toBe('critical') diff --git a/src/checks/validators/githubOrgMFA.js b/src/checks/validators/githubOrgMFA.js index 2f8daf4..5bae7de 100644 --- a/src/checks/validators/githubOrgMFA.js +++ b/src/checks/validators/githubOrgMFA.js @@ -1,5 +1,5 @@ const debug = require('debug')('checks:validator:githubOrgMFA') -const { getSeverityFromPriorityGroup, isCheckApplicableToProjectCategory, groupArrayItemsByCriteria } = require('../../utils') +const { getSeverityFromPriorityGroup, groupArrayItemsByCriteria } = require('../../utils') const groupByProject = groupArrayItemsByCriteria('project_id') @@ -17,17 +17,11 @@ module.exports = ({ organizations = [], check, projects = [] }) => { organizationsGroupedByProject.forEach((projectOrgs) => { debug(`Processing project (${projectOrgs[0].project_id})`) const project = projects.find(p => p.id === projectOrgs[0].project_id) - const isInScope = isCheckApplicableToProjectCategory(check, project) - // If the check is not in scope, skip it. - if (!isInScope) { - debug(`This check is not in scope for project (${project.id})`) - return - } const baseData = { project_id: projectOrgs[0].project_id, compliance_check_id: check.id, - severity: getSeverityFromPriorityGroup(check.priority_group) + severity: getSeverityFromPriorityGroup(check.default_priority_group) } const result = { ...baseData } diff --git a/src/checks/validators/softwareDesignTraining.js b/src/checks/validators/softwareDesignTraining.js index 872fcca..f4e47d4 100644 --- a/src/checks/validators/softwareDesignTraining.js +++ b/src/checks/validators/softwareDesignTraining.js @@ -1,5 +1,5 @@ const debug = require('debug')('checks:validator:softwareDesignTraining') -const { isCheckApplicableToProjectCategory, getSeverityFromPriorityGroup, isDateWithinPolicy } = require('../../utils') +const { getSeverityFromPriorityGroup, isDateWithinPolicy } = require('../../utils') const expirationPolicy = '6m' @@ -11,17 +11,10 @@ module.exports = ({ trainings = [], check, projects = [] }) => { debug('Processing Projects...') projects.forEach(project => { - const isInScope = isCheckApplicableToProjectCategory(check, project) - // If the check is not in scope, skip it. - if (!isInScope) { - debug(`This check is not in scope for project (${project.id})`) - return - } - const baseData = { project_id: project.id, compliance_check_id: check.id, - severity: getSeverityFromPriorityGroup(check.priority_group) + severity: getSeverityFromPriorityGroup(check.default_priority_group) } const result = { ...baseData } diff --git a/src/database/migrations/1733494222119_add_compliance_checks.js b/src/database/migrations/1733494222119_add_compliance_checks.js index 41a5dbe..fe2cce5 100644 --- a/src/database/migrations/1733494222119_add_compliance_checks.js +++ b/src/database/migrations/1733494222119_add_compliance_checks.js @@ -1,4 +1,8 @@ const statusLevels = ['n/a', 'deferrable', 'expected', 'recommended'] +const priorityGroupOptions = [ + 'P0', 'P1', 'P2', 'P3', 'P4', 'P5', 'P6', 'P7', 'P8', 'P9', 'P10', 'P11', 'P12', 'P13', 'P14', + 'R0', 'R1', 'R2', 'R3', 'R4', 'R5', 'R6', 'R7', 'R8', 'R9', 'R10', 'R11', 'R12', 'R13', 'R14' +] exports.up = async (knex) => { await knex.schema.createTable('compliance_checks', (table) => { @@ -8,7 +12,7 @@ exports.up = async (knex) => { table.string('section_number').notNullable() table.string('section_name').notNullable() table.string('code_name').unique().notNullable() - table.string('priority_group').notNullable() + table.enum('priority_group', priorityGroupOptions).notNullable() table.boolean('is_c_scrm').notNullable().defaultTo(false) table.enum('level_incubating_status', statusLevels).notNullable() table.enum('level_active_status', statusLevels).notNullable() diff --git a/src/database/migrations/1735041299955_alter_compliance_checks.js b/src/database/migrations/1735041299955_alter_compliance_checks.js new file mode 100644 index 0000000..7caf383 --- /dev/null +++ b/src/database/migrations/1735041299955_alter_compliance_checks.js @@ -0,0 +1,29 @@ +const statusLevels = ['n/a', 'deferrable', 'expected', 'recommended'] + +exports.up = async (knex) => { + await knex.schema.alterTable('compliance_checks', (table) => { + // Drop old fields + table.dropColumn('level_incubating_status') + table.dropColumn('level_active_status') + table.dropColumn('level_retiring_status') + + // Rename fields + table.renameColumn('priority_group', 'default_priority_group') + table.renameColumn('section_number', 'default_section_number') + table.renameColumn('section_name', 'default_section_name') + }) +} + +exports.down = async (knex) => { + await knex.schema.alterTable('compliance_checks', (table) => { + // IMPORTANT: Re-add dropped fields but without the original values and nullable. + table.enum('level_incubating_status', statusLevels).nullable() + table.enum('level_active_status', statusLevels).nullable() + table.enum('level_retiring_status', statusLevels).nullable() + + // Rename fields back + table.renameColumn('default_priority_group', 'priority_group') + table.renameColumn('default_section_number', 'section_number') + table.renameColumn('default_section_name', 'section_name') + }) +} diff --git a/src/database/migrations/1735041508837_add_compliance_checklists.js b/src/database/migrations/1735041508837_add_compliance_checklists.js new file mode 100644 index 0000000..fc7fde6 --- /dev/null +++ b/src/database/migrations/1735041508837_add_compliance_checklists.js @@ -0,0 +1,29 @@ +exports.up = async (knex) => { + await knex.schema.createTable('compliance_checklists', (table) => { + table.increments('id').primary() // Primary key + table.text('author').notNullable() + table.string('title').notNullable() + table.text('description').notNullable() + table.string('code_name').notNullable() + table.text('url').notNullable() + + // Timestamps + table.timestamp('created_at').defaultTo(knex.fn.now()).notNullable() + table.timestamp('updated_at').defaultTo(knex.fn.now()).notNullable() + }) + + // Add trigger to automatically update the 'updated_at' column + await knex.raw(` + CREATE TRIGGER set_updated_at_compliance_checklists + BEFORE UPDATE ON compliance_checklists + FOR EACH ROW + EXECUTE FUNCTION update_updated_at_column(); + `) +} + +exports.down = async (knex) => { + // Drop trigger + await knex.raw('DROP TRIGGER IF EXISTS set_updated_at_compliance_checklists ON compliance_checklists;') + // Drop table + await knex.schema.dropTableIfExists('compliance_checklists') +} diff --git a/src/database/migrations/1735041860349_populate_compliance_checklists.js b/src/database/migrations/1735041860349_populate_compliance_checklists.js new file mode 100644 index 0000000..c39d61f --- /dev/null +++ b/src/database/migrations/1735041860349_populate_compliance_checklists.js @@ -0,0 +1,53 @@ +const list = [ + { + id: 1, + author: 'OpenJS Foundation', + title: 'Security Compliance Guide v1.0 - Incubating', + description: 'This checklist is for projects that are in the incubating phase and have multiple maintainers.', + url: 'https://openpathfinder.com/docs/checklists/openjsSCGv1.0-incubating', + code_name: 'OpenJS-SCGv1.0-incubating' + }, { + id: 2, + author: 'OpenJS Foundation', + title: 'Security Compliance Guide v1.0 - Active', + description: 'This checklist is for projects that are in the active phase and have multiple maintainers.', + url: 'https://openpathfinder.com/docs/checklists/openjsSCGv1.0-active', + code_name: 'OpenJS-SCGv1.0-active' + }, { + id: 3, + author: 'OpenJS Foundation', + title: 'Security Compliance Guide v1.0 - Retiring', + description: 'This checklist is for projects that are in the retiring phase and have multiple maintainers.', + url: 'https://openpathfinder.com/docs/checklists/openjsSCGv1.0-retiring', + code_name: 'OpenJS-SCGv1.0-retiring' + }, { + id: 4, + author: 'OpenJS Foundation', + title: 'Security Compliance Guide v1.0 - Solo Maintainers incubating', + description: 'This checklist is for projects that are in the incubating phase and have a solo maintainer.', + url: 'https://openpathfinder.com/docs/checklists/openjsSCGv1.0-solo-incubating', + code_name: 'OpenJS-SCGv1.0-solo-incubating' + }, { + id: 5, + author: 'OpenJS Foundation', + title: 'Security Compliance Guide v1.0 - Solo Maintainers Active', + description: 'This checklist is for projects that are in the active phase and have a solo maintainer.', + url: 'https://openpathfinder.com/docs/checklists/openjsSCGv1.0-solo-active', + code_name: 'OpenJS-SCGv1.0-solo-active' + }, { + id: 6, + author: 'OpenJS Foundation', + title: 'Security Compliance Guide v1.0 - Solo Maintainers Retiring', + description: 'This checklist is for projects that are in the retiring phase and have a solo maintainer.', + url: 'https://openpathfinder.com/docs/checklists/openjsSCGv1.0-solo-retiring', + code_name: 'OpenJS-SCGv1.0-solo-retiring' + } +] + +exports.up = async (knex) => { + await knex('compliance_checklists').insert(list) +} + +exports.down = async (knex) => { + await knex('compliance_checklists').del() +} diff --git a/src/database/migrations/1735042950475_add_checklist_items.js b/src/database/migrations/1735042950475_add_checklist_items.js new file mode 100644 index 0000000..72c0cbf --- /dev/null +++ b/src/database/migrations/1735042950475_add_checklist_items.js @@ -0,0 +1,39 @@ +const priorityGroupOptions = [ + 'P0', 'P1', 'P2', 'P3', 'P4', 'P5', 'P6', 'P7', 'P8', 'P9', 'P10', 'P11', 'P12', 'P13', 'P14', + 'R0', 'R1', 'R2', 'R3', 'R4', 'R5', 'R6', 'R7', 'R8', 'R9', 'R10', 'R11', 'R12', 'R13', 'R14' +] + +exports.up = async (knex) => { + await knex.schema.createTable('checklist_items', (table) => { + table.increments('id').primary() // Primary key + table.integer('checklist_id').unsigned().notNullable() // Foreign key to compliance_checklists + table.foreign('checklist_id').references('id').inTable('compliance_checklists').onDelete('CASCADE') + + table.integer('compliance_check_id').unsigned().notNullable() // Foreign key to compliance_checks + table.foreign('compliance_check_id').references('id').inTable('compliance_checks').onDelete('CASCADE') + + // Overrides for the association if needed + table.enum('priority_group', priorityGroupOptions).nullable() + table.string('section_number').nullable() + table.string('section_name').nullable() + + // Timestamps + table.timestamp('created_at').defaultTo(knex.fn.now()).notNullable() + table.timestamp('updated_at').defaultTo(knex.fn.now()).notNullable() + }) + + // Add trigger to automatically update the 'updated_at' column + await knex.raw(` + CREATE TRIGGER set_updated_at_checklist_items + BEFORE UPDATE ON checklist_items + FOR EACH ROW + EXECUTE FUNCTION update_updated_at_column(); + `) +} + +exports.down = async (knex) => { + // Drop trigger + await knex.raw('DROP TRIGGER IF EXISTS set_updated_at_checklist_items ON checklist_items;') + // Drop table + await knex.schema.dropTableIfExists('checklist_items') +} diff --git a/src/database/migrations/1735043007720_populate_checklist_items.js b/src/database/migrations/1735043007720_populate_checklist_items.js new file mode 100644 index 0000000..d42feca --- /dev/null +++ b/src/database/migrations/1735043007720_populate_checklist_items.js @@ -0,0 +1,588 @@ +const checks = [ + { + compliance_check_id: 2, + code_name: 'owaspTop10Training', + priority_group: 'P0', + section_number: '7', + section_name: 'code quality' + }, + { + compliance_check_id: 4, + code_name: 'npmOrgMFA', + priority_group: 'P1', + section_number: '1', + section_name: 'user authentication' + }, + { + compliance_check_id: 5, + code_name: 'orgToolingMFA', + priority_group: 'P1', + section_number: '1', + section_name: 'user authentication' + }, + { + compliance_check_id: 6, + code_name: 'MFAImpersonationDefense', + priority_group: 'P1', + section_number: '1', + section_name: 'user authentication' + }, + { + compliance_check_id: 7, + code_name: 'noSensitiveInfoInRepositories', + priority_group: 'P2', + section_number: '3', + section_name: 'service authentication' + }, + { + compliance_check_id: 8, + code_name: 'injectedSecretsAtRuntime', + priority_group: 'P2', + section_number: '3', + section_name: 'service authentication' + }, + { + compliance_check_id: 9, + code_name: 'scanCommitsForSensitiveInfo', + priority_group: 'P2', + section_number: '7', + section_name: 'code quality' + }, + { + compliance_check_id: 10, + code_name: 'preventLandingSensitiveCommits', + priority_group: 'P2', + section_number: '7', + section_name: 'code quality' + }, + { + compliance_check_id: 11, + code_name: 'SSHKeysRequired', + priority_group: 'P3', + section_number: '1', + section_name: 'user authentication' + }, + { + compliance_check_id: 12, + code_name: 'npmPublicationMFA', + priority_group: 'P3', + section_number: '3', + section_name: 'service authentication' + }, + { + compliance_check_id: 13, + code_name: 'githubWebhookSecrets', + priority_group: 'P3', + section_number: '3', + section_name: 'service authentication' + }, + { + compliance_check_id: 67, + code_name: 'requireCodeOwnersReviewForLargeTeams', + priority_group: 'R6', + section_number: '8', + section_name: 'code review' + }, + { + compliance_check_id: 14, + code_name: 'restrictedOrgPermissions', + priority_group: 'P4', + section_number: '2', + section_name: 'user account permissions' + }, + { + compliance_check_id: 15, + code_name: 'adminRepoCreationOnly', + priority_group: 'P4', + section_number: '2', + section_name: 'user account permissions' + }, + { + compliance_check_id: 16, + code_name: 'preventBranchProtectionBypass', + priority_group: 'P4', + section_number: '2', + section_name: 'user account permissions' + }, + { + compliance_check_id: 17, + code_name: 'defineFunctionalRoles', + priority_group: 'P4', + section_number: '2', + section_name: 'user account permissions' + }, + { + compliance_check_id: 18, + code_name: 'githubWriteAccessRoles', + priority_group: 'P4', + section_number: '2', + section_name: 'user account permissions' + }, + { + compliance_check_id: 19, + code_name: 'twoOrMoreOwnersForAccess', + priority_group: 'P4', + section_number: '2', + section_name: 'user account permissions' + }, + { + compliance_check_id: 20, + code_name: 'patchCriticalVulns30Days', + priority_group: 'P5', + section_number: '5', + section_name: 'vulnerability management' + }, + { + compliance_check_id: 21, + code_name: 'patchNonCriticalVulns90Days', + priority_group: 'P5', + section_number: '5', + section_name: 'vulnerability management' + }, + { + compliance_check_id: 22, + code_name: 'automateVulnDetection', + priority_group: 'P6', + section_number: '11', + section_name: 'dependency management' + }, + { + compliance_check_id: 23, + code_name: 'staticCodeAnalysis', + priority_group: 'P6', + section_number: '7', + section_name: 'code quality' + }, + { + compliance_check_id: 24, + code_name: 'resolveLinterWarnings', + priority_group: 'P6', + section_number: '7', + section_name: 'code quality' + }, + { + compliance_check_id: 25, + code_name: 'staticAppSecTesting', + priority_group: 'P6', + section_number: '7', + section_name: 'code quality' + }, + { + compliance_check_id: 26, + code_name: 'commitStatusChecks', + priority_group: 'P6', + section_number: '7', + section_name: 'code quality' + }, + { + compliance_check_id: 27, + code_name: 'securityMdMeetsOpenJSCVD', + priority_group: 'P7', + section_number: '6', + section_name: 'coordinated vulnerability disclosure' + }, + { + compliance_check_id: 28, + code_name: 'useCVDToolForVulns', + priority_group: 'P7', + section_number: '6', + section_name: 'coordinated vulnerability disclosure' + }, + { + compliance_check_id: 29, + code_name: 'vulnResponse14Days', + priority_group: 'P7', + section_number: '6', + section_name: 'coordinated vulnerability disclosure' + }, + { + compliance_check_id: 30, + code_name: 'incidentResponsePlan', + priority_group: 'P7', + section_number: '6', + section_name: 'coordinated vulnerability disclosure' + }, + { + compliance_check_id: 31, + code_name: 'assignCVEForKnownVulns', + priority_group: 'P7', + section_number: '6', + section_name: 'coordinated vulnerability disclosure' + }, + { + compliance_check_id: 32, + code_name: 'includeCVEInReleaseNotes', + priority_group: 'P7', + section_number: '6', + section_name: 'coordinated vulnerability disclosure' + }, + { + compliance_check_id: 33, + code_name: 'regressionTestsForVulns', + priority_group: 'P8', + section_number: '7', + section_name: 'code quality' + }, + { + compliance_check_id: 34, + code_name: 'defaultTokenPermissionsReadOnly', + priority_group: 'P9', + section_number: '4', + section_name: 'github workflow permissions' + }, + { + compliance_check_id: 35, + code_name: 'blockWorkflowPRApproval', + priority_group: 'P9', + section_number: '4', + section_name: 'github workflow permissions' + }, + { + compliance_check_id: 36, + code_name: 'noForcePushDefaultBranch', + priority_group: 'P9', + section_number: '9', + section_name: 'source control' + }, + { + compliance_check_id: 37, + code_name: 'preventDeletionDefaultBranch', + priority_group: 'P9', + section_number: '9', + section_name: 'source control' + }, + { + compliance_check_id: 38, + code_name: 'upToDateDefaultBranchBeforeMerge', + priority_group: 'P9', + section_number: '9', + section_name: 'source control' + }, + { + compliance_check_id: 39, + code_name: 'restrictOrgSecrets', + priority_group: 'P10', + section_number: '4', + section_name: 'github workflows' + }, + { + compliance_check_id: 40, + code_name: 'verifiedActionsOnly', + priority_group: 'P10', + section_number: '4', + section_name: 'github workflows' + }, + { + compliance_check_id: 41, + code_name: 'noSelfHostedRunners', + priority_group: 'P10', + section_number: '4', + section_name: 'github workflows' + }, + { + compliance_check_id: 42, + code_name: 'noArbitraryCodeInPipeline', + priority_group: 'P11', + section_number: '4', + section_name: 'github workflows' + }, + { + compliance_check_id: 43, + code_name: 'limitWorkflowWritePermissions', + priority_group: 'P11', + section_number: '4', + section_name: 'github workflows' + }, + { + compliance_check_id: 44, + code_name: 'preventScriptInjection', + priority_group: 'P11', + section_number: '4', + section_name: 'github workflows' + }, + { + compliance_check_id: 45, + code_name: 'consistentBuildProcessDocs', + priority_group: 'P12', + section_number: '4', + section_name: 'github workflows' + }, + { + compliance_check_id: 46, + code_name: 'upgradePathDocs', + priority_group: 'P12', + section_number: '5', + section_name: 'vulnerability management' + }, + { + compliance_check_id: 47, + code_name: 'softwareArchitectureDocs', + priority_group: 'P12', + section_number: '8', + section_name: 'code review' + }, + { + compliance_check_id: 48, + code_name: 'ciAndCdPipelineAsCode', + priority_group: 'P12', + section_number: '9', + section_name: 'source control' + }, + { + compliance_check_id: 49, + code_name: 'pinActionsToSHA', + priority_group: 'P13', + section_number: '4', + section_name: 'github workflows' + }, + { + compliance_check_id: 50, + code_name: 'automateDependencyManagement', + priority_group: 'P14', + section_number: '10', + section_name: 'dependency inventory' + }, + { + compliance_check_id: 51, + code_name: 'machineReadableDependencies', + priority_group: 'P14', + section_number: '10', + section_name: 'dependency inventory' + }, + { + compliance_check_id: 52, + code_name: 'identifyModifiedDependencies', + priority_group: 'P14', + section_number: '10', + section_name: 'dependency inventory' + }, + { + compliance_check_id: 53, + code_name: 'annualDependencyRefresh', + priority_group: 'P14', + section_number: '5', + section_name: 'vulnerability management' + }, + { + compliance_check_id: 54, + code_name: 'useHwKeyGithubAccess', + priority_group: 'R1', + section_number: '1', + section_name: 'user authentication' + }, + { + compliance_check_id: 55, + code_name: 'useHwKeyGithubNonInteractive', + priority_group: 'R1', + section_number: '1', + section_name: 'user authentication' + }, + { + compliance_check_id: 56, + code_name: 'useHwKeyOtherContexts', + priority_group: 'R1', + section_number: '1', + section_name: 'user authentication' + }, + { + compliance_check_id: 57, + code_name: 'forkWorkflowApproval', + priority_group: 'R2', + section_number: '4', + section_name: 'github workflows' + }, + { + compliance_check_id: 58, + code_name: 'workflowSecurityScanner', + priority_group: 'R2', + section_number: '4', + section_name: 'github workflows' + }, + { + compliance_check_id: 59, + code_name: 'runnerSecurityScanner', + priority_group: 'R2', + section_number: '4', + section_name: 'github workflows' + }, + { + compliance_check_id: 60, + code_name: 'activeAdminsSixMonths', + priority_group: 'R3', + section_number: '2', + section_name: 'user account permissions' + }, + { + compliance_check_id: 61, + code_name: 'activeWritersSixMonths', + priority_group: 'R3', + section_number: '2', + section_name: 'user account permissions' + }, + { + compliance_check_id: 62, + code_name: 'PRsBeforeMerge', + priority_group: 'R4', + section_number: '9', + section_name: 'source control' + }, + { + compliance_check_id: 63, + code_name: 'commitSignoffForWeb', + priority_group: 'R4', + section_number: '9', + section_name: 'source control' + }, + { + compliance_check_id: 64, + code_name: 'requireSignedCommits', + priority_group: 'R4', + section_number: '9', + section_name: 'source control' + }, + { + compliance_check_id: 65, + code_name: 'includePackageLock', + priority_group: 'R5', + section_number: '10', + section_name: 'dependency inventory' + }, + { + compliance_check_id: 66, + code_name: 'requireTwoPartyReview', + priority_group: 'R6', + section_number: '8', + section_name: 'code review' + }, + { + compliance_check_id: 68, + code_name: 'requirePRApprovalForMainline', + priority_group: 'R6', + section_number: '9', + section_name: 'source control' + }, + { + compliance_check_id: 69, + code_name: 'limitOrgOwners', + priority_group: 'R7', + section_number: '2', + section_name: 'user account permissions' + }, + { + compliance_check_id: 70, + code_name: 'limitRepoAdmins', + priority_group: 'R7', + section_number: '2', + section_name: 'user account permissions' + }, + { + compliance_check_id: 71, + code_name: 'patchExploitableHighVulns14Days', + priority_group: 'R8', + section_number: '5', + section_name: 'vulnerability management' + }, + { + compliance_check_id: 72, + code_name: 'patchExploitableNoncCriticalVulns60Days', + priority_group: 'R8', + section_number: '5', + section_name: 'vulnerability management' + }, + { + compliance_check_id: 3, + code_name: 'githubOrgMFA', + priority_group: 'P1', + section_number: '1', + section_name: 'user authentication' + }, + { + compliance_check_id: 1, + code_name: 'softwareDesignTraining', + priority_group: 'P0', + section_number: '7', + section_name: 'code quality' + } +] + +const checkListsIds = { + incubating: 1, + active: 2, + retiring: 3, + 'solo-incubating': 4, + 'solo-active': 5, + 'solo-retiring': 6 +} + +const retiringExcludedChecks = [ + 'scanCommitsForSensitiveInfo', + 'preventLandingSensitiveCommits', + 'patchCriticalVulns30Days', + 'patchNonCriticalVulns90Days', + 'staticCodeAnalysis', + 'resolveLinterWarnings', + 'staticAppSecTesting', + 'commitStatusChecks', + 'vulnResponse14Days', + 'regressionTestsForVulns', + 'defaultTokenPermissionsReadOnly', + 'restrictOrgSecrets', + 'verifiedActionsOnly', + 'noArbitraryCodeInPipeline', + 'preventScriptInjection', + 'consistentBuildProcessDocs', + 'upgradePathDocs', + 'softwareArchitectureDocs', + 'ciAndCdPipelineAsCode', + 'pinActionsToSHA', + 'annualDependencyRefresh', + 'activeAdminsSixMonths', + 'activeWritersSixMonths', + 'requireTwoPartyReview', + 'requireCodeOwnersReviewForLargeTeams', + 'patchExploitableHighVulns14Days', + 'patchExploitableNoncCriticalVulns60Days' +] + +const soloMaintainerExcludedChecks = [ + 'preventBranchProtectionBypass', + 'twoOrMoreOwnersForAccess', + 'softwareArchitectureDocs', + 'requireTwoPartyReview', + 'requirePRApprovalForMainline' +] + +// IMPORTANT: Deferrable items are included, only N/A items are excluded +const activeChecks = checks.map(check => ({ ...check, checklist_id: checkListsIds.active })) +const incubatingChecks = checks.map(check => ({ ...check, checklist_id: checkListsIds.incubating })) +const retiringChecks = checks + .filter(check => !retiringExcludedChecks.includes(check.code_name)) + .map(check => ({ ...check, checklist_id: checkListsIds.retiring })) + +const soloActiveChecks = activeChecks + .filter(check => !soloMaintainerExcludedChecks.includes(check.code_name)) + .map(check => ({ ...check, checklist_id: checkListsIds['solo-active'] })) +const soloIncubatingChecks = incubatingChecks + .filter(check => !soloMaintainerExcludedChecks.includes(check.code_name)) + .map(check => ({ ...check, checklist_id: checkListsIds['solo-incubating'] })) +const soloRetiringChecks = retiringChecks + .filter(check => !soloMaintainerExcludedChecks.includes(check.code_name)) + .map(check => ({ ...check, checklist_id: checkListsIds['solo-retiring'] })) + +const list = [ + ...activeChecks, + ...incubatingChecks, + ...retiringChecks, + ...soloActiveChecks, + ...soloIncubatingChecks, + ...soloRetiringChecks +] // Remove unused properties + .map(({ code_name: codeName, ...rest }) => rest) + +exports.up = async (knex) => { + await knex('checklist_items').insert(list) +} + +exports.down = async (knex) => { + await knex('checklist_items').del() +} diff --git a/src/database/schema/schema.sql b/src/database/schema/schema.sql index e03a45d..d70d934 100644 --- a/src/database/schema/schema.sql +++ b/src/database/schema/schema.sql @@ -35,6 +35,79 @@ SET default_tablespace = ''; SET default_table_access_method = heap; +-- +-- Name: checklist_items; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.checklist_items ( + id integer NOT NULL, + checklist_id integer NOT NULL, + compliance_check_id integer NOT NULL, + priority_group text, + section_number character varying(255), + section_name character varying(255), + created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + CONSTRAINT checklist_items_priority_group_check CHECK ((priority_group = ANY (ARRAY['P0'::text, 'P1'::text, 'P2'::text, 'P3'::text, 'P4'::text, 'P5'::text, 'P6'::text, 'P7'::text, 'P8'::text, 'P9'::text, 'P10'::text, 'P11'::text, 'P12'::text, 'P13'::text, 'P14'::text, 'R0'::text, 'R1'::text, 'R2'::text, 'R3'::text, 'R4'::text, 'R5'::text, 'R6'::text, 'R7'::text, 'R8'::text, 'R9'::text, 'R10'::text, 'R11'::text, 'R12'::text, 'R13'::text, 'R14'::text]))) +); + + +-- +-- Name: checklist_items_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.checklist_items_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: checklist_items_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.checklist_items_id_seq OWNED BY public.checklist_items.id; + + +-- +-- Name: compliance_checklists; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.compliance_checklists ( + id integer NOT NULL, + author text NOT NULL, + title character varying(255) NOT NULL, + description text NOT NULL, + code_name character varying(255) NOT NULL, + url text NOT NULL, + created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL +); + + +-- +-- Name: compliance_checklists_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.compliance_checklists_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: compliance_checklists_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.compliance_checklists_id_seq OWNED BY public.compliance_checklists.id; + + -- -- Name: compliance_checks; Type: TABLE; Schema: public; Owner: - -- @@ -43,14 +116,11 @@ CREATE TABLE public.compliance_checks ( id integer NOT NULL, title character varying(255) NOT NULL, description text NOT NULL, - section_number character varying(255) NOT NULL, - section_name character varying(255) NOT NULL, + default_section_number character varying(255) NOT NULL, + default_section_name character varying(255) NOT NULL, code_name character varying(255) NOT NULL, - priority_group character varying(255) NOT NULL, + default_priority_group text NOT NULL, is_c_scrm boolean DEFAULT false NOT NULL, - level_incubating_status text NOT NULL, - level_active_status text NOT NULL, - level_retiring_status text NOT NULL, mitre_url character varying(255), mitre_description character varying(255), how_to_url character varying(255), @@ -65,9 +135,7 @@ CREATE TABLE public.compliance_checks ( updated_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, CONSTRAINT compliance_checks_implementation_status_check CHECK ((implementation_status = ANY (ARRAY['pending'::text, 'completed'::text]))), CONSTRAINT compliance_checks_implementation_type_check CHECK ((implementation_type = ANY (ARRAY['manual'::text, 'computed'::text]))), - CONSTRAINT compliance_checks_level_active_status_check CHECK ((level_active_status = ANY (ARRAY['n/a'::text, 'deferrable'::text, 'expected'::text, 'recommended'::text]))), - CONSTRAINT compliance_checks_level_incubating_status_check CHECK ((level_incubating_status = ANY (ARRAY['n/a'::text, 'deferrable'::text, 'expected'::text, 'recommended'::text]))), - CONSTRAINT compliance_checks_level_retiring_status_check CHECK ((level_retiring_status = ANY (ARRAY['n/a'::text, 'deferrable'::text, 'expected'::text, 'recommended'::text]))) + CONSTRAINT compliance_checks_priority_group_check CHECK ((default_priority_group = ANY (ARRAY['P0'::text, 'P1'::text, 'P2'::text, 'P3'::text, 'P4'::text, 'P5'::text, 'P6'::text, 'P7'::text, 'P8'::text, 'P9'::text, 'P10'::text, 'P11'::text, 'P12'::text, 'P13'::text, 'P14'::text, 'R0'::text, 'R1'::text, 'R2'::text, 'R3'::text, 'R4'::text, 'R5'::text, 'R6'::text, 'R7'::text, 'R8'::text, 'R9'::text, 'R10'::text, 'R11'::text, 'R12'::text, 'R13'::text, 'R14'::text]))) ); @@ -641,6 +709,20 @@ CREATE SEQUENCE public.software_design_training_id_seq ALTER SEQUENCE public.software_design_training_id_seq OWNED BY public.software_design_training.id; +-- +-- Name: checklist_items id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.checklist_items ALTER COLUMN id SET DEFAULT nextval('public.checklist_items_id_seq'::regclass); + + +-- +-- Name: compliance_checklists id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.compliance_checklists ALTER COLUMN id SET DEFAULT nextval('public.compliance_checklists_id_seq'::regclass); + + -- -- Name: compliance_checks id; Type: DEFAULT; Schema: public; Owner: - -- @@ -718,6 +800,22 @@ ALTER TABLE ONLY public.projects ALTER COLUMN id SET DEFAULT nextval('public.pro ALTER TABLE ONLY public.software_design_training ALTER COLUMN id SET DEFAULT nextval('public.software_design_training_id_seq'::regclass); +-- +-- Name: checklist_items checklist_items_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.checklist_items + ADD CONSTRAINT checklist_items_pkey PRIMARY KEY (id); + + +-- +-- Name: compliance_checklists compliance_checklists_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.compliance_checklists + ADD CONSTRAINT compliance_checklists_pkey PRIMARY KEY (id); + + -- -- Name: compliance_checks_alerts compliance_checks_alerts_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -846,6 +944,20 @@ ALTER TABLE ONLY public.software_design_training ADD CONSTRAINT software_design_training_pkey PRIMARY KEY (id); +-- +-- Name: checklist_items set_updated_at_checklist_items; Type: TRIGGER; Schema: public; Owner: - +-- + +CREATE TRIGGER set_updated_at_checklist_items BEFORE UPDATE ON public.checklist_items FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column(); + + +-- +-- Name: compliance_checklists set_updated_at_compliance_checklists; Type: TRIGGER; Schema: public; Owner: - +-- + +CREATE TRIGGER set_updated_at_compliance_checklists BEFORE UPDATE ON public.compliance_checklists FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column(); + + -- -- Name: compliance_checks set_updated_at_compliance_checks; Type: TRIGGER; Schema: public; Owner: - -- @@ -909,6 +1021,22 @@ CREATE TRIGGER set_updated_at_projects BEFORE UPDATE ON public.projects FOR EACH CREATE TRIGGER set_updated_at_software_design_training BEFORE UPDATE ON public.software_design_training FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column(); +-- +-- Name: checklist_items checklist_items_checklist_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.checklist_items + ADD CONSTRAINT checklist_items_checklist_id_foreign FOREIGN KEY (checklist_id) REFERENCES public.compliance_checklists(id) ON DELETE CASCADE; + + +-- +-- Name: checklist_items checklist_items_compliance_check_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.checklist_items + ADD CONSTRAINT checklist_items_compliance_check_id_foreign FOREIGN KEY (compliance_check_id) REFERENCES public.compliance_checks(id) ON DELETE CASCADE; + + -- -- Name: compliance_checks_alerts compliance_checks_alerts_compliance_check_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - -- diff --git a/src/utils/index.js b/src/utils/index.js index 6133994..ff16076 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -61,19 +61,6 @@ const getSeverityFromPriorityGroup = (priorityGroup) => { } } -const isCheckApplicableToProjectCategory = (check, project) => { - if (['impact', 'at-large'].includes(project.category) && check.level_active_status === 'n/a') { - return false - } - if (project.category === 'incubation' && check.level_incubating_status === 'n/a') { - return false - } - if (project.category === 'emeritus' && check.level_retiring_status === 'n/a') { - return false - } - return true -} - const groupArrayItemsByCriteria = criteria => items => Object.values( items.reduce((acc, item) => { if (!acc[item[criteria]]) { @@ -124,7 +111,6 @@ module.exports = { validateGithubUrl, ensureGithubToken, getSeverityFromPriorityGroup, - isCheckApplicableToProjectCategory, groupArrayItemsByCriteria, redactSensitiveData, logger