From a1a11fffdbfc6dd44d11df46049fd6602b8836b0 Mon Sep 17 00:00:00 2001 From: Andrew Foote Date: Mon, 20 Apr 2026 16:13:16 -0400 Subject: [PATCH 01/86] Adding validation for invalid key input on Org creation/update --- schemas/registry-org/BaseOrg.json | 2 +- schemas/registry-org/CNAOrg.json | 24 +++++++++++++++++++++--- src/model/cnaorg.js | 4 ++-- 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/schemas/registry-org/BaseOrg.json b/schemas/registry-org/BaseOrg.json index 87f1b1e57..d3c4a45a3 100644 --- a/schemas/registry-org/BaseOrg.json +++ b/schemas/registry-org/BaseOrg.json @@ -1,6 +1,6 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "./BaseOrg.json", + "$id": "/BaseOrg", "type": "object", "title": "CVE Base Organization", "description": "Base schema for a CVE Organization", diff --git a/schemas/registry-org/CNAOrg.json b/schemas/registry-org/CNAOrg.json index 0402e8338..32a4bbcc9 100644 --- a/schemas/registry-org/CNAOrg.json +++ b/schemas/registry-org/CNAOrg.json @@ -5,9 +5,26 @@ "title": "CVE CNA Organization", "description": "Schema for a CVE CNA Organization", "allOf": [ - { "$ref": "./BaseOrg.json" }, + { "$ref": "/BaseOrg" }, { "properties": { + "UUID": {}, + "__t": {}, + "short_name": {}, + "long_name": {}, + "aliases": {}, + "root_or_tlr": {}, + "users": {}, + "admins": {}, + "contact_info": {}, + "partner_role": {}, + "partner_type": {}, + "partner_country": {}, + "vulnerability_advisory_locations": {}, + "advisory_location_require_credentials": {}, + "industry": {}, + "tl_root_start_date": {}, + "is_cna_discussion_list": {}, "authority": { "const": ["CNA"] }, @@ -15,7 +32,7 @@ "type": "array", "uniqueItems": true, "items": { - "$ref": "./BaseOrg.json#/definitions/uuidType" + "$ref": "/BaseOrg#/definitions/uuidType" } }, "hard_quota": { @@ -36,7 +53,8 @@ "$ref": "/BaseOrg#/definitions/uriType" } }, + "additionalProperties": false, "required": ["hard_quota"] } ] -} +} \ No newline at end of file diff --git a/src/model/cnaorg.js b/src/model/cnaorg.js index ab17599c9..45f2e0233 100644 --- a/src/model/cnaorg.js +++ b/src/model/cnaorg.js @@ -3,8 +3,8 @@ const BaseOrg = require('./baseorg') const fs = require('fs') const Ajv = require('ajv') const addFormats = require('ajv-formats') -const BaseOrgSchema = JSON.parse(fs.readFileSync('src/middleware/schemas/BaseOrg.json')) -const CnaOrgSchema = JSON.parse(fs.readFileSync('src/middleware/schemas/CNAOrg.json')) +const BaseOrgSchema = JSON.parse(fs.readFileSync('schemas/registry-org/BaseOrg.json')) +const CnaOrgSchema = JSON.parse(fs.readFileSync('schemas/registry-org/CNAOrg.json')) const ajv = new Ajv({ allErrors: true }) addFormats(ajv) ajv.addSchema(BaseOrgSchema) From 35132d7f51a1afac2841690ff75a11f20bd34d4f Mon Sep 17 00:00:00 2001 From: Andrew Foote Date: Tue, 21 Apr 2026 16:55:40 -0400 Subject: [PATCH 02/86] Condensed two schema locations into one, updated references for tests, added new fields to schemas for validation, updated existing tests to handle schema changes, added new tests to handle user/org POST/PUT requests that have erroneus data previously dropped --- .../5.2.0_published_cna_container.json | 0 .../5.2.0_rejected_cna_container.json | 0 .../middleware/schemas => schemas}/Audit.json | 0 .../CVE_JSON_5.2.0_bundled.json | 0 schemas/registry-org/ADPOrg.json | 2 +- schemas/registry-org/BaseOrg.json | 23 ++- schemas/registry-org/BulkDownloadOrg.json | 4 +- schemas/registry-org/CNAOrg.json | 115 ++++++----- schemas/registry-org/SecretariatOrg.json | 4 +- schemas/registry-user/BaseUser.json | 104 ++++++++++ src/constants/index.js | 2 +- .../cve.controller/cve.middleware.js | 4 +- src/middleware/middleware.js | 2 +- src/middleware/schemas/ADPOrg.json | 17 -- src/middleware/schemas/BaseOrg.json | 157 -------------- src/middleware/schemas/BaseUser.json | 72 ------- src/middleware/schemas/BulkDownloadOrg.json | 17 -- src/middleware/schemas/CNAOrg.json | 42 ---- src/middleware/schemas/RegistryUser.json | 10 - src/middleware/schemas/SecretariatOrg.json | 33 --- src/model/adporg.js | 4 +- src/model/audit.js | 2 +- src/model/baseuser.js | 2 +- src/model/bulkdownloadorg.js | 4 +- src/model/cve.js | 2 +- src/model/secretariatorg.js | 4 +- test/integration-tests/constants.js | 15 ++ .../conversation/editConversationTest.js | 5 + test/integration-tests/helpers.js | 6 + test/integration-tests/org/postOrgTest.js | 14 ++ .../integration-tests/org/postOrgUsersTest.js | 22 ++ test/integration-tests/org/registryOrg.js | 36 ++-- .../org/registryOrgAsOrgAdmin.js | 4 + .../org/regularUsersTestRegistry.js | 1 + .../registry-org/createUserByOrgTest.js | 191 ++++++++++-------- .../registry-org/registryOrgCRUDTest.js | 30 +++ test/integration-tests/user/createUserTest.js | 137 +++++++------ .../user/regularUserUpdateTest.js | 1 + test/integration-tests/user/updateUserTest.js | 36 +++- 39 files changed, 544 insertions(+), 580 deletions(-) rename {src/middleware/schemas => schemas}/5.2.0_published_cna_container.json (100%) rename {src/middleware/schemas => schemas}/5.2.0_rejected_cna_container.json (100%) rename {src/middleware/schemas => schemas}/Audit.json (100%) rename {src/middleware/schemas => schemas}/CVE_JSON_5.2.0_bundled.json (100%) create mode 100644 schemas/registry-user/BaseUser.json delete mode 100644 src/middleware/schemas/ADPOrg.json delete mode 100644 src/middleware/schemas/BaseOrg.json delete mode 100644 src/middleware/schemas/BaseUser.json delete mode 100644 src/middleware/schemas/BulkDownloadOrg.json delete mode 100644 src/middleware/schemas/CNAOrg.json delete mode 100644 src/middleware/schemas/RegistryUser.json delete mode 100644 src/middleware/schemas/SecretariatOrg.json diff --git a/src/middleware/schemas/5.2.0_published_cna_container.json b/schemas/5.2.0_published_cna_container.json similarity index 100% rename from src/middleware/schemas/5.2.0_published_cna_container.json rename to schemas/5.2.0_published_cna_container.json diff --git a/src/middleware/schemas/5.2.0_rejected_cna_container.json b/schemas/5.2.0_rejected_cna_container.json similarity index 100% rename from src/middleware/schemas/5.2.0_rejected_cna_container.json rename to schemas/5.2.0_rejected_cna_container.json diff --git a/src/middleware/schemas/Audit.json b/schemas/Audit.json similarity index 100% rename from src/middleware/schemas/Audit.json rename to schemas/Audit.json diff --git a/src/middleware/schemas/CVE_JSON_5.2.0_bundled.json b/schemas/CVE_JSON_5.2.0_bundled.json similarity index 100% rename from src/middleware/schemas/CVE_JSON_5.2.0_bundled.json rename to schemas/CVE_JSON_5.2.0_bundled.json diff --git a/schemas/registry-org/ADPOrg.json b/schemas/registry-org/ADPOrg.json index be9829003..7979d1f55 100644 --- a/schemas/registry-org/ADPOrg.json +++ b/schemas/registry-org/ADPOrg.json @@ -5,7 +5,7 @@ "title": "CVE ADP Organization", "description": "Schema for a CVE ADP Organization", "allOf": [ - { "$ref": "./BaseOrg.json" }, + { "$ref": "/BaseOrg" }, { "properties": { "authority": { diff --git a/schemas/registry-org/BaseOrg.json b/schemas/registry-org/BaseOrg.json index d3c4a45a3..d2a42bbf5 100644 --- a/schemas/registry-org/BaseOrg.json +++ b/schemas/registry-org/BaseOrg.json @@ -4,6 +4,7 @@ "type": "object", "title": "CVE Base Organization", "description": "Base schema for a CVE Organization", + "additionalProperties": false, "definitions": { "uuidType": { "description": "A version 4 (random) universally unique identifier (UUID) as defined by [RFC 4122](https://tools.ietf.org/html/rfc4122#section-4.1.3).", @@ -34,7 +35,12 @@ "authority": { "description": "The authority (role) of this organization within the CVE program", "type": "string", - "enum": ["CNA", "SECRETARIAT", "BULK_DOWNLOAD", "ADP"] + "enum": [ + "CNA", + "SECRETARIAT", + "BULK_DOWNLOAD", + "ADP" + ] } }, "properties": { @@ -47,6 +53,9 @@ "long_name": { "$ref": "#/definitions/longName" }, + "new_short_name": { + "$ref": "#/definitions/shortName" + }, "aliases": { "type": "array", "uniqueItems": true, @@ -81,6 +90,18 @@ "$ref": "#/definitions/uuidType" } }, + "hard_quota": { + "description": "The maximum number of CVE IDs this organization can reserve.", + "type": "integer", + "minimum": 0, + "maximum": 100000 + }, + "soft_quota": { + "description": "The threshold for notifying the organization about their remaining CVE ID count.", + "type": "integer", + "minimum": 0, + "maximum": 100000 + }, "contact_info": { "type": "object", "properties": { diff --git a/schemas/registry-org/BulkDownloadOrg.json b/schemas/registry-org/BulkDownloadOrg.json index cabc0777a..526626f17 100644 --- a/schemas/registry-org/BulkDownloadOrg.json +++ b/schemas/registry-org/BulkDownloadOrg.json @@ -1,11 +1,11 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "BaseOrg", + "$id": "BulkDownloadOrg", "type": "object", "title": "CVE Bulk Download Organization", "description": "Schema for a CVE Bulk Download Organization", "allOf": [ - { "$ref": "./BaseOrg.json" }, + { "$ref": "/BaseOrg" }, { "properties": { "authority": { diff --git a/schemas/registry-org/CNAOrg.json b/schemas/registry-org/CNAOrg.json index 32a4bbcc9..367302530 100644 --- a/schemas/registry-org/CNAOrg.json +++ b/schemas/registry-org/CNAOrg.json @@ -1,60 +1,75 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", "$id": "CNAOrg", + "type": "object", "title": "CVE CNA Organization", "description": "Schema for a CVE CNA Organization", - "allOf": [ - { "$ref": "/BaseOrg" }, - { + "additionalProperties": false, + "properties": { + "UUID": { "$ref": "/BaseOrg#/definitions/uuidType" }, + "short_name": { "$ref": "/BaseOrg#/definitions/shortName" }, + "long_name": { "$ref": "/BaseOrg#/definitions/longName" }, + "new_short_name": { + "description": "Used to rename an organization's short name during an update.", + "type": "string", + "minLength": 2, + "maxLength": 32 + }, + "contact_info": { + "type": "object", "properties": { - "UUID": {}, - "__t": {}, - "short_name": {}, - "long_name": {}, - "aliases": {}, - "root_or_tlr": {}, - "users": {}, - "admins": {}, - "contact_info": {}, - "partner_role": {}, - "partner_type": {}, - "partner_country": {}, - "vulnerability_advisory_locations": {}, - "advisory_location_require_credentials": {}, - "industry": {}, - "tl_root_start_date": {}, - "is_cna_discussion_list": {}, - "authority": { - "const": ["CNA"] - }, - "oversees": { - "type": "array", - "uniqueItems": true, - "items": { - "$ref": "/BaseOrg#/definitions/uuidType" - } - }, - "hard_quota": { - "type": "integer", - "minimum": 0 - }, - "soft_quota": { + "poc": { "type": "string" }, + "poc_email": { "type": "string" }, + "poc_phone": { "type": "string" }, + "org_email": { "type": "string" }, + "website": { "type": "string" } + }, + "additionalProperties": false + }, + "authority": { + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "/BaseOrg#/definitions/authority" + } + }, + "policies": { + "type": "object", + "properties": { + "id_quota": { "type": "integer", - "minimum": 0 - }, - "charter_or_scope": { - "$ref": "/BaseOrg#/definitions/uriType" - }, - "disclosure_policy": { - "$ref": "/BaseOrg#/definitions/uriType" - }, - "product_list": { - "$ref": "/BaseOrg#/definitions/uriType" + "minimum": 0, + "maximum": 100000 } - }, - "additionalProperties": false, - "required": ["hard_quota"] + } + }, + "hard_quota": { + "type": "integer", + "minimum": 0, + "maximum": 100000 + }, + "soft_quota": { + "type": "integer", + "minimum": 0, + "maximum": 100000 + }, + "oversees": { + "type": "array", + "uniqueItems": true, + "items": { + "type": "string", + "format": "uuid" + } + }, + "partner_role": { + "type": "string" + }, + "partner_type": { + "type": "string" + }, + "partner_country": { + "type": "string" } - ] + }, + "required": ["short_name", "hard_quota"] } \ No newline at end of file diff --git a/schemas/registry-org/SecretariatOrg.json b/schemas/registry-org/SecretariatOrg.json index 469bd7df5..4e658b571 100644 --- a/schemas/registry-org/SecretariatOrg.json +++ b/schemas/registry-org/SecretariatOrg.json @@ -5,7 +5,7 @@ "title": "CVE Secretariat Organization", "description": "Schema for a CVE Secretariat Organization", "allOf": [ - { "$ref": "./BaseOrg.json" }, + { "$ref": "/BaseOrg" }, { "properties": { "authority": { @@ -15,7 +15,7 @@ "type": "array", "uniqueItems": true, "items": { - "$ref": "./BaseOrg.json#/definitions/uuidType" + "$ref": "/BaseOrg#/definitions/uuidType" } }, "hard_quota": { diff --git a/schemas/registry-user/BaseUser.json b/schemas/registry-user/BaseUser.json new file mode 100644 index 000000000..5fa85687e --- /dev/null +++ b/schemas/registry-user/BaseUser.json @@ -0,0 +1,104 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "/BaseUser", + "type": "object", + "title": "CVE Base User Schema", + "additionalProperties": false, + "description": "The schema for CVE Services Users", + "definitions": { + "uuidType": { + "description": "A version 4 (random) universally unique identifier (UUID) as defined by [RFC 4122](https://tools.ietf.org/html/rfc4122#section-4.1.3).", + "type": "string", + "format": "uuid", + "pattern": "^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$" + }, + "name": { + "description": "User's name components", + "type": "object", + "required": [ + "first", + "last" + ], + "properties": { + "first": { + "type": "string", + "maxLength": 100 + }, + "middle": { + "type": "string", + "maxLength": 100 + }, + "last": { + "type": "string", + "maxLength": 100 + }, + "suffix": { + "type": "string", + "maxLength": 100 + } + }, + "additionalProperties": false + } + }, + "properties": { + "name": { + "$ref": "#/definitions/name" + }, + "username": { + "description": "Username should be 3-128 characters. Allowed characters are alphanumeric and -_@.", + "type": "string", + "minLength": 3, + "maxLength": 128, + "pattern": "^[A-Za-z0-9\\-_@.]{3,128}$" + }, + "active": { + "description": "Whether the user account is active. Supports boolean or string based on legacy test constants.", + "type": [ + "boolean", + "string" + ] + }, + "authority": { + "description": "The user's authority and roles, often used in joint review contexts.", + "type": "object", + "properties": { + "active_roles": { + "type": "array", + "uniqueItems": true, + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "secret": { + "description": "Hashed secret for user authentication", + "type": "string" + }, + "UUID": { + "$ref": "#/definitions/uuidType" + }, + "status": { + "description": "User status: 'active' or 'inactive'", + "type": "string", + "enum": [ + "active", + "inactive" + ] + }, + "role": { + "description": "The user's role in the organization", + "type": "string" + }, + "org_short_name": { + "description": "Used to update the organization association of a user", + "type": "string", + "minLength": 2, + "maxLength": 32 + } + }, + "required": [ + "username" + ] +} \ No newline at end of file diff --git a/src/constants/index.js b/src/constants/index.js index a4c73c910..69f295e9f 100644 --- a/src/constants/index.js +++ b/src/constants/index.js @@ -1,5 +1,5 @@ const fs = require('fs') -const cveSchemaV5 = JSON.parse(fs.readFileSync('src/middleware/schemas/CVE_JSON_5.2.0_bundled.json')) +const cveSchemaV5 = JSON.parse(fs.readFileSync('schemas/CVE_JSON_5.2.0_bundled.json')) /** * Return default values. diff --git a/src/controller/cve.controller/cve.middleware.js b/src/controller/cve.controller/cve.middleware.js index 90b5a64e7..576bdad3f 100644 --- a/src/controller/cve.controller/cve.middleware.js +++ b/src/controller/cve.controller/cve.middleware.js @@ -4,8 +4,8 @@ const errors = require('./error') const error = new errors.CveControllerError() const utils = require('../../utils/utils') const fs = require('fs') -const RejectedSchema = JSON.parse(fs.readFileSync('src/middleware/schemas/5.2.0_rejected_cna_container.json')) -const cnaContainerSchema = JSON.parse(fs.readFileSync('src/middleware/schemas/5.2.0_published_cna_container.json')) +const RejectedSchema = JSON.parse(fs.readFileSync('schemas/5.2.0_rejected_cna_container.json')) +const cnaContainerSchema = JSON.parse(fs.readFileSync('schemas/5.2.0_published_cna_container.json')) const logger = require('../../middleware/logger') const Ajv = require('ajv') const addFormats = require('ajv-formats') diff --git a/src/middleware/middleware.js b/src/middleware/middleware.js index 67e47e7b8..6cd272531 100644 --- a/src/middleware/middleware.js +++ b/src/middleware/middleware.js @@ -1,6 +1,6 @@ const getConstants = require('../constants').getConstants const fs = require('fs') -const cveSchemaV5 = JSON.parse(fs.readFileSync('src/middleware/schemas/CVE_JSON_5.2.0_bundled.json')) +const cveSchemaV5 = JSON.parse(fs.readFileSync('schemas/CVE_JSON_5.2.0_bundled.json')) const argon2 = require('argon2') const logger = require('./logger') const Ajv = require('ajv') diff --git a/src/middleware/schemas/ADPOrg.json b/src/middleware/schemas/ADPOrg.json deleted file mode 100644 index 7979d1f55..000000000 --- a/src/middleware/schemas/ADPOrg.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "ADPOrg", - "type": "object", - "title": "CVE ADP Organization", - "description": "Schema for a CVE ADP Organization", - "allOf": [ - { "$ref": "/BaseOrg" }, - { - "properties": { - "authority": { - "const": ["ADP"] - } - } - } - ] -} diff --git a/src/middleware/schemas/BaseOrg.json b/src/middleware/schemas/BaseOrg.json deleted file mode 100644 index f7039bcca..000000000 --- a/src/middleware/schemas/BaseOrg.json +++ /dev/null @@ -1,157 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "/BaseOrg", - "type": "object", - "title": "CVE Base Organization", - "description": "Base schema for a CVE Organization", - "definitions": { - "uuidType": { - "description": "A version 4 (random) universally unique identifier (UUID) as defined by [RFC 4122](https://tools.ietf.org/html/rfc4122#section-4.1.3).", - "type": "string", - "format": "uuid", - "pattern": "^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$" - }, - "uriType": { - "description": "A universal resource identifier (URI), according to [RFC 3986](https://tools.ietf.org/html/rfc3986).", - "type": "string", - "format": "uri", - "pattern": "^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\\?([^#]*))?(#(.*))?", - "minLength": 1, - "maxLength": 2048 - }, - "shortName": { - "description": "A 2-32 character name that can be used to complement an organization's UUID.", - "type": "string", - "minLength": 2, - "maxLength": 32 - }, - "longName": { - "description": "A 1-256 character name that can be used to complement an organization's short_name.", - "type": "string", - "minLength": 1, - "maxLength": 256 - }, - "authority": { - "description": "The authority (role) of this organization within the CVE program", - "type": "string", - "enum": ["CNA", "SECRETARIAT", "BULK_DOWNLOAD", "ADP"] - }, - "discriminator": { - "description": "Discriminator key used by Mongoose for type inheritance", - "type": "string" - }, - "timestamp": { - "description": "Date/time format based on RFC3339 and ISO ISO8601, with an optional timezone in the format 'yyyy-MM-ddTHH:mm:ss[+-]ZH:ZM'. If timezone offset is not given, GMT (+00:00) is assumed.", - "pattern": "^(((2000|2400|2800|(19|2[0-9](0[48]|[2468][048]|[13579][26])))-02-29)|(((19|2[0-9])[0-9]{2})-02-(0[1-9]|1[0-9]|2[0-8]))|(((19|2[0-9])[0-9]{2})-(0[13578]|10|12)-(0[1-9]|[12][0-9]|3[01]))|(((19|2[0-9])[0-9]{2})-(0[469]|11)-(0[1-9]|[12][0-9]|30)))T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})?$", - "type": "string" - } - }, - "properties": { - "UUID": { - "$ref": "#/definitions/uuidType" - }, - "__t": { - "$ref": "#/definitions/discriminator" - }, - "short_name": { - "$ref": "#/definitions/shortName" - }, - "long_name": { - "$ref": "#/definitions/longName" - }, - "aliases": { - "type": "array", - "uniqueItems": true, - "items": { - "type": "string" - } - }, - "authority": { - "type": "array", - "uniqueItems": true, - "items": { - "$ref": "#/definitions/authority" - } - }, - "root_or_tlr": { - "type": "boolean" - }, - "users": { - "type": "array", - "uniqueItems": true, - "items": { - "$ref": "#/definitions/uuidType" - } - }, - "admins": { - "type": "array", - "uniqueItems": true, - "items": { - "$ref": "#/definitions/uuidType" - } - }, - "contact_info": { - "type": "object", - "properties": { - "additional_contact_users": { - "type": "array", - "uniqueItems": true, - "items": { - "$ref": "#/definitions/uuidType" - } - }, - "poc": { - "type": "string" - }, - "poc_email": { - "type": "string", - "format": "email" - }, - "poc_phone": { - "type": "string" - }, - "org_email": { - "type": "string", - "format": "email" - }, - "website": { - "$ref": "#/definitions/uriType", - "pattern": "^(ftp|http)s?://\\S+$" - } - }, - "additionalProperties": false - }, - "partner_role": { - "type": "string" - }, - "partner_type": { - "type": "string" - }, - "partner_country": { - "type": "string" - }, - "vulnerability_advisory_locations": { - "type": "array", - "uniqueItems": true, - "items": { - "type": "string" - } - }, - "advisory_location_require_credentials": { - "type": "boolean" - }, - "industry": { - "type": "string" - }, - "tl_root_start_date": { - "$ref": "#/definitions/timestamp" - }, - "is_cna_discussion_list": { - "type": "boolean" - } - }, - "required": [ - "short_name", - "long_name" - ] -} \ No newline at end of file diff --git a/src/middleware/schemas/BaseUser.json b/src/middleware/schemas/BaseUser.json deleted file mode 100644 index 2c1bf93de..000000000 --- a/src/middleware/schemas/BaseUser.json +++ /dev/null @@ -1,72 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "/BaseUser", - "type": "object", - "title": "CVE Base User Schema", - "description": "The schema for CVE Services Users", - "definitions": { - "uuidType": { - "description": "A version 4 (random) universally unique identifier (UUID) as defined by [RFC 4122](https://tools.ietf.org/html/rfc4122#section-4.1.3).", - "type": "string", - "format": "uuid", - "pattern": "^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$" - }, - "name": { - "description": "User's name components", - "type": "object", - "required": [ - "first", - "last" - ], - "properties": { - "first": { - "type": "string", - "maxLength": 100 - }, - "middle": { - "type": "string", - "maxLength": 100 - }, - "last": { - "type": "string", - "maxLength": 100 - }, - "suffix": { - "type": "string", - "maxLength": 100 - } - }, - "additionalProperties": false - } - }, - "properties": { - "name": { - "$ref": "#/definitions/name" - }, - "username": { - "description": "Username should be 3-128 characters. Allowed characters are alphanumeric and -_@.", - "type": "string", - "minLength": 3, - "maxLength": 128, - "pattern": "^[A-Za-z0-9\\-_@.]{3,128}$" - }, - "secret": { - "description": "Hashed secret for user authentication", - "type": "string" - }, - "UUID": { - "$ref": "#/definitions/uuidType" - }, - "status": { - "description": "User status: 'active' or 'inactive'", - "type": "string", - "enum": [ - "active", - "inactive" - ] - } - }, - "required": [ - "username" - ] -} \ No newline at end of file diff --git a/src/middleware/schemas/BulkDownloadOrg.json b/src/middleware/schemas/BulkDownloadOrg.json deleted file mode 100644 index ada140853..000000000 --- a/src/middleware/schemas/BulkDownloadOrg.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "BaseOrg", - "type": "object", - "title": "CVE Bulk Download Organization", - "description": "Schema for a CVE Bulk Download Organization", - "allOf": [ - { "$ref": "/BaseOrg" }, - { - "properties": { - "authority": { - "const": ["BULK_DOWNLOAD"] - } - } - } - ] -} diff --git a/src/middleware/schemas/CNAOrg.json b/src/middleware/schemas/CNAOrg.json deleted file mode 100644 index c1188c8c4..000000000 --- a/src/middleware/schemas/CNAOrg.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "$id": "CNAOrg", - "title": "CVE CNA Organization", - "description": "Schema for a CVE CNA Organization", - "allOf": [ - { "$ref": "/BaseOrg" }, - { - "properties": { - "authority": { - "const": ["CNA"] - }, - "oversees": { - "type": "array", - "uniqueItems": true, - "items": { - "$ref": "/BaseOrg#/definitions/uuidType" - } - }, - "hard_quota": { - "type": "integer", - "minimum": 0 - }, - "soft_quota": { - "type": "integer", - "minimum": 0 - }, - "charter_or_scope": { - "$ref": "/BaseOrg#/definitions/uriType" - }, - "disclosure_policy": { - "$ref": "/BaseOrg#/definitions/uriType" - }, - "product_list": { - "$ref": "/BaseOrg#/definitions/uriType" - } - }, - "required": ["hard_quota"] - } - ] -} diff --git a/src/middleware/schemas/RegistryUser.json b/src/middleware/schemas/RegistryUser.json deleted file mode 100644 index de95595ab..000000000 --- a/src/middleware/schemas/RegistryUser.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "$id": "RegistryUser", - "title": "CVE Registry User Schema", - "description": "Schema for a CVE Registry User", - "allOf": [ - { "$ref": "/BaseUser" } - ] -} diff --git a/src/middleware/schemas/SecretariatOrg.json b/src/middleware/schemas/SecretariatOrg.json deleted file mode 100644 index 4e658b571..000000000 --- a/src/middleware/schemas/SecretariatOrg.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "SecretariatOrg", - "type": "object", - "title": "CVE Secretariat Organization", - "description": "Schema for a CVE Secretariat Organization", - "allOf": [ - { "$ref": "/BaseOrg" }, - { - "properties": { - "authority": { - "const": ["SECRETARIAT"] - }, - "oversees": { - "type": "array", - "uniqueItems": true, - "items": { - "$ref": "/BaseOrg#/definitions/uuidType" - } - }, - "hard_quota": { - "type": "integer", - "minimum": 0 - }, - "soft_quota": { - "type": "integer", - "minimum": 0 - } - }, - "required": ["hard_quota"] - } - ] -} diff --git a/src/model/adporg.js b/src/model/adporg.js index f5efa867c..0c5a80799 100644 --- a/src/model/adporg.js +++ b/src/model/adporg.js @@ -3,8 +3,8 @@ const BaseOrg = require('./baseorg') const fs = require('fs') const Ajv = require('ajv') const addFormats = require('ajv-formats') -const BaseOrgSchema = JSON.parse(fs.readFileSync('src/middleware/schemas/BaseOrg.json')) -const AdpOrgSchema = JSON.parse(fs.readFileSync('src/middleware/schemas/ADPOrg.json')) +const BaseOrgSchema = JSON.parse(fs.readFileSync('schemas/registry-org/BaseOrg.json')) +const AdpOrgSchema = JSON.parse(fs.readFileSync('schemas/registry-org/ADPOrg.json')) const ajv = new Ajv({ allErrors: true }) addFormats(ajv) ajv.addSchema(BaseOrgSchema) diff --git a/src/model/audit.js b/src/model/audit.js index 9d346566c..d749b691d 100644 --- a/src/model/audit.js +++ b/src/model/audit.js @@ -4,7 +4,7 @@ const aggregatePaginate = require('mongoose-aggregate-paginate-v2') const MongoPaging = require('mongo-cursor-pagination') const Ajv = require('ajv') const addFormats = require('ajv-formats') -const AuditSchemaJSON = JSON.parse(fs.readFileSync('src/middleware/schemas/Audit.json')) +const AuditSchemaJSON = JSON.parse(fs.readFileSync('schemas/Audit.json')) // Initialize AJV const ajv = new Ajv({ allErrors: true }) diff --git a/src/model/baseuser.js b/src/model/baseuser.js index 260179970..fcd4b1777 100644 --- a/src/model/baseuser.js +++ b/src/model/baseuser.js @@ -6,7 +6,7 @@ const Ajv = require('ajv') const addFormats = require('ajv-formats') // Load BaseUser JSON schema -const BaseUserSchemaJSON = JSON.parse(fs.readFileSync('src/middleware/schemas/BaseUser.json')) +const BaseUserSchemaJSON = JSON.parse(fs.readFileSync('schemas/registry-user/BaseUser.json')) // Initialize AJV const ajv = new Ajv({ allErrors: true }) diff --git a/src/model/bulkdownloadorg.js b/src/model/bulkdownloadorg.js index e196b5ff3..cdf06cf9d 100644 --- a/src/model/bulkdownloadorg.js +++ b/src/model/bulkdownloadorg.js @@ -3,8 +3,8 @@ const BaseOrg = require('./baseorg') const fs = require('fs') const Ajv = require('ajv') const addFormats = require('ajv-formats') -const BaseOrgSchema = JSON.parse(fs.readFileSync('src/middleware/schemas/BaseOrg.json')) -const BulkDownloadOrgSchema = JSON.parse(fs.readFileSync('src/middleware/schemas/BulkDownloadOrg.json')) +const BaseOrgSchema = JSON.parse(fs.readFileSync('schemas/registry-org/BaseOrg.json')) +const BulkDownloadOrgSchema = JSON.parse(fs.readFileSync('schemas/registry-org/BulkDownloadOrg.json')) const ajv = new Ajv({ allErrors: true }) addFormats(ajv) ajv.addSchema(BaseOrgSchema) diff --git a/src/model/cve.js b/src/model/cve.js index 59e23aeee..81f1eee87 100644 --- a/src/model/cve.js +++ b/src/model/cve.js @@ -2,7 +2,7 @@ const mongoose = require('mongoose') const aggregatePaginate = require('mongoose-aggregate-paginate-v2') const MongoPaging = require('mongo-cursor-pagination') const fs = require('fs') -const cveSchemaV5 = JSON.parse(fs.readFileSync('src/middleware/schemas/CVE_JSON_5.2.0_bundled.json')) +const cveSchemaV5 = JSON.parse(fs.readFileSync('schemas/CVE_JSON_5.2.0_bundled.json')) const Ajv = require('ajv') const addFormats = require('ajv-formats') diff --git a/src/model/secretariatorg.js b/src/model/secretariatorg.js index 127d236a6..073f1c9d7 100644 --- a/src/model/secretariatorg.js +++ b/src/model/secretariatorg.js @@ -3,8 +3,8 @@ const BaseOrg = require('./baseorg') const fs = require('fs') const Ajv = require('ajv') const addFormats = require('ajv-formats') -const BaseOrgSchema = JSON.parse(fs.readFileSync('src/middleware/schemas/BaseOrg.json')) -const SecretariatOrgSchema = JSON.parse(fs.readFileSync('src/middleware/schemas/SecretariatOrg.json')) +const BaseOrgSchema = JSON.parse(fs.readFileSync('schemas/registry-org/BaseOrg.json')) +const SecretariatOrgSchema = JSON.parse(fs.readFileSync('schemas/registry-org/SecretariatOrg.json')) const ajv = new Ajv({ allErrors: true }) addFormats(ajv) ajv.addSchema(BaseOrgSchema) diff --git a/test/integration-tests/constants.js b/test/integration-tests/constants.js index de4946825..80700e31e 100644 --- a/test/integration-tests/constants.js +++ b/test/integration-tests/constants.js @@ -363,6 +363,20 @@ const testOrg = { } } +const testOrg2 = { + + short_name: 'test_org2', + name: 'Test Organization 2', + authority: { + active_roles: [ + 'CNA' + ] + }, + policies: { + id_quota: 100000 + } +} + const testRegistryOrg = { short_name: 'test_registry_org', long_name: 'Test Registry Organization', @@ -432,6 +446,7 @@ module.exports = { testAdp, testAdp2, testOrg, + testOrg2, testRegistryOrg, testRegistryOrg2, existingOrg, diff --git a/test/integration-tests/conversation/editConversationTest.js b/test/integration-tests/conversation/editConversationTest.js index 7adff82e6..ced1fb59e 100644 --- a/test/integration-tests/conversation/editConversationTest.js +++ b/test/integration-tests/conversation/editConversationTest.js @@ -29,6 +29,11 @@ describe('Testing Conversation endpoints', () => { expect(err).to.be.undefined expect(res).to.have.status(200) org = res.body + delete org.created + delete org.last_updated + delete org.admins + delete org.users + delete org.root_or_tlr }) await chai diff --git a/test/integration-tests/helpers.js b/test/integration-tests/helpers.js index 2b8685d4e..de16ccfd8 100644 --- a/test/integration-tests/helpers.js +++ b/test/integration-tests/helpers.js @@ -122,6 +122,9 @@ async function userDeactivateAsSecHelper (userName, orgShortName) { .set(constants.headers) .then(res => res.body) + delete user.created + delete user.last_updated + delete user.created_by await chai.request(app) .put(`/api/registry/org/${orgShortName}/user/${userName}`) .set(constants.headers) @@ -139,6 +142,9 @@ async function userReactivateAsSecHelper (userName, orgShortName) { .then(res => res.body) user.status = 'active' + delete user.created + delete user.last_updated + delete user.created_by await chai.request(app) .put(`/api/registry/org/${orgShortName}/user/${userName}`) diff --git a/test/integration-tests/org/postOrgTest.js b/test/integration-tests/org/postOrgTest.js index fbc8b8dde..a94245fd6 100644 --- a/test/integration-tests/org/postOrgTest.js +++ b/test/integration-tests/org/postOrgTest.js @@ -97,5 +97,19 @@ describe('Testing Org post endpoint', () => { expect(res.body.error).to.equal('ORG_EXISTS') }) }) + it('Should fail to create an org with an erroneous key not found in the schema with registry enabled', async () => { + await chai.request(app) + .post('/api/registry/org') + .set({ ...constants.headers }) + .send({ + ...constants.testRegistryOrg, + test: 'additional key not in schema' + }) + .then((res, err) => { + expect(res).to.have.status(400) + expect(res.body.message).to.equal('Parameters were invalid') + expect(res.body.errors[0].message).to.equal('must NOT have additional properties') + }) + }) }) }) diff --git a/test/integration-tests/org/postOrgUsersTest.js b/test/integration-tests/org/postOrgUsersTest.js index 098763b1b..d952c7237 100644 --- a/test/integration-tests/org/postOrgUsersTest.js +++ b/test/integration-tests/org/postOrgUsersTest.js @@ -332,5 +332,27 @@ describe('Testing user post endpoint', () => { ) }) }) + it('Fails creation of user with registry enabled and an erroneous key not found in the schema', async () => { + await chai + .request(app) + .post('/api/registry/org/mitre/user') + .set({ ...constants.headers, ...shortName }) + .send({ + username: 'fakeregistryuser1002', + name: { + first: 'FirstName', + last: 'LastName', + middle: 'MiddleName', + suffix: 'Suffix' + }, + role: 'ADMIN', + test: 'additional key not in schema' + }) + .then((res) => { + expect(res).to.have.status(400) + expect(res.body.message).to.equal('Parameters were invalid') + expect(res.body.errors[0].message).to.equal('must NOT have additional properties') + }) + }) }) }) diff --git a/test/integration-tests/org/registryOrg.js b/test/integration-tests/org/registryOrg.js index 9328f32dc..0dec2824e 100644 --- a/test/integration-tests/org/registryOrg.js +++ b/test/integration-tests/org/registryOrg.js @@ -157,21 +157,6 @@ describe('Testing Secretariat functionality for Orgs', () => { }) }) - it('A new user is created even if extra data is in the body', async () => { - const username = uuidv4() - await chai.request(app) - .post('/api/registry/org/mitre/user') - .set(secretariatHeaders) - .send({ - username, - ubiquitous: 'mendacious' - }) - .then((res) => { - expect(res).to.have.status(200) - expect(res.body.message).to.equal(`${username} was successfully created.`) - }) - }) - it('A users username can be updated', async function () { const { orgShortName, username } = await createNewUserWithNewOrg() const newUsername = uuidv4() @@ -179,6 +164,8 @@ describe('Testing Secretariat functionality for Orgs', () => { await chai.request(app).get(`/api/registry/org/${orgShortName}/user/${username}`).set(secretariatHeaders).then((res) => { user = res.body }) + delete user.created + delete user.last_updated await chai.request(app) .put(`/api/registry/org/${orgShortName}/user/${username}`) .set(secretariatHeaders) @@ -209,6 +196,8 @@ describe('Testing Secretariat functionality for Orgs', () => { let user await chai.request(app).get(`/api/registry/org/${orgShortName}/user/${username}`).set(secretariatHeaders).then((res) => { user = res.body }) + delete user.created + delete user.last_updated await chai.request(app) .put(`/api/registry/org/${orgShortName}/user/${username}`) .set(secretariatHeaders) @@ -244,6 +233,8 @@ describe('Testing Secretariat functionality for Orgs', () => { await chai.request(app).get(`/api/registry/org/${orgShortName}/user/${username}`).set(secretariatHeaders).then((res) => { user = res.body }) + delete user.created + delete user.last_updated await chai.request(app) .put(`/api/registry/org/${orgShortName}/user/${username}`) .set(secretariatHeaders) @@ -319,6 +310,21 @@ describe('Testing Secretariat functionality for Orgs', () => { }) context('Negative Tests', () => { + it('A new user is not created if extra data is in the body', async () => { + const username = uuidv4() + await chai.request(app) + .post('/api/registry/org/mitre/user') + .set(secretariatHeaders) + .send({ + username, + ubiquitous: 'mendacious' + }) + .then((res) => { + expect(res).to.have.status(400) + expect(res.body.message).to.equal('Parameters were invalid') + }) + }) + it('Should not retrieve an org for a non-existent UUID', async () => { const nonExistentUUID = 'nonexistent123' await chai.request(app) diff --git a/test/integration-tests/org/registryOrgAsOrgAdmin.js b/test/integration-tests/org/registryOrgAsOrgAdmin.js index d248215dd..ef2356bce 100644 --- a/test/integration-tests/org/registryOrgAsOrgAdmin.js +++ b/test/integration-tests/org/registryOrgAsOrgAdmin.js @@ -282,6 +282,10 @@ describe('Testing Registry Org as org admin', () => { it('Registry: Services api prevents org admins from updating a users username if that user already exists', async () => { let user await chai.request(app).get('/api/registry/org/beat_10/user/patriciawilliams@beat_10.com').set(adminHeaders).then((res) => { user = res.body }) + + delete user.created + delete user.last_updated + delete user.created_by await chai.request(app) .put('/api/registry/org/beat_10/user/patriciawilliams@beat_10.com') .set(adminHeaders) diff --git a/test/integration-tests/org/regularUsersTestRegistry.js b/test/integration-tests/org/regularUsersTestRegistry.js index 348df2336..b67aee73f 100644 --- a/test/integration-tests/org/regularUsersTestRegistry.js +++ b/test/integration-tests/org/regularUsersTestRegistry.js @@ -24,6 +24,7 @@ describe('Testing regular user permissions for /api/registry/org/ endpoints with .set(constants.nonSecretariatUserHeaders) .then((res) => { previousBody = res.body }) + delete previousBody.created_by await chai.request(app) .put(`/api/registry/org/${org}/user/${user}`) .set(constants.nonSecretariatUserHeaders) diff --git a/test/integration-tests/registry-org/createUserByOrgTest.js b/test/integration-tests/registry-org/createUserByOrgTest.js index 9397eb8db..3cba5a152 100644 --- a/test/integration-tests/registry-org/createUserByOrgTest.js +++ b/test/integration-tests/registry-org/createUserByOrgTest.js @@ -9,98 +9,111 @@ const constants = require('../constants.js') const app = require('../../../src/index.js') describe('Testing POST /api/registryOrg/:shortname/user endpoint', () => { - // Positive test - it('Should create a new user in an organization', (done) => { - const orgShortName = 'mitre' - const newUser = { - username: 'testuser@example.com', - name: { - first: 'Test', - last: 'User' - }, - role: 'ADMIN' - } - - chai.request(app) - .post(`/api/registryOrg/${orgShortName}/user`) - .set(constants.headers) - .send(newUser) - .end((err, res) => { - expect(err).to.be.null - expect(res).to.have.status(200) - expect(res.body).to.have.property('message').equal(`${newUser.username} was successfully created.`) - expect(res.body).to.have.property('created') - expect(res.body.created).to.have.property('username', newUser.username) - expect(res.body.created).to.have.property('secret') - done() - }) - }) - - // Negative test: Organization does not exist - it('Should not create a user in a non-existent organization', (done) => { - const orgShortName = 'nonexistentorg' - const newUser = { - username: 'testuser2@example.com', - name: { - first: 'Test', - last: 'User' + context('Positive Tests', () => { + it('Should create a new user in an organization', (done) => { + const orgShortName = 'mitre' + const newUser = { + username: 'testuser@example.com', + name: { + first: 'Test', + last: 'User' + }, + role: 'ADMIN' } - } - - chai.request(app) - .post(`/api/registryOrg/${orgShortName}/user`) - .set(constants.headers) - .send(newUser) - .end((err, res) => { - expect(err).to.be.null - expect(res).to.have.status(404) - expect(res.body).to.have.property('message').equal(`The '${orgShortName}' organization designated by the shortname path parameter does not exist.`) - done() - }) + chai.request(app) + .post(`/api/registryOrg/${orgShortName}/user`) + .set(constants.headers) + .send(newUser) + .end((err, res) => { + expect(err).to.be.null + expect(res).to.have.status(200) + expect(res.body).to.have.property('message').equal(`${newUser.username} was successfully created.`) + expect(res.body).to.have.property('created') + expect(res.body.created).to.have.property('username', newUser.username) + expect(res.body.created).to.have.property('secret') + done() + }) + }) }) - - // Negative test: User already exists - it('Should not create a user that already exists', (done) => { - const orgShortName = 'mitre' - const existingUser = { - username: 'testuser@example.com', - name: { - first: 'Test', - last: 'User' + context('Negative Tests', () => { + it('Should not create a user in a non-existent organization', (done) => { + const orgShortName = 'nonexistentorg' + const newUser = { + username: 'testuser2@example.com', + name: { + first: 'Test', + last: 'User' + } } - } - - chai.request(app) - .post(`/api/registryOrg/${orgShortName}/user`) - .set(constants.headers) - .send(existingUser) - .end((err, res) => { - expect(err).to.be.null - expect(res).to.have.status(400) - expect(res.body).to.have.property('message').equal(`The user '${existingUser.username}' already exists.`) - done() - }) - }) - - // Negative test: Validation error (missing username) - it('Should not create a user with a missing username', (done) => { - const orgShortName = 'mitre' - const invalidUser = { - name: { - first: 'Test', - last: 'User' + chai.request(app) + .post(`/api/registryOrg/${orgShortName}/user`) + .set(constants.headers) + .send(newUser) + .end((err, res) => { + expect(err).to.be.null + expect(res).to.have.status(404) + expect(res.body).to.have.property('message').equal(`The '${orgShortName}' organization designated by the shortname path parameter does not exist.`) + done() + }) + }) + it('Should not create a user that already exists', (done) => { + const orgShortName = 'mitre' + const existingUser = { + username: 'testuser@example.com', + name: { + first: 'Test', + last: 'User' + } } - } - - chai.request(app) - .post(`/api/registryOrg/${orgShortName}/user`) - .set(constants.headers) - .send(invalidUser) - .end((err, res) => { - expect(err).to.be.null - expect(res).to.have.status(400) - expect(res.body).to.have.property('message').equal('Parameters were invalid') - done() - }) + chai.request(app) + .post(`/api/registryOrg/${orgShortName}/user`) + .set(constants.headers) + .send(existingUser) + .end((err, res) => { + expect(err).to.be.null + expect(res).to.have.status(400) + expect(res.body).to.have.property('message').equal(`The user '${existingUser.username}' already exists.`) + done() + }) + }) + it('Should not create a user with a missing username', (done) => { + const orgShortName = 'mitre' + const invalidUser = { + name: { + first: 'Test', + last: 'User' + } + } + chai.request(app) + .post(`/api/registryOrg/${orgShortName}/user`) + .set(constants.headers) + .send(invalidUser) + .end((err, res) => { + expect(err).to.be.null + expect(res).to.have.status(400) + expect(res.body).to.have.property('message').equal('Parameters were invalid') + done() + }) + }) + it('Should not create a user with an erroneous key not found in the schema', async () => { + const orgShortName = 'mitre' + const existingUser = { + username: 'testuser@example.com', + name: { + first: 'Test', + last: 'User' + }, + test: 'additional key not in schema' + } + chai.request(app) + .post(`/api/registryOrg/${orgShortName}/user`) + .set(constants.headers) + .send(existingUser) + .then((res) => { + expect(res).to.have.status(400) + expect(res.body.message).to.equal('Parameters were invalid') + expect(res.body.errors[0].message).to.equal('must NOT have additional properties') + }) + }) }) }) diff --git a/test/integration-tests/registry-org/registryOrgCRUDTest.js b/test/integration-tests/registry-org/registryOrgCRUDTest.js index e1e7f04d3..85b218053 100644 --- a/test/integration-tests/registry-org/registryOrgCRUDTest.js +++ b/test/integration-tests/registry-org/registryOrgCRUDTest.js @@ -60,6 +60,8 @@ describe('Testing /registryOrg endpoints', () => { expect(res.body.created.partner_country).to.equal(testRegistryOrg.partner_country) createdOrg = res.body.created + delete createdOrg.created + delete createdOrg.last_updated }) }) }) @@ -102,6 +104,20 @@ describe('Testing /registryOrg endpoints', () => { expect(res.body.details[0].msg).to.equal('reports_to must not be present') }) }) + it('Fails to create a new registry organization with an erroneous key not found in the schema', async () => { + await chai.request(app) + .post('/api/registryOrg') + .set(secretariatHeaders) + .send({ + ...testRegistryOrg, + test: 'additional key not in schema' + }) + .then((res) => { + expect(res).to.have.status(400) + expect(res.body.message).to.equal('Parameters were invalid') + expect(res.body.errors[0].message).to.equal('must NOT have additional properties') + }) + }) }) }) context('Testing GET /registryOrg endpoints', () => { @@ -324,6 +340,20 @@ describe('Testing /registryOrg endpoints', () => { expect(res.body.details[0].msg).to.equal('reports_to must not be present') }) }) + it('Fails to update a registry organization providing an erroneous key not found in the schema', async () => { + await chai.request(app) + .put('/api/registryOrg/registry_org_test') + .set(secretariatHeaders) + .send({ + ...createdOrg, + test: 'additional key not in schema' + }) + .then((res) => { + expect(res).to.have.status(400) + expect(res.body.message).to.equal('Parameters were invalid') + expect(res.body.errors[0].message).to.equal('must NOT have additional properties') + }) + }) }) }) context('Testing DELETE /registryOrg endpoint', () => { diff --git a/test/integration-tests/user/createUserTest.js b/test/integration-tests/user/createUserTest.js index a2050a67e..2b2e90010 100644 --- a/test/integration-tests/user/createUserTest.js +++ b/test/integration-tests/user/createUserTest.js @@ -24,16 +24,13 @@ const body = { const registryBody = { username: 'adpUser2', - active: 'true', name: { first: 'SecondTestCnaAdmin', last: 'test', middle: 'N', suffix: 'I' }, - authority: { - active_roles: ['Admin'] - } + status: 'active' } const nonAdminBody = { @@ -51,68 +48,94 @@ const nonAdminBody = { const registryNonAdminBody = { username: 'nonAdminUser2', - active: 'true', name: { first: 'SecondTestCnaAdmin', last: 'test', middle: 'N', suffix: 'I' }, - authority: { - } + status: 'active' } describe('Testing create user endpoint', () => { - it('Should return 200 and new user', (done) => { - chai.request(app) - .post('/api/org/range_4/user') - .set(constants.headers) - .send(body) - .end((err, res) => { - expect(err).to.be.null - expect(res.body).to.have.property('created') - expect(res.body.created.username).to.equal(body.username) - expect(res).to.have.status(200) - done() - }) - }) - it('Should return 200 and new user with registry enabled', (done) => { - chai.request(app) - .post('/api/registry/org/range_4/user') - .set(constants.headers) - .send(registryBody) - .end((err, res) => { - expect(err).to.be.null - expect(res.body).to.have.property('created') - expect(res.body.created.username).to.equal(registryBody.username) - expect(res).to.have.status(200) - done() - }) - }) - it('Should return 200 and create a non admin user', (done) => { - chai.request(app) - .post('/api/org/range_4/user') - .set(constants.headers) - .send(nonAdminBody) - .end((err, res) => { - expect(err).to.be.null - expect(res.body).to.have.property('created') - expect(res.body.created.username).to.equal(nonAdminBody.username) - expect(res).to.have.status(200) - done() - }) + context('Positive Tests', () => { + it('Should return 200 and new user', (done) => { + chai.request(app) + .post('/api/org/range_4/user') + .set(constants.headers) + .send(body) + .end((err, res) => { + expect(err).to.be.null + expect(res.body).to.have.property('created') + expect(res.body.created.username).to.equal(body.username) + expect(res).to.have.status(200) + done() + }) + }) + it('Should return 200 and new user with registry enabled', (done) => { + chai.request(app) + .post('/api/registry/org/range_4/user') + .set(constants.headers) + .send(registryBody) + .end((err, res) => { + expect(err).to.be.null + expect(res).to.have.status(200) + done() + }) + }) + it('Should return 200 and create a non admin user', (done) => { + chai.request(app) + .post('/api/org/range_4/user') + .set(constants.headers) + .send(nonAdminBody) + .end((err, res) => { + expect(err).to.be.null + expect(res.body).to.have.property('created') + expect(res.body.created.username).to.equal(nonAdminBody.username) + expect(res).to.have.status(200) + done() + }) + }) + it('Should return 200 and create a non admin user with registry enabled', (done) => { + chai.request(app) + .post('/api/registry/org/range_4/user') + .set(constants.headers) + .send(registryNonAdminBody) + .end((err, res) => { + expect(err).to.be.null + expect(res).to.have.status(200) + done() + }) + }) }) - it('Should return 200 and create a non admin user with registry enabled', (done) => { - chai.request(app) - .post('/api/registry/org/range_4/user') - .set(constants.headers) - .send(registryNonAdminBody) - .end((err, res) => { - expect(err).to.be.null - expect(res.body).to.have.property('created') - expect(res.body.created.username).to.equal(registryNonAdminBody.username) - expect(res).to.have.status(200) - done() - }) + context('Negative Tests', () => { + it('Should return 400 creating a new user with an erroneous key not found in the schema', async () => { + chai.request(app) + .post('/api/org/range_4/user') + .set(constants.headers) + .send({ + ...body, + test: 'additional key not in schema' + }) + .then((res) => { + expect(res).to.have.status(400) + expect(res.body.message).to.equal('Parameters were invalid') + expect(res.body.errors[0].message).to.equal('must NOT have additional properties') + }) + }) + it('Should return 400 creating a new user with registry enabled and an erroneous key not found in the schema', async () => { + chai.request(app) + .post('/api/registry/org/range_4/user') + .set(constants.headers) + .send({ + ...registryBody, + test: 'additional key not in schema' + }) + .then((res) => { + expect(res).to.have.status(400) + expect(res.body.message).to.equal('Parameters were invalid') + expect(res.body.errors[0].message).to.equal('must NOT have additional properties') + }) + }) }) }) diff --git a/test/integration-tests/user/regularUserUpdateTest.js b/test/integration-tests/user/regularUserUpdateTest.js index 7be104954..32ff85d5a 100644 --- a/test/integration-tests/user/regularUserUpdateTest.js +++ b/test/integration-tests/user/regularUserUpdateTest.js @@ -22,6 +22,7 @@ describe('Regular User Self-Update Tests', () => { expect(res).to.have.status(200) expect(res.body.username).to.equal('jasminesmith@win_5.com') user = res.body + delete user.created_by }) }) it('Should allow regular user to update their own contact info (name)', async () => { diff --git a/test/integration-tests/user/updateUserTest.js b/test/integration-tests/user/updateUserTest.js index 610438687..51f401f90 100644 --- a/test/integration-tests/user/updateUserTest.js +++ b/test/integration-tests/user/updateUserTest.js @@ -9,7 +9,7 @@ const constants = require('../constants.js') const app = require('../../../src/index.js') describe('Testing Edit user endpoint', () => { - context('User edit tests', () => { + context('Positive Tests', () => { it('Should return 200 when only name changes are done', async () => { await chai.request(app) .put('/api/org/win_5/user/jasminesmith@win_5.com?name.first=NewName') @@ -22,6 +22,7 @@ describe('Testing Edit user endpoint', () => { it('Should return 200 when only name changes are done with registry enabled', async () => { let user await chai.request(app).get('/api/registry/org/win_5/user/jasminesmith@win_5.com').set(constants.nonSecretariatUserHeaders).then((res) => { user = res.body }) + delete user.created_by await chai.request(app) .put('/api/registry/org/win_5/user/jasminesmith@win_5.com') .set(constants.nonSecretariatUserHeaders) @@ -37,6 +38,8 @@ describe('Testing Edit user endpoint', () => { expect(res).to.have.status(200) }) }) + }) + context('Negative Tests', () => { it('Should return an error when admin is required', async () => { await chai.request(app) .put('/api/org/win_5/user/jasminesmith@win_5.com?new_username=NewUsername') @@ -75,6 +78,7 @@ describe('Testing Edit user endpoint', () => { it('Should not allow a first name of more than 100 characters with registry enabled', async () => { let user await chai.request(app).get('/api/registry/org/win_5/user/jasminesmith@win_5.com').set(constants.nonSecretariatUserHeaders).then((res) => { user = res.body }) + delete user.created_by await chai.request(app) .put('/api/registry/org/win_5/user/jasminesmith@win_5.com') .set(constants.nonSecretariatUserHeaders) @@ -103,6 +107,7 @@ describe('Testing Edit user endpoint', () => { it('Should not allow a middle name of more than 100 characters with registry enabled', async () => { let user await chai.request(app).get('/api/registry/org/win_5/user/jasminesmith@win_5.com').set(constants.nonSecretariatUserHeaders).then((res) => { user = res.body }) + delete user.created_by await chai.request(app) .put('/api/registry/org/win_5/user/jasminesmith@win_5.com') .set(constants.nonSecretariatUserHeaders) @@ -131,6 +136,7 @@ describe('Testing Edit user endpoint', () => { it('Should not allow a last name of more than 100 characters with registry enabled', async () => { let user await chai.request(app).get('/api/registry/org/win_5/user/jasminesmith@win_5.com').set(constants.nonSecretariatUserHeaders).then((res) => { user = res.body }) + delete user.created_by await chai.request(app) .put('/api/registry/org/win_5/user/jasminesmith@win_5.com') .set(constants.nonSecretariatUserHeaders) @@ -159,6 +165,7 @@ describe('Testing Edit user endpoint', () => { it('Should not allow a suffix of more than 100 characters with registry enabled', async () => { let user await chai.request(app).get('/api/registry/org/win_5/user/jasminesmith@win_5.com').set(constants.nonSecretariatUserHeaders).then((res) => { user = res.body }) + delete user.created_by await chai.request(app) .put('/api/registry/org/win_5/user/jasminesmith@win_5.com?name.suffix=1:1234567,2:1234567,3:1234567,4:1234567,5:1234567,6:1234567,7:1234567,8:1234567,9:1234567,10:1234567,11:1234567') .set(constants.nonSecretariatUserHeaders) @@ -221,7 +228,6 @@ describe('Testing Edit user endpoint', () => { expect(res.body.error).to.contain('ORG_DNE_PARAM') }) }) - it('Should return 404 when target organization in body does not exist', async () => { const user = constants.headers['CVE-API-USER'] const org = constants.headers['CVE-API-ORG'] @@ -236,5 +242,31 @@ describe('Testing Edit user endpoint', () => { expect(res.body.error).to.contain('ORG_DNE_PARAM') }) }) + it('Should return 400 updating a user with an erroneous key not found in the schema', async () => { + await chai.request(app) + .put('/api/org/win_5/user/jasminesmith@win_5.com?test=testing123') + .set(constants.nonSecretariatUserHeaders) + .then((res, err) => { + expect(res).to.have.status(400) + expect(res.body.message).to.equal('Parameters were invalid') + expect(res.body.details[0].msg).to.equal("'test' is not a valid parameter name.") + }) + }) + it('Should return 400 updating a registry user with an erroneous key not found in the schema', async () => { + const user = constants.headers['CVE-API-USER'] + const org = constants.headers['CVE-API-ORG'] + await chai.request(app) + .put(`/api/registry/org/${org}/user/${user}`) + .set(constants.headers) + .send({ + username: 'user1', + test: 'additional key not in schema' + }) + .then((res) => { + expect(res).to.have.status(400) + expect(res.body.message).to.equal('Parameters were invalid') + expect(res.body.errors[0].message).to.equal('must NOT have additional properties') + }) + }) }) }) From de2b7a68092ef1d333860e172dc4878b3e98f3cf Mon Sep 17 00:00:00 2001 From: Andrew Foote Date: Wed, 22 Apr 2026 09:20:45 -0400 Subject: [PATCH 03/86] Fixed int tests that broke when merging with dev --- .../registry-org/registryOrgCRUDTest.js | 38 +++++++++---------- test/integration-tests/user/updateUserTest.js | 6 +++ 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/test/integration-tests/registry-org/registryOrgCRUDTest.js b/test/integration-tests/registry-org/registryOrgCRUDTest.js index b6bf8e383..a36b6a419 100644 --- a/test/integration-tests/registry-org/registryOrgCRUDTest.js +++ b/test/integration-tests/registry-org/registryOrgCRUDTest.js @@ -343,27 +343,6 @@ describe('Testing /registryOrg endpoints', () => { .delete(`/api/registryOrg/${subOrg.short_name}`) .set(secretariatHeaders) }) - it('Ignores protected fields such as users and admins during an update', async () => { - const maliciousUsers = ['d41d8cd9-8f00-3204-a980-0998ecf8427e'] - const maliciousAdmins = ['d41d8cd9-8f00-3204-a980-0998ecf8427e'] - - await chai.request(app) - .put(`/api/registryOrg/${createdOrg.short_name}`) - .set(secretariatHeaders) - .send({ - ...createdOrg, - users: maliciousUsers, - admins: maliciousAdmins - }) - .then((res, err) => { - expect(err).to.be.undefined - expect(res).to.have.status(200) - - // Ensure the response body.updated does not contain the malicious data - expect(res.body.updated.users || []).to.not.include(maliciousUsers[0]) - expect(res.body.updated.admins || []).to.not.include(maliciousAdmins[0]) - }) - }) }) context('Negative Tests', () => { it('Fails to update a registry organization that does not exist', async () => { @@ -405,6 +384,23 @@ describe('Testing /registryOrg endpoints', () => { expect(res.body.message).to.equal('Parameters were invalid') }) }) + it('Ignores protected fields such as users and admins during an update', async () => { + const maliciousUsers = ['d41d8cd9-8f00-3204-a980-0998ecf8427e'] + const maliciousAdmins = ['d41d8cd9-8f00-3204-a980-0998ecf8427e'] + + await chai.request(app) + .put(`/api/registryOrg/${createdOrg.short_name}`) + .set(secretariatHeaders) + .send({ + ...createdOrg, + users: maliciousUsers, + admins: maliciousAdmins + }) + .then((res) => { + expect(res).to.have.status(400) + expect(res.body.message).to.equal('Parameters were invalid') + }) + }) it('Fails to update a registry organization with reports_to manually provided', async () => { await chai.request(app) .put(`/api/registryOrg/${createdOrg.short_name}`) diff --git a/test/integration-tests/user/updateUserTest.js b/test/integration-tests/user/updateUserTest.js index 1fe33f07c..3871a4b2a 100644 --- a/test/integration-tests/user/updateUserTest.js +++ b/test/integration-tests/user/updateUserTest.js @@ -42,6 +42,9 @@ describe('Testing Edit user endpoint', () => { let user await chai.request(app).get('/api/registry/org/win_5/user/jasminesmith@win_5.com').set(constants.headers).then((res) => { user = res.body }) + delete user.created + delete user.created_by + delete user.last_updated const maliciousUUID = 'd41d8cd9-8f00-3204-a980-0998ecf8427e' const originalUUID = user.UUID @@ -65,6 +68,9 @@ describe('Testing Edit user endpoint', () => { let user await chai.request(app).get('/api/registry/org/win_5/user/jasminesmith@win_5.com').set(constants.headers).then((res) => { user = res.body }) + delete user.created + delete user.created_by + delete user.last_updated await chai.request(app) .put('/api/registry/org/win_5/user/jasminesmith@win_5.com') .set(constants.headers) From 7c6edb3d76176c54768150db00db8f4748c153d2 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Thu, 23 Apr 2026 12:46:14 -0400 Subject: [PATCH 04/86] testing readPreference --- src/utils/db.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/db.js b/src/utils/db.js index 6fde268f5..4b3be05a8 100644 --- a/src/utils/db.js +++ b/src/utils/db.js @@ -26,7 +26,7 @@ function getMongoConnectionString () { logger.info(`Will try to connect to database ${dbName} at ${dbHost}:${dbPort}`) if (process.env.useAWS) { - return `mongodb://${dbLoginPrepend}${dbHost}:${dbPort}/${dbName}?retryWrites=false` + return `mongodb://${dbLoginPrepend}${dbHost}:${dbPort}/${dbName}?replicaSet=rs0&readPreference=secondaryPreferred&retryWrites=false` } else { return `mongodb://${dbLoginPrepend}${dbHost}:${dbPort}/${dbName}` } From 320944b318a1afbfdcf6ccd11c09bbe9cba69d9b Mon Sep 17 00:00:00 2001 From: david-rocca Date: Thu, 23 Apr 2026 13:06:04 -0400 Subject: [PATCH 05/86] Revert "testing readPreference" This reverts commit c7414e3278fbef14fc1fff2341c3d397c8c28079. --- src/utils/db.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/db.js b/src/utils/db.js index 4b3be05a8..6fde268f5 100644 --- a/src/utils/db.js +++ b/src/utils/db.js @@ -26,7 +26,7 @@ function getMongoConnectionString () { logger.info(`Will try to connect to database ${dbName} at ${dbHost}:${dbPort}`) if (process.env.useAWS) { - return `mongodb://${dbLoginPrepend}${dbHost}:${dbPort}/${dbName}?replicaSet=rs0&readPreference=secondaryPreferred&retryWrites=false` + return `mongodb://${dbLoginPrepend}${dbHost}:${dbPort}/${dbName}?retryWrites=false` } else { return `mongodb://${dbLoginPrepend}${dbHost}:${dbPort}/${dbName}` } From 3b1a752c0416b40cbd486ece033677fe16ef67f9 Mon Sep 17 00:00:00 2001 From: "Daigneau, Jeremy T" Date: Thu, 23 Apr 2026 13:35:50 -0400 Subject: [PATCH 06/86] testing read preference at transaction level --- .../registry-org.controller/registry-org.controller.js | 2 +- src/utils/db.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/controller/registry-org.controller/registry-org.controller.js b/src/controller/registry-org.controller/registry-org.controller.js index 64da4d038..2e8ef9e1a 100644 --- a/src/controller/registry-org.controller/registry-org.controller.js +++ b/src/controller/registry-org.controller/registry-org.controller.js @@ -134,7 +134,7 @@ async function getOrg (req, res, next) { */ async function createOrg (req, res, next) { try { - const session = await mongoose.startSession({ causalConsistency: false }) + const session = await mongoose.startSession({ causalConsistency: false, readPreference: 'primary' }) const repo = req.ctx.repositories.getBaseOrgRepository() const body = req.ctx.body const isSecretariat = await repo.isSecretariatByShortName(req.ctx.org, { session }) diff --git a/src/utils/db.js b/src/utils/db.js index 6fde268f5..4b3be05a8 100644 --- a/src/utils/db.js +++ b/src/utils/db.js @@ -26,7 +26,7 @@ function getMongoConnectionString () { logger.info(`Will try to connect to database ${dbName} at ${dbHost}:${dbPort}`) if (process.env.useAWS) { - return `mongodb://${dbLoginPrepend}${dbHost}:${dbPort}/${dbName}?retryWrites=false` + return `mongodb://${dbLoginPrepend}${dbHost}:${dbPort}/${dbName}?replicaSet=rs0&readPreference=secondaryPreferred&retryWrites=false` } else { return `mongodb://${dbLoginPrepend}${dbHost}:${dbPort}/${dbName}` } From 9da249b1714340e2e1a31653bf3b2fc0e4959338 Mon Sep 17 00:00:00 2001 From: "Daigneau, Jeremy T" Date: Thu, 23 Apr 2026 13:45:52 -0400 Subject: [PATCH 07/86] moving read preference from session to transaction --- .../registry-org.controller/registry-org.controller.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/controller/registry-org.controller/registry-org.controller.js b/src/controller/registry-org.controller/registry-org.controller.js index 2e8ef9e1a..cb37d8651 100644 --- a/src/controller/registry-org.controller/registry-org.controller.js +++ b/src/controller/registry-org.controller/registry-org.controller.js @@ -134,7 +134,7 @@ async function getOrg (req, res, next) { */ async function createOrg (req, res, next) { try { - const session = await mongoose.startSession({ causalConsistency: false, readPreference: 'primary' }) + const session = await mongoose.startSession({ causalConsistency: false }) const repo = req.ctx.repositories.getBaseOrgRepository() const body = req.ctx.body const isSecretariat = await repo.isSecretariatByShortName(req.ctx.org, { session }) @@ -146,7 +146,7 @@ async function createOrg (req, res, next) { } try { - session.startTransaction() + session.startTransaction({ readPreference: 'primary' }) const result = repo.validateOrg(body, { session }) if (!result.isValid) { logger.error(JSON.stringify({ uuid: req.ctx.uuid, message: 'CVE JSON schema validation FAILED.' })) From 2603b89bcea51a35f854f190f2c05f4a7ca3755f Mon Sep 17 00:00:00 2001 From: "Daigneau, Jeremy T" Date: Thu, 23 Apr 2026 13:57:37 -0400 Subject: [PATCH 08/86] Reverting dev changes --- .../registry-org.controller/registry-org.controller.js | 2 +- src/utils/db.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/controller/registry-org.controller/registry-org.controller.js b/src/controller/registry-org.controller/registry-org.controller.js index cb37d8651..64da4d038 100644 --- a/src/controller/registry-org.controller/registry-org.controller.js +++ b/src/controller/registry-org.controller/registry-org.controller.js @@ -146,7 +146,7 @@ async function createOrg (req, res, next) { } try { - session.startTransaction({ readPreference: 'primary' }) + session.startTransaction() const result = repo.validateOrg(body, { session }) if (!result.isValid) { logger.error(JSON.stringify({ uuid: req.ctx.uuid, message: 'CVE JSON schema validation FAILED.' })) diff --git a/src/utils/db.js b/src/utils/db.js index 4b3be05a8..6fde268f5 100644 --- a/src/utils/db.js +++ b/src/utils/db.js @@ -26,7 +26,7 @@ function getMongoConnectionString () { logger.info(`Will try to connect to database ${dbName} at ${dbHost}:${dbPort}`) if (process.env.useAWS) { - return `mongodb://${dbLoginPrepend}${dbHost}:${dbPort}/${dbName}?replicaSet=rs0&readPreference=secondaryPreferred&retryWrites=false` + return `mongodb://${dbLoginPrepend}${dbHost}:${dbPort}/${dbName}?retryWrites=false` } else { return `mongodb://${dbLoginPrepend}${dbHost}:${dbPort}/${dbName}` } From 21d8a235c63159684036afafb2c37298540f468f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Apr 2026 15:21:43 +0000 Subject: [PATCH 09/86] Bump path-to-regexp from 0.1.12 to 0.1.13 Bumps [path-to-regexp](https://github.com/pillarjs/path-to-regexp) from 0.1.12 to 0.1.13. - [Release notes](https://github.com/pillarjs/path-to-regexp/releases) - [Changelog](https://github.com/pillarjs/path-to-regexp/blob/v.0.1.13/History.md) - [Commits](https://github.com/pillarjs/path-to-regexp/compare/v0.1.12...v.0.1.13) --- updated-dependencies: - dependency-name: path-to-regexp dependency-version: 0.1.13 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 88c96fde7..227cb2a19 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "cve-services", - "version": "2.7.0", + "version": "2.7.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cve-services", - "version": "2.7.0", + "version": "2.7.1", "license": "(CC0)", "dependencies": { "ajv": "^8.6.2", @@ -6685,9 +6685,9 @@ "license": "MIT" }, "node_modules/path-to-regexp": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", - "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", "license": "MIT" }, "node_modules/path-type": { From 0dd84af6f2ca886bc9d963675c3a147ba58e853e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Apr 2026 15:21:56 +0000 Subject: [PATCH 10/86] Bump brace-expansion from 1.1.12 to 1.1.13 Bumps [brace-expansion](https://github.com/juliangruber/brace-expansion) from 1.1.12 to 1.1.13. - [Release notes](https://github.com/juliangruber/brace-expansion/releases) - [Commits](https://github.com/juliangruber/brace-expansion/compare/v1.1.12...v1.1.13) --- updated-dependencies: - dependency-name: brace-expansion dependency-version: 1.1.13 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 84 +++++++++++++++++++++++------------------------ 1 file changed, 42 insertions(+), 42 deletions(-) diff --git a/package-lock.json b/package-lock.json index 227cb2a19..39c922f3d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -431,9 +431,9 @@ "license": "Python-2.0" }, "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -512,9 +512,9 @@ } }, "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -1510,9 +1510,9 @@ } }, "node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", "dev": true, "license": "MIT", "dependencies": { @@ -2883,9 +2883,9 @@ } }, "node_modules/eslint-plugin-import/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -2978,9 +2978,9 @@ } }, "node_modules/eslint-plugin-node/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -3126,9 +3126,9 @@ "license": "Python-2.0" }, "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -5846,9 +5846,9 @@ } }, "node_modules/multimatch/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -6116,9 +6116,9 @@ } }, "node_modules/nyc/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -7482,9 +7482,9 @@ } }, "node_modules/replace-in-file/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -7685,9 +7685,9 @@ } }, "node_modules/rimraf/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -8359,9 +8359,9 @@ } }, "node_modules/standard/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -9059,9 +9059,9 @@ } }, "node_modules/swagger-autogen/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -9158,9 +9158,9 @@ } }, "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -9867,9 +9867,9 @@ } }, "node_modules/yamljs/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", From 2999738e35699b90d35a1eb34173edddef5adc58 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Tue, 7 Apr 2026 15:15:06 -0400 Subject: [PATCH 11/86] fix high vuln --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 39c922f3d..aa1aceef9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5305,9 +5305,9 @@ } }, "node_modules/lodash": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "license": "MIT" }, "node_modules/lodash.flattendeep": { From 3774de8b28ba9ecd1285c82ade5a4ecfc0444941 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Tue, 7 Apr 2026 15:29:45 -0400 Subject: [PATCH 12/86] updating versions --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index aa1aceef9..f590f20c2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "cve-services", - "version": "2.7.1", + "version": "2.7.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cve-services", - "version": "2.7.1", + "version": "2.7.2", "license": "(CC0)", "dependencies": { "ajv": "^8.6.2", From 43329b87b77f7c6410591f98c08b532450aaa58e Mon Sep 17 00:00:00 2001 From: david-rocca Date: Wed, 29 Apr 2026 12:45:54 -0400 Subject: [PATCH 13/86] Fixing version number conflicts --- api-docs/openapi.json | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/api-docs/openapi.json b/api-docs/openapi.json index 01ab0fad9..eaf2feb02 100644 --- a/api-docs/openapi.json +++ b/api-docs/openapi.json @@ -1,7 +1,7 @@ { "openapi": "3.0.2", "info": { - "version": "2.7.5", + "version": "2.8.0", "title": "CVE Services API", "description": "The CVE Services API supports automation tooling for the CVE Program. Credentials are required for most service endpoints. Representatives of CVE Numbering Authorities (CNAs) should use one of the methods below to obtain credentials:
  • If your organization already has an Organizational Administrator (OA) account for the CVE Services, ask your admin for credentials
  • Contact your Root (Google, INCIBE, JPCERT/CC, or Red Hat) or Top-Level Root (CISA ICS or MITRE) to request credentials

CVE data is to be in the JSON 5.2 CVE Record format. Details of the JSON 5.2 schema are located here.

Contact the CVE Services team", "contact": { diff --git a/package-lock.json b/package-lock.json index f590f20c2..6c434b937 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "cve-services", - "version": "2.7.2", + "version": "2.8.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cve-services", - "version": "2.7.2", + "version": "2.8.0", "license": "(CC0)", "dependencies": { "ajv": "^8.6.2", diff --git a/package.json b/package.json index 8e0ec46bb..2b6186b3c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "cve-services", "author": "Automation Working Group", - "version": "2.7.5", + "version": "2.8.0", "license": "(CC0)", "devDependencies": { "@faker-js/faker": "^7.6.0", From b62d7ad4c65e8548327c16fb830e6a40b0ee4cf1 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Wed, 29 Apr 2026 12:57:08 -0400 Subject: [PATCH 14/86] Conflicts --- schemas/glossary/glossary.json | 24 ++ .../list-glossary-items-response.json | 16 + .../glossary.controller.js | 128 +++++++ src/controller/glossary.controller/index.js | 341 ++++++++++++++++++ src/middleware/schemas/ADPOrg.json | 18 + src/middleware/schemas/BaseOrg.json | 158 ++++++++ src/middleware/schemas/BulkDownloadOrg.json | 18 + src/middleware/schemas/CNAOrg.json | 45 +++ src/middleware/schemas/SecretariatOrg.json | 36 ++ src/model/glossary.js | 14 + src/repositories/glossaryRepository.js | 26 ++ src/repositories/repositoryFactory.js | 6 + src/routes.config.js | 2 + src/scripts/migrate.js | 11 + src/scripts/populate.js | 8 +- .../glossary/glossaryCRUDTest.js | 156 ++++++++ 16 files changed, 1005 insertions(+), 2 deletions(-) create mode 100644 schemas/glossary/glossary.json create mode 100644 schemas/glossary/list-glossary-items-response.json create mode 100644 src/controller/glossary.controller/glossary.controller.js create mode 100644 src/controller/glossary.controller/index.js create mode 100644 src/middleware/schemas/ADPOrg.json create mode 100644 src/middleware/schemas/BaseOrg.json create mode 100644 src/middleware/schemas/BulkDownloadOrg.json create mode 100644 src/middleware/schemas/CNAOrg.json create mode 100644 src/middleware/schemas/SecretariatOrg.json create mode 100644 src/model/glossary.js create mode 100644 src/repositories/glossaryRepository.js create mode 100644 test/integration-tests/glossary/glossaryCRUDTest.js diff --git a/schemas/glossary/glossary.json b/schemas/glossary/glossary.json new file mode 100644 index 000000000..0f6bbe633 --- /dev/null +++ b/schemas/glossary/glossary.json @@ -0,0 +1,24 @@ +{ + "$id": "https://cve.mitre.org/api-docs/schema/glossary/glossary.json", + "type": "object", + "properties": { + "short_name": { + "type": "string", + "minLength": 1 + }, + "label": { + "type": "string", + "minLength": 1 + }, + "def": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "short_name", + "label", + "def" + ], + "additionalProperties": false +} diff --git a/schemas/glossary/list-glossary-items-response.json b/schemas/glossary/list-glossary-items-response.json new file mode 100644 index 000000000..fee135e11 --- /dev/null +++ b/schemas/glossary/list-glossary-items-response.json @@ -0,0 +1,16 @@ +{ + "$id": "https://cve.mitre.org/api-docs/schema/glossary/list-glossary-items-response.json", + "type": "object", + "properties": { + "glossary": { + "type": "array", + "items": { + "type": "object" + } + } + }, + "required": [ + "glossary" + ], + "additionalProperties": false +} diff --git a/src/controller/glossary.controller/glossary.controller.js b/src/controller/glossary.controller/glossary.controller.js new file mode 100644 index 000000000..18f0f2a8f --- /dev/null +++ b/src/controller/glossary.controller/glossary.controller.js @@ -0,0 +1,128 @@ +const errors = require('../../utils/error') +const error = new errors.IDRError() + +/** + * Retrieves all glossary items. + * + * @param {Object} req - The Express request object. + * @param {Object} res - The Express response object. + * @param {Function} next - The Express next middleware function. + * @returns {Promise} Returns a JSON response containing an array of all glossary items. + */ +async function getAllGlossaryItems (req, res, next) { + try { + const glossaryRepo = req.ctx.repositories.getGlossaryRepository() + const result = await glossaryRepo.getAll() + return res.status(200).json({ glossary: result }) + } catch (err) { + next(err) + } +} + +/** + * Retrieves a single glossary item by its short name. + * + * @param {Object} req - The Express request object. + * @param {Object} res - The Express response object. + * @param {Function} next - The Express next middleware function. + * @returns {Promise} Returns a JSON response containing the requested glossary item, or a 404 if not found. + */ +async function getGlossaryItem (req, res, next) { + try { + const glossaryRepo = req.ctx.repositories.getGlossaryRepository() + const shortName = req.params.short_name + + const result = await glossaryRepo.findOneByShortName(shortName) + if (!result) { + return res.status(404).json(error.notFound()) + } + return res.status(200).json(result) + } catch (err) { + next(err) + } +} + +/** + * Creates a new glossary item. + * + * @param {Object} req - The Express request object. + * @param {Object} res - The Express response object. + * @param {Function} next - The Express next middleware function. + * @returns {Promise} Returns a JSON response containing the newly created glossary item, or a 400 if it already exists. + */ +async function createGlossaryItem (req, res, next) { + try { + const glossaryRepo = req.ctx.repositories.getGlossaryRepository() + const glossaryData = req.body + + const existing = await glossaryRepo.findOneByShortName(glossaryData.short_name) + if (existing) { + return res.status(400).json(error.badInput(['Glossary item with this short_name already exists'])) + } + + const result = await glossaryRepo.collection.create(glossaryData) + return res.status(200).json(result) + } catch (err) { + next(err) + } +} + +/** + * Updates an existing glossary item by its short name. + * + * @param {Object} req - The Express request object. + * @param {Object} res - The Express response object. + * @param {Function} next - The Express next middleware function. + * @returns {Promise} Returns a JSON response containing the updated glossary item, or a 404 if not found. + */ +async function updateGlossaryItem (req, res, next) { + try { + const glossaryRepo = req.ctx.repositories.getGlossaryRepository() + const shortName = req.params.short_name + const glossaryData = req.body + + if (glossaryData.short_name && glossaryData.short_name !== shortName) { + return res.status(400).json(error.badInput(['Cannot change short_name through this endpoint.'])) + } + + const result = await glossaryRepo.updateByShortName(shortName, glossaryData) + if (!result) { + return res.status(404).json(error.notFound()) + } + + return res.status(200).json(result) + } catch (err) { + next(err) + } +} + +/** + * Deletes an existing glossary item by its short name. + * + * @param {Object} req - The Express request object. + * @param {Object} res - The Express response object. + * @param {Function} next - The Express next middleware function. + * @returns {Promise} Returns a JSON message confirming deletion, or a 404 if not found. + */ +async function deleteGlossaryItem (req, res, next) { + try { + const glossaryRepo = req.ctx.repositories.getGlossaryRepository() + const shortName = req.params.short_name + + const result = await glossaryRepo.deleteByShortName(shortName) + if (!result) { + return res.status(404).json(error.notFound()) + } + return res.status(200).json({ message: 'Glossary item deleted' }) + } catch (err) { + next(err) + } +} + +module.exports = { + getAllGlossaryItems, + getGlossaryItem, + createGlossaryItem, + updateGlossaryItem, + deleteGlossaryItem +} diff --git a/src/controller/glossary.controller/index.js b/src/controller/glossary.controller/index.js new file mode 100644 index 000000000..8d83cc8ca --- /dev/null +++ b/src/controller/glossary.controller/index.js @@ -0,0 +1,341 @@ +const router = require('express').Router() +const controller = require('./glossary.controller') +const mw = require('../../middleware/middleware') + +// Get all glossary items - SEC only +router.get('/glossary', + /* + #swagger.tags = ['Glossary'] + #swagger.operationId = 'glossaryAll' + #swagger.summary = "Retrieves all glossary items (accessible to Secretariat only)" + #swagger.description = " +

Access Control

+

User must belong to an organization with the Secretariat role

+

Expected Behavior

+

Secretariat: Retrieves all glossary items

" + #swagger.parameters['$ref'] = [ + '#/components/parameters/apiEntityHeader', + '#/components/parameters/apiUserHeader', + '#/components/parameters/apiSecretHeader' + ] + #swagger.responses[200] = { + description: 'Returns a list of all glossary items', + content: { + "application/json": { + schema: { + $ref: '../schemas/glossary/list-glossary-items-response.json' + } + } + } + } + #swagger.responses[401] = { + description: 'Not Authenticated', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + #swagger.responses[403] = { + description: 'Forbidden', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + #swagger.responses[500] = { + description: 'Internal Server Error', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + */ + mw.validateUser, + mw.onlySecretariat, + controller.getAllGlossaryItems +) + +// Get glossary item by short_name - SEC only +router.get('/glossary/short_name/:short_name', + /* + #swagger.tags = ['Glossary'] + #swagger.operationId = 'glossarySingle' + #swagger.summary = "Retrieves a single glossary item by its short name (accessible to Secretariat only)" + #swagger.description = " +

Access Control

+

User must belong to an organization with the Secretariat role

+

Expected Behavior

+

Secretariat: Retrieves the specified glossary item

" + #swagger.parameters['short_name'] = { description: 'The short name of the glossary item' } + #swagger.parameters['$ref'] = [ + '#/components/parameters/apiEntityHeader', + '#/components/parameters/apiUserHeader', + '#/components/parameters/apiSecretHeader' + ] + #swagger.responses[200] = { + description: 'Returns the specified glossary item', + content: { + "application/json": { + schema: { + $ref: '../schemas/glossary/glossary.json' + } + } + } + } + #swagger.responses[401] = { + description: 'Not Authenticated', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + #swagger.responses[403] = { + description: 'Forbidden', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + #swagger.responses[404] = { + description: 'Not Found', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + #swagger.responses[500] = { + description: 'Internal Server Error', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + */ + mw.validateUser, + mw.onlySecretariat, + controller.getGlossaryItem +) + +// Create a glossary item - SEC only +router.post('/glossary', + /* + #swagger.tags = ['Glossary'] + #swagger.operationId = 'glossaryCreate' + #swagger.summary = "Creates a new glossary item (accessible to Secretariat only)" + #swagger.description = " +

Access Control

+

User must belong to an organization with the Secretariat role

+

Expected Behavior

+

Secretariat: Creates a new glossary item

" + #swagger.parameters['$ref'] = [ + '#/components/parameters/apiEntityHeader', + '#/components/parameters/apiUserHeader', + '#/components/parameters/apiSecretHeader' + ] + #swagger.requestBody = { + required: true, + content: { + 'application/json': { + schema: { + $ref: '../schemas/glossary/glossary.json' + } + } + } + } + #swagger.responses[200] = { + description: 'Returns the created glossary item', + content: { + "application/json": { + schema: { + $ref: '../schemas/glossary/glossary.json' + } + } + } + } + #swagger.responses[400] = { + description: 'Bad Request', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/bad-request.json' } + } + } + } + #swagger.responses[401] = { + description: 'Not Authenticated', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + #swagger.responses[403] = { + description: 'Forbidden', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + #swagger.responses[500] = { + description: 'Internal Server Error', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + */ + mw.validateUser, + mw.onlySecretariat, + controller.createGlossaryItem +) + +// Update a glossary item - SEC only +router.put('/glossary/short_name/:short_name', + /* + #swagger.tags = ['Glossary'] + #swagger.operationId = 'glossaryUpdate' + #swagger.summary = "Updates an existing glossary item (accessible to Secretariat only)" + #swagger.description = " +

Access Control

+

User must belong to an organization with the Secretariat role

+

Expected Behavior

+

Secretariat: Updates the specified glossary item

" + #swagger.parameters['short_name'] = { description: 'The short name of the glossary item' } + #swagger.parameters['$ref'] = [ + '#/components/parameters/apiEntityHeader', + '#/components/parameters/apiUserHeader', + '#/components/parameters/apiSecretHeader' + ] + #swagger.requestBody = { + required: true, + content: { + 'application/json': { + schema: { + $ref: '../schemas/glossary/glossary.json' + } + } + } + } + #swagger.responses[200] = { + description: 'Returns the updated glossary item', + content: { + "application/json": { + schema: { + $ref: '../schemas/glossary/glossary.json' + } + } + } + } + #swagger.responses[400] = { + description: 'Bad Request', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/bad-request.json' } + } + } + } + #swagger.responses[401] = { + description: 'Not Authenticated', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + #swagger.responses[403] = { + description: 'Forbidden', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + #swagger.responses[404] = { + description: 'Not Found', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + #swagger.responses[500] = { + description: 'Internal Server Error', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + */ + mw.validateUser, + mw.onlySecretariat, + controller.updateGlossaryItem +) + +// Delete a glossary item - SEC only +router.delete('/glossary/short_name/:short_name', + /* + #swagger.tags = ['Glossary'] + #swagger.operationId = 'glossaryDelete' + #swagger.summary = "Deletes an existing glossary item (accessible to Secretariat only)" + #swagger.description = " +

Access Control

+

User must belong to an organization with the Secretariat role

+

Expected Behavior

+

Secretariat: Deletes the specified glossary item

" + #swagger.parameters['short_name'] = { description: 'The short name of the glossary item' } + #swagger.parameters['$ref'] = [ + '#/components/parameters/apiEntityHeader', + '#/components/parameters/apiUserHeader', + '#/components/parameters/apiSecretHeader' + ] + #swagger.responses[200] = { + description: 'Confirms deletion of the glossary item' + } + #swagger.responses[401] = { + description: 'Not Authenticated', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + #swagger.responses[403] = { + description: 'Forbidden', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + #swagger.responses[404] = { + description: 'Not Found', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + #swagger.responses[500] = { + description: 'Internal Server Error', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + */ + mw.validateUser, + mw.onlySecretariat, + controller.deleteGlossaryItem +) + +module.exports = router diff --git a/src/middleware/schemas/ADPOrg.json b/src/middleware/schemas/ADPOrg.json new file mode 100644 index 000000000..b5bea44cb --- /dev/null +++ b/src/middleware/schemas/ADPOrg.json @@ -0,0 +1,18 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "ADPOrg", + "type": "object", + "title": "CVE ADP Organization", + "description": "Schema for a CVE ADP Organization", + "allOf": [ + { "$ref": "/BaseOrg" }, + { + "type": "object", + "properties": { + "authority": { + "const": ["ADP"] + } + } + } + ] +} diff --git a/src/middleware/schemas/BaseOrg.json b/src/middleware/schemas/BaseOrg.json new file mode 100644 index 000000000..a87e55fe4 --- /dev/null +++ b/src/middleware/schemas/BaseOrg.json @@ -0,0 +1,158 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "/BaseOrg", + "type": "object", + "title": "CVE Base Organization", + "description": "Base schema for a CVE Organization", + "definitions": { + "uuidType": { + "description": "A version 4 (random) universally unique identifier (UUID) as defined by [RFC 4122](https://tools.ietf.org/html/rfc4122#section-4.1.3).", + "type": "string", + "format": "uuid", + "pattern": "^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$" + }, + "uriType": { + "description": "A universal resource identifier (URI), according to [RFC 3986](https://tools.ietf.org/html/rfc3986).", + "type": "string", + "format": "uri", + "pattern": "^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\\?([^#]*))?(#(.*))?", + "minLength": 1, + "maxLength": 2048 + }, + "shortName": { + "description": "A 2-32 character name that can be used to complement an organization's UUID.", + "type": "string", + "minLength": 2, + "maxLength": 32 + }, + "longName": { + "description": "A 1-256 character name that can be used to complement an organization's short_name.", + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "authority": { + "description": "The authority (role) of this organization within the CVE program", + "type": "string", + "enum": ["CNA", "SECRETARIAT", "BULK_DOWNLOAD", "ADP"] + }, + "discriminator": { + "description": "Discriminator key used by Mongoose for type inheritance", + "type": "string" + }, + "timestamp": { + "description": "Date/time format based on RFC3339 and ISO ISO8601, with an optional timezone in the format 'yyyy-MM-ddTHH:mm:ss[+-]ZH:ZM'. If timezone offset is not given, GMT (+00:00) is assumed.", + "pattern": "^(((2000|2400|2800|(19|2[0-9](0[48]|[2468][048]|[13579][26])))-02-29)|(((19|2[0-9])[0-9]{2})-02-(0[1-9]|1[0-9]|2[0-8]))|(((19|2[0-9])[0-9]{2})-(0[13578]|10|12)-(0[1-9]|[12][0-9]|3[01]))|(((19|2[0-9])[0-9]{2})-(0[469]|11)-(0[1-9]|[12][0-9]|30)))T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})?$", + "type": "string" + } + }, + "properties": { + "UUID": { + "$ref": "#/definitions/uuidType" + }, + "__t": { + "$ref": "#/definitions/discriminator" + }, + "short_name": { + "$ref": "#/definitions/shortName" + }, + "long_name": { + "$ref": "#/definitions/longName" + }, + "aliases": { + "type": "array", + "uniqueItems": true, + "items": { + "type": "string" + } + }, + "authority": { + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "#/definitions/authority" + } + }, + "root_or_tlr": { + "type": "boolean" + }, + "users": { + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "#/definitions/uuidType" + } + }, + "admins": { + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "#/definitions/uuidType" + } + }, + "contact_info": { + "type": "object", + "properties": { + "additional_contact_users": { + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "#/definitions/uuidType" + } + }, + "poc": { + "type": "string" + }, + "poc_email": { + "type": "string", + "format": "email" + }, + "poc_phone": { + "type": "string" + }, + "org_email": { + "type": "string", + "format": "email" + }, + "website": { + "$ref": "#/definitions/uriType", + "type": "string", + "pattern": "^(ftp|http)s?://\\S+$" + } + }, + "additionalProperties": false + }, + "partner_role": { + "type": "string" + }, + "partner_type": { + "type": "string" + }, + "partner_country": { + "type": "string" + }, + "vulnerability_advisory_locations": { + "type": "array", + "uniqueItems": true, + "items": { + "type": "string" + } + }, + "advisory_location_require_credentials": { + "type": "boolean" + }, + "industry": { + "type": "string" + }, + "tl_root_start_date": { + "$ref": "#/definitions/timestamp" + }, + "is_cna_discussion_list": { + "type": "boolean" + } + }, + "required": [ + "short_name", + "long_name" + ] +} \ No newline at end of file diff --git a/src/middleware/schemas/BulkDownloadOrg.json b/src/middleware/schemas/BulkDownloadOrg.json new file mode 100644 index 000000000..768ae1123 --- /dev/null +++ b/src/middleware/schemas/BulkDownloadOrg.json @@ -0,0 +1,18 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "BaseOrg", + "type": "object", + "title": "CVE Bulk Download Organization", + "description": "Schema for a CVE Bulk Download Organization", + "allOf": [ + { "$ref": "/BaseOrg" }, + { + "type": "object", + "properties": { + "authority": { + "const": ["BULK_DOWNLOAD"] + } + } + } + ] +} diff --git a/src/middleware/schemas/CNAOrg.json b/src/middleware/schemas/CNAOrg.json new file mode 100644 index 000000000..966056f75 --- /dev/null +++ b/src/middleware/schemas/CNAOrg.json @@ -0,0 +1,45 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "$id": "CNAOrg", + "title": "CVE CNA Organization", + "description": "Schema for a CVE CNA Organization", + "allOf": [ + { "$ref": "/BaseOrg" }, + { + "type": "object", + "properties": { + "authority": { + "const": ["CNA"] + }, + "oversees": { + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "/BaseOrg#/definitions/uuidType" + } + }, + "hard_quota": { + "type": "integer", + "minimum": 0, + "maximum": 100000 + }, + "soft_quota": { + "type": "integer", + "minimum": 0, + "maximum": 100000 + }, + "charter_or_scope": { + "$ref": "/BaseOrg#/definitions/uriType" + }, + "disclosure_policy": { + "$ref": "/BaseOrg#/definitions/uriType" + }, + "product_list": { + "$ref": "/BaseOrg#/definitions/uriType" + } + }, + "required": ["hard_quota"] + } + ] +} diff --git a/src/middleware/schemas/SecretariatOrg.json b/src/middleware/schemas/SecretariatOrg.json new file mode 100644 index 000000000..f4e4e1637 --- /dev/null +++ b/src/middleware/schemas/SecretariatOrg.json @@ -0,0 +1,36 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "SecretariatOrg", + "type": "object", + "title": "CVE Secretariat Organization", + "description": "Schema for a CVE Secretariat Organization", + "allOf": [ + { "$ref": "/BaseOrg" }, + { + "type": "object", + "properties": { + "authority": { + "const": ["SECRETARIAT"] + }, + "oversees": { + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "/BaseOrg#/definitions/uuidType" + } + }, + "hard_quota": { + "type": "integer", + "minimum": 0, + "maximum": 100000 + }, + "soft_quota": { + "type": "integer", + "minimum": 0, + "maximum": 100000 + } + }, + "required": ["hard_quota"] + } + ] +} diff --git a/src/model/glossary.js b/src/model/glossary.js new file mode 100644 index 000000000..fd9e9a2b5 --- /dev/null +++ b/src/model/glossary.js @@ -0,0 +1,14 @@ +const mongoose = require('mongoose') + +const schema = { + short_name: { type: String, required: true }, + label: { type: String, required: true }, + def: { type: String, required: true } +} + +const GlossarySchema = new mongoose.Schema(schema, { collection: 'Glossary', timestamps: { createdAt: 'createdAt', updatedAt: 'updatedAt' } }) + +GlossarySchema.index({ short_name: 1 }, { unique: true }) + +const Glossary = mongoose.model('Glossary', GlossarySchema) +module.exports = Glossary diff --git a/src/repositories/glossaryRepository.js b/src/repositories/glossaryRepository.js new file mode 100644 index 000000000..934cce610 --- /dev/null +++ b/src/repositories/glossaryRepository.js @@ -0,0 +1,26 @@ +const BaseRepository = require('./baseRepository') +const Glossary = require('../model/glossary') + +class GlossaryRepository extends BaseRepository { + constructor () { + super(Glossary) + } + + async getAll () { + return this.find({}, { multiple: true }) + } + + async findOneByShortName (shortName) { + return this.findOne({ short_name: shortName }) + } + + async updateByShortName (shortName, newGlossaryData) { + return this.findOneAndUpdate({ short_name: shortName }, newGlossaryData) + } + + async deleteByShortName (shortName) { + return this.collection.findOneAndDelete({ short_name: shortName }) + } +} + +module.exports = GlossaryRepository diff --git a/src/repositories/repositoryFactory.js b/src/repositories/repositoryFactory.js index 4750fffea..7f97e1177 100644 --- a/src/repositories/repositoryFactory.js +++ b/src/repositories/repositoryFactory.js @@ -7,6 +7,7 @@ const BaseOrgRepository = require('./baseOrgRepository') const BaseUserRepository = require('./baseUserRepository') const ConversationRepository = require('./conversationRepository') const ReviewObjectRepository = require('./reviewObjectRepository') +const GlossaryRepository = require('./glossaryRepository') class RepositoryFactory { getOrgRepository () { @@ -54,6 +55,11 @@ class RepositoryFactory { return repo } + getGlossaryRepository () { + const repo = new GlossaryRepository() + return repo + } + getAuditRepository () { const AuditRepository = require('./auditRepository') const repo = new AuditRepository() diff --git a/src/routes.config.js b/src/routes.config.js index 1cf2fe159..9cf95cdc3 100644 --- a/src/routes.config.js +++ b/src/routes.config.js @@ -12,6 +12,7 @@ const RegistryOrgController = require('./controller/registry-org.controller') const AuditController = require('./controller/audit.controller') const ConversationController = require('./controller/conversation.controller') const ReviewObjectController = require('./controller/review-object.controller') +const GlossaryController = require('./controller/glossary.controller') var options = { swaggerOptions: { @@ -40,6 +41,7 @@ module.exports = async function configureRoutes (app) { app.use('/api/', RegistryOrgController) app.use('/api/', ConversationController) app.use('/api/', ReviewObjectController) + app.use('/api/', GlossaryController) app.get('/api-docs/openapi.json', (req, res) => res.json(openApiSpecification)) app.use('/api-docs', swaggerUi.serveFiles(null, options), swaggerUi.setup(null, setupOptions)) app.use('/schemas/', SchemasController) diff --git a/src/scripts/migrate.js b/src/scripts/migrate.js index 7f4bd5879..a0097c1d6 100644 --- a/src/scripts/migrate.js +++ b/src/scripts/migrate.js @@ -65,6 +65,7 @@ async function run () { // Each helper handlers querying changes from srcDB and updating trgDB await orgHelper(db) await userHelper(db) + await glossaryHelper(db) } catch (err) { // Ensures that the client will close when you finish/error await dbClient.close() @@ -265,3 +266,13 @@ async function userHelper (db) { await trgUserCol.updateOne(trgQuery, updateDoc, options) } } + +async function glossaryHelper (db) { + console.log('Ensuring Glossary collection exists...') + // Create collection if it doesn't exist + await db.createCollection('Glossary').catch((err) => { + if (err.codeName !== 'NamespaceExists') { + console.warn('Could not create Glossary collection', err) + } + }) +} diff --git a/src/scripts/populate.js b/src/scripts/populate.js index 28fe3d057..c70713938 100644 --- a/src/scripts/populate.js +++ b/src/scripts/populate.js @@ -21,6 +21,7 @@ const BaseUser = require('../model/baseuser') const ReviewObject = require('../model/reviewobject') const Conversation = require('../model/conversation') const Audit = require('../model/audit') +const Glossary = require('../model/glossary') const error = new errors.IDRError() @@ -34,14 +35,16 @@ const populateTheseCollections = { BaseUser: BaseUser, ReviewObject: ReviewObject, Conversation: Conversation, - Audit: Audit + Audit: Audit, + Glossary: Glossary } const indexesToCreate = { Cve: [{ 'cve.cveMetadata.cveId': 1 }, { 'cve.cveMetadata.dateUpdated': 1 }], 'Cve-Id': [{ cve_id: 1 }, { owning_cna: 1, state: 1 }, { reserved: 1 }], User: [{ UUID: 1 }], - Org: [{ UUID: 1 }, { 'authority.active_roles': 1 }] + Org: [{ UUID: 1 }, { 'authority.active_roles': 1 }], + Glossary: [{ short_name: 1 }] } // Body Parser Middleware @@ -143,6 +146,7 @@ db.once('open', async () => { await Audit.createCollection() await ReviewObject.createCollection() await Conversation.createCollection() + await Glossary.createCollection() } catch (err) { logger.error('Error creating indexes:', err) } finally { diff --git a/test/integration-tests/glossary/glossaryCRUDTest.js b/test/integration-tests/glossary/glossaryCRUDTest.js new file mode 100644 index 000000000..dfe65fc87 --- /dev/null +++ b/test/integration-tests/glossary/glossaryCRUDTest.js @@ -0,0 +1,156 @@ +/* eslint-disable no-unused-expressions */ +const chai = require('chai') +const expect = chai.expect +chai.use(require('chai-http')) + +const constants = require('../constants.js') +const app = require('../../../src/index.js') + +const secretariatHeaders = { ...constants.headers, 'content-type': 'application/json' } + +const testGlossaryItem = { + short_name: 'test_glossary_item', + label: 'Test Glossary Item', + def: 'The definition of Test Glossary Item' +} + +describe('Testing /glossary endpoints', () => { + context('Testing POST /glossary endpoint', () => { + context('Positive Tests', () => { + it('Creates a new glossary item', async () => { + await chai.request(app) + .post('/api/glossary') + .set(secretariatHeaders) + .send(testGlossaryItem) + .then((res, err) => { + expect(err).to.be.undefined + expect(res).to.have.status(200) + + expect(res.body).to.haveOwnProperty('short_name') + expect(res.body.short_name).to.equal(testGlossaryItem.short_name) + + expect(res.body).to.haveOwnProperty('label') + expect(res.body.label).to.equal(testGlossaryItem.label) + + expect(res.body).to.haveOwnProperty('def') + expect(res.body.def).to.equal(testGlossaryItem.def) + }) + }) + }) + context('Negative Tests', () => { + it('Fails to create a new glossary item with an existing short name', async () => { + await chai.request(app) + .post('/api/glossary') + .set(secretariatHeaders) + .send(testGlossaryItem) + .then((res) => { + expect(res).to.have.status(400) + expect(res.body.message).to.equal('Parameters were invalid') + expect(res.body.details[0]).to.equal('Glossary item with this short_name already exists') + }) + }) + }) + }) + context('Testing GET /glossary endpoints', () => { + context('Positive Tests', () => { + it('Gets a list of all glossary items', async () => { + await chai.request(app) + .get('/api/glossary') + .set(secretariatHeaders) + .then((res) => { + expect(res).to.have.status(200) + expect(res.body.glossary).to.be.an('array').that.is.not.empty + }) + }) + it('Gets a glossary item by short name', async () => { + await chai.request(app) + .get(`/api/glossary/short_name/${testGlossaryItem.short_name}`) + .set(secretariatHeaders) + .then((res) => { + expect(res).to.have.status(200) + expect(res.body).to.have.property('label', testGlossaryItem.label) + expect(res.body).to.have.property('short_name', testGlossaryItem.short_name) + expect(res.body).to.have.property('def', testGlossaryItem.def) + }) + }) + }) + context('Negative Tests', () => { + it('Fails to get a glossary item that does not exist', async () => { + await chai.request(app) + .get('/api/glossary/short_name/nonexistent_item') + .set(secretariatHeaders) + .then((res) => { + expect(res).to.have.status(404) + expect(res.body.message).to.equal('404: resource not found') + }) + }) + }) + }) + context('Testing PUT /glossary endpoint', () => { + context('Positive Tests', () => { + it('Updates a glossary item', async () => { + await chai.request(app) + .put(`/api/glossary/short_name/${testGlossaryItem.short_name}`) + .set(secretariatHeaders) + .send({ + ...testGlossaryItem, + label: 'Updated Glossary Item', + def: 'Updated definition' + }) + .then((res, err) => { + expect(err).to.be.undefined + expect(res).to.have.status(200) + + expect(res.body).to.haveOwnProperty('short_name') + expect(res.body.short_name).to.equal(testGlossaryItem.short_name) + + expect(res.body).to.haveOwnProperty('label') + expect(res.body.label).to.equal('Updated Glossary Item') + + expect(res.body).to.haveOwnProperty('def') + expect(res.body.def).to.equal('Updated definition') + }) + }) + }) + context('Negative Tests', () => { + it('Fails to update a glossary item changing its short_name', async () => { + await chai.request(app) + .put(`/api/glossary/short_name/${testGlossaryItem.short_name}`) + .set(secretariatHeaders) + .send({ + ...testGlossaryItem, + short_name: 'new_short_name' + }) + .then((res) => { + expect(res).to.have.status(400) + expect(res.body.message).to.equal('Parameters were invalid') + expect(res.body.details[0]).to.equal('Cannot change short_name through this endpoint.') + }) + }) + }) + }) + context('Testing DELETE /glossary endpoint', () => { + context('Positive Tests', () => { + it('Deletes a glossary item', async () => { + await chai.request(app) + .delete(`/api/glossary/short_name/${testGlossaryItem.short_name}`) + .set(secretariatHeaders) + .then((res) => { + expect(res).to.have.status(200) + expect(res.body.message).to.equal('Glossary item deleted') + }) + }) + }) + context('Negative Tests', () => { + it('Fails to delete a glossary item that does not exist', async () => { + await chai.request(app) + .delete('/api/glossary/short_name/nonexistent_item') + .set(secretariatHeaders) + .then((res) => { + expect(res).to.have.status(404) + expect(res.body.message).to.equal('404: resource not found') + }) + }) + }) + }) +}) From 66dd9c609a078549340b857898b324fa0ac9fc21 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Tue, 7 Apr 2026 14:46:17 -0400 Subject: [PATCH 15/86] Added Glossary capability --- datadump/pre-population/glossary.json | 7 ++ .../create-glossary-item-response.json | 17 +++++ schemas/glossary/glossary.json | 4 +- .../glossary.controller.js | 33 +++++---- src/controller/glossary.controller/index.js | 18 ++--- src/model/glossary.js | 4 +- src/repositories/glossaryRepository.js | 14 ++-- src/scripts/populate.js | 8 ++- .../glossary/glossaryCRUDTest.js | 67 +++++++++++++------ 9 files changed, 119 insertions(+), 53 deletions(-) create mode 100644 datadump/pre-population/glossary.json create mode 100644 schemas/glossary/create-glossary-item-response.json diff --git a/datadump/pre-population/glossary.json b/datadump/pre-population/glossary.json new file mode 100644 index 000000000..ac9ccc4e7 --- /dev/null +++ b/datadump/pre-population/glossary.json @@ -0,0 +1,7 @@ +[ + { + "services_short_name": "long_name", + "label": "Long Name", + "def": "The full, official name of an organization participating in the CVE program." + } +] diff --git a/schemas/glossary/create-glossary-item-response.json b/schemas/glossary/create-glossary-item-response.json new file mode 100644 index 000000000..d4c4b2c38 --- /dev/null +++ b/schemas/glossary/create-glossary-item-response.json @@ -0,0 +1,17 @@ +{ + "$id": "https://cve.mitre.org/api-docs/schema/glossary/create-glossary-item-response.json", + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "glossary_item_added": { + "$ref": "glossary.json" + } + }, + "required": [ + "message", + "glossary_item_added" + ], + "additionalProperties": false +} diff --git a/schemas/glossary/glossary.json b/schemas/glossary/glossary.json index 0f6bbe633..d5e59f8b0 100644 --- a/schemas/glossary/glossary.json +++ b/schemas/glossary/glossary.json @@ -2,7 +2,7 @@ "$id": "https://cve.mitre.org/api-docs/schema/glossary/glossary.json", "type": "object", "properties": { - "short_name": { + "services_short_name": { "type": "string", "minLength": 1 }, @@ -16,7 +16,7 @@ } }, "required": [ - "short_name", + "services_short_name", "label", "def" ], diff --git a/src/controller/glossary.controller/glossary.controller.js b/src/controller/glossary.controller/glossary.controller.js index 18f0f2a8f..29410395c 100644 --- a/src/controller/glossary.controller/glossary.controller.js +++ b/src/controller/glossary.controller/glossary.controller.js @@ -30,9 +30,9 @@ async function getAllGlossaryItems (req, res, next) { async function getGlossaryItem (req, res, next) { try { const glossaryRepo = req.ctx.repositories.getGlossaryRepository() - const shortName = req.params.short_name + const servicesShortName = req.params.services_short_name - const result = await glossaryRepo.findOneByShortName(shortName) + const result = await glossaryRepo.findOneByServicesShortName(servicesShortName) if (!result) { return res.status(404).json(error.notFound()) } @@ -55,13 +55,22 @@ async function createGlossaryItem (req, res, next) { const glossaryRepo = req.ctx.repositories.getGlossaryRepository() const glossaryData = req.body - const existing = await glossaryRepo.findOneByShortName(glossaryData.short_name) + const existing = await glossaryRepo.findOneByServicesShortName(glossaryData.services_short_name) if (existing) { - return res.status(400).json(error.badInput(['Glossary item with this short_name already exists'])) + return res.status(400).json(error.badInput(['Glossary item with this services_short_name already exists'])) } - const result = await glossaryRepo.collection.create(glossaryData) - return res.status(200).json(result) + const createdDoc = await glossaryRepo.collection.create(glossaryData) + const result = createdDoc.toObject() + delete result._id + delete result.__v + delete result.createdAt + delete result.updatedAt + + return res.status(200).json({ + message: 'glossary item successfully added', + glossary_item_added: result + }) } catch (err) { next(err) } @@ -78,14 +87,14 @@ async function createGlossaryItem (req, res, next) { async function updateGlossaryItem (req, res, next) { try { const glossaryRepo = req.ctx.repositories.getGlossaryRepository() - const shortName = req.params.short_name + const servicesShortName = req.params.services_short_name const glossaryData = req.body - if (glossaryData.short_name && glossaryData.short_name !== shortName) { - return res.status(400).json(error.badInput(['Cannot change short_name through this endpoint.'])) + if (glossaryData.services_short_name && glossaryData.services_short_name !== servicesShortName) { + return res.status(400).json(error.badInput(['Cannot change services_short_name through this endpoint.'])) } - const result = await glossaryRepo.updateByShortName(shortName, glossaryData) + const result = await glossaryRepo.updateByServicesShortName(servicesShortName, glossaryData) if (!result) { return res.status(404).json(error.notFound()) } @@ -107,9 +116,9 @@ async function updateGlossaryItem (req, res, next) { async function deleteGlossaryItem (req, res, next) { try { const glossaryRepo = req.ctx.repositories.getGlossaryRepository() - const shortName = req.params.short_name + const servicesShortName = req.params.services_short_name - const result = await glossaryRepo.deleteByShortName(shortName) + const result = await glossaryRepo.deleteByServicesShortName(servicesShortName) if (!result) { return res.status(404).json(error.notFound()) } diff --git a/src/controller/glossary.controller/index.js b/src/controller/glossary.controller/index.js index 8d83cc8ca..d510651de 100644 --- a/src/controller/glossary.controller/index.js +++ b/src/controller/glossary.controller/index.js @@ -58,8 +58,8 @@ router.get('/glossary', controller.getAllGlossaryItems ) -// Get glossary item by short_name - SEC only -router.get('/glossary/short_name/:short_name', +// Get glossary item by services_short_name - SEC only +router.get('/glossary/:services_short_name', /* #swagger.tags = ['Glossary'] #swagger.operationId = 'glossarySingle' @@ -69,7 +69,7 @@ router.get('/glossary/short_name/:short_name',

User must belong to an organization with the Secretariat role

Expected Behavior

Secretariat: Retrieves the specified glossary item

" - #swagger.parameters['short_name'] = { description: 'The short name of the glossary item' } + #swagger.parameters['services_short_name'] = { description: 'The short name of the glossary item' } #swagger.parameters['$ref'] = [ '#/components/parameters/apiEntityHeader', '#/components/parameters/apiUserHeader', @@ -150,11 +150,11 @@ router.post('/glossary', } } #swagger.responses[200] = { - description: 'Returns the created glossary item', + description: 'Returns the created glossary item wrapped in a success message', content: { "application/json": { schema: { - $ref: '../schemas/glossary/glossary.json' + $ref: '../schemas/glossary/create-glossary-item-response.json' } } } @@ -198,7 +198,7 @@ router.post('/glossary', ) // Update a glossary item - SEC only -router.put('/glossary/short_name/:short_name', +router.put('/glossary/:services_short_name', /* #swagger.tags = ['Glossary'] #swagger.operationId = 'glossaryUpdate' @@ -208,7 +208,7 @@ router.put('/glossary/short_name/:short_name',

User must belong to an organization with the Secretariat role

Expected Behavior

Secretariat: Updates the specified glossary item

" - #swagger.parameters['short_name'] = { description: 'The short name of the glossary item' } + #swagger.parameters['services_short_name'] = { description: 'The short name of the glossary item' } #swagger.parameters['$ref'] = [ '#/components/parameters/apiEntityHeader', '#/components/parameters/apiUserHeader', @@ -281,7 +281,7 @@ router.put('/glossary/short_name/:short_name', ) // Delete a glossary item - SEC only -router.delete('/glossary/short_name/:short_name', +router.delete('/glossary/:services_short_name', /* #swagger.tags = ['Glossary'] #swagger.operationId = 'glossaryDelete' @@ -291,7 +291,7 @@ router.delete('/glossary/short_name/:short_name',

User must belong to an organization with the Secretariat role

Expected Behavior

Secretariat: Deletes the specified glossary item

" - #swagger.parameters['short_name'] = { description: 'The short name of the glossary item' } + #swagger.parameters['services_short_name'] = { description: 'The short name of the glossary item' } #swagger.parameters['$ref'] = [ '#/components/parameters/apiEntityHeader', '#/components/parameters/apiUserHeader', diff --git a/src/model/glossary.js b/src/model/glossary.js index fd9e9a2b5..d06f09a61 100644 --- a/src/model/glossary.js +++ b/src/model/glossary.js @@ -1,14 +1,14 @@ const mongoose = require('mongoose') const schema = { - short_name: { type: String, required: true }, + services_short_name: { type: String, required: true }, label: { type: String, required: true }, def: { type: String, required: true } } const GlossarySchema = new mongoose.Schema(schema, { collection: 'Glossary', timestamps: { createdAt: 'createdAt', updatedAt: 'updatedAt' } }) -GlossarySchema.index({ short_name: 1 }, { unique: true }) +GlossarySchema.index({ services_short_name: 1 }, { unique: true }) const Glossary = mongoose.model('Glossary', GlossarySchema) module.exports = Glossary diff --git a/src/repositories/glossaryRepository.js b/src/repositories/glossaryRepository.js index 934cce610..3ff74abe4 100644 --- a/src/repositories/glossaryRepository.js +++ b/src/repositories/glossaryRepository.js @@ -7,19 +7,19 @@ class GlossaryRepository extends BaseRepository { } async getAll () { - return this.find({}, { multiple: true }) + return this.collection.find({}, { _id: 0, __v: 0, createdAt: 0, updatedAt: 0 }).exec() } - async findOneByShortName (shortName) { - return this.findOne({ short_name: shortName }) + async findOneByServicesShortName (servicesShortName) { + return this.collection.findOne({ services_short_name: servicesShortName }, { _id: 0, __v: 0, createdAt: 0, updatedAt: 0 }).exec() } - async updateByShortName (shortName, newGlossaryData) { - return this.findOneAndUpdate({ short_name: shortName }, newGlossaryData) + async updateByServicesShortName (servicesShortName, newGlossaryData) { + return this.collection.findOneAndUpdate({ services_short_name: servicesShortName }, newGlossaryData, { projection: { _id: 0, __v: 0, createdAt: 0, updatedAt: 0 }, new: true }).exec() } - async deleteByShortName (shortName) { - return this.collection.findOneAndDelete({ short_name: shortName }) + async deleteByServicesShortName (servicesShortName) { + return this.collection.findOneAndDelete({ services_short_name: servicesShortName }, { projection: { _id: 0, __v: 0, createdAt: 0, updatedAt: 0 } }).exec() } } diff --git a/src/scripts/populate.js b/src/scripts/populate.js index c70713938..b92659901 100644 --- a/src/scripts/populate.js +++ b/src/scripts/populate.js @@ -44,7 +44,7 @@ const indexesToCreate = { 'Cve-Id': [{ cve_id: 1 }, { owning_cna: 1, state: 1 }, { reserved: 1 }], User: [{ UUID: 1 }], Org: [{ UUID: 1 }, { 'authority.active_roles': 1 }], - Glossary: [{ short_name: 1 }] + Glossary: [{ services_short_name: 1 }] } // Body Parser Middleware @@ -125,6 +125,12 @@ db.once('open', async () => { CveId, dataUtils.newCveIdTransform )) + // Glossary + populatePromises.push(dataUtils.populateCollection( + './datadump/pre-population/glossary.json', + Glossary + )) + // don't close database connection until all remaining populate // promises are resolved Promise.all(populatePromises).then(async function () { diff --git a/test/integration-tests/glossary/glossaryCRUDTest.js b/test/integration-tests/glossary/glossaryCRUDTest.js index dfe65fc87..921fc3c10 100644 --- a/test/integration-tests/glossary/glossaryCRUDTest.js +++ b/test/integration-tests/glossary/glossaryCRUDTest.js @@ -9,7 +9,7 @@ const app = require('../../../src/index.js') const secretariatHeaders = { ...constants.headers, 'content-type': 'application/json' } const testGlossaryItem = { - short_name: 'test_glossary_item', + services_short_name: 'test_glossary_item', label: 'Test Glossary Item', def: 'The definition of Test Glossary Item' } @@ -26,14 +26,25 @@ describe('Testing /glossary endpoints', () => { expect(err).to.be.undefined expect(res).to.have.status(200) - expect(res.body).to.haveOwnProperty('short_name') - expect(res.body.short_name).to.equal(testGlossaryItem.short_name) + expect(res.body).to.haveOwnProperty('message') + expect(res.body.message).to.equal('glossary item successfully added') - expect(res.body).to.haveOwnProperty('label') - expect(res.body.label).to.equal(testGlossaryItem.label) + expect(res.body).to.haveOwnProperty('glossary_item_added') + const item = res.body.glossary_item_added - expect(res.body).to.haveOwnProperty('def') - expect(res.body.def).to.equal(testGlossaryItem.def) + expect(item).to.haveOwnProperty('services_short_name') + expect(item.services_short_name).to.equal(testGlossaryItem.services_short_name) + + expect(item).to.haveOwnProperty('label') + expect(item.label).to.equal(testGlossaryItem.label) + + expect(item).to.haveOwnProperty('def') + expect(item.def).to.equal(testGlossaryItem.def) + + expect(item).to.not.have.property('_id') + expect(item).to.not.have.property('__v') + expect(item).to.not.have.property('createdAt') + expect(item).to.not.have.property('updatedAt') }) }) }) @@ -46,7 +57,7 @@ describe('Testing /glossary endpoints', () => { .then((res) => { expect(res).to.have.status(400) expect(res.body.message).to.equal('Parameters were invalid') - expect(res.body.details[0]).to.equal('Glossary item with this short_name already exists') + expect(res.body.details[0]).to.equal('Glossary item with this services_short_name already exists') }) }) }) @@ -60,24 +71,35 @@ describe('Testing /glossary endpoints', () => { .then((res) => { expect(res).to.have.status(200) expect(res.body.glossary).to.be.an('array').that.is.not.empty + res.body.glossary.forEach(item => { + expect(item).to.not.have.property('_id') + expect(item).to.not.have.property('__v') + expect(item).to.not.have.property('createdAt') + expect(item).to.not.have.property('updatedAt') + }) }) }) it('Gets a glossary item by short name', async () => { await chai.request(app) - .get(`/api/glossary/short_name/${testGlossaryItem.short_name}`) + .get(`/api/glossary/${testGlossaryItem.services_short_name}`) .set(secretariatHeaders) .then((res) => { expect(res).to.have.status(200) expect(res.body).to.have.property('label', testGlossaryItem.label) - expect(res.body).to.have.property('short_name', testGlossaryItem.short_name) + expect(res.body).to.have.property('services_short_name', testGlossaryItem.services_short_name) expect(res.body).to.have.property('def', testGlossaryItem.def) + + expect(res.body).to.not.have.property('_id') + expect(res.body).to.not.have.property('__v') + expect(res.body).to.not.have.property('createdAt') + expect(res.body).to.not.have.property('updatedAt') }) }) }) context('Negative Tests', () => { it('Fails to get a glossary item that does not exist', async () => { await chai.request(app) - .get('/api/glossary/short_name/nonexistent_item') + .get('/api/glossary/nonexistent_item') .set(secretariatHeaders) .then((res) => { expect(res).to.have.status(404) @@ -90,7 +112,7 @@ describe('Testing /glossary endpoints', () => { context('Positive Tests', () => { it('Updates a glossary item', async () => { await chai.request(app) - .put(`/api/glossary/short_name/${testGlossaryItem.short_name}`) + .put(`/api/glossary/${testGlossaryItem.services_short_name}`) .set(secretariatHeaders) .send({ ...testGlossaryItem, @@ -101,30 +123,35 @@ describe('Testing /glossary endpoints', () => { expect(err).to.be.undefined expect(res).to.have.status(200) - expect(res.body).to.haveOwnProperty('short_name') - expect(res.body.short_name).to.equal(testGlossaryItem.short_name) + expect(res.body).to.haveOwnProperty('services_short_name') + expect(res.body.services_short_name).to.equal(testGlossaryItem.services_short_name) expect(res.body).to.haveOwnProperty('label') expect(res.body.label).to.equal('Updated Glossary Item') expect(res.body).to.haveOwnProperty('def') expect(res.body.def).to.equal('Updated definition') + + expect(res.body).to.not.have.property('_id') + expect(res.body).to.not.have.property('__v') + expect(res.body).to.not.have.property('createdAt') + expect(res.body).to.not.have.property('updatedAt') }) }) }) context('Negative Tests', () => { - it('Fails to update a glossary item changing its short_name', async () => { + it('Fails to update a glossary item changing its services_short_name', async () => { await chai.request(app) - .put(`/api/glossary/short_name/${testGlossaryItem.short_name}`) + .put(`/api/glossary/${testGlossaryItem.services_short_name}`) .set(secretariatHeaders) .send({ ...testGlossaryItem, - short_name: 'new_short_name' + services_short_name: 'new_services_short_name' }) .then((res) => { expect(res).to.have.status(400) expect(res.body.message).to.equal('Parameters were invalid') - expect(res.body.details[0]).to.equal('Cannot change short_name through this endpoint.') + expect(res.body.details[0]).to.equal('Cannot change services_short_name through this endpoint.') }) }) }) @@ -133,7 +160,7 @@ describe('Testing /glossary endpoints', () => { context('Positive Tests', () => { it('Deletes a glossary item', async () => { await chai.request(app) - .delete(`/api/glossary/short_name/${testGlossaryItem.short_name}`) + .delete(`/api/glossary/${testGlossaryItem.services_short_name}`) .set(secretariatHeaders) .then((res) => { expect(res).to.have.status(200) @@ -144,7 +171,7 @@ describe('Testing /glossary endpoints', () => { context('Negative Tests', () => { it('Fails to delete a glossary item that does not exist', async () => { await chai.request(app) - .delete('/api/glossary/short_name/nonexistent_item') + .delete('/api/glossary/nonexistent_item') .set(secretariatHeaders) .then((res) => { expect(res).to.have.status(404) From 3a0ef426cef9b441d38d8413048b1846c4d8872e Mon Sep 17 00:00:00 2001 From: david-rocca Date: Tue, 7 Apr 2026 14:56:00 -0400 Subject: [PATCH 16/86] Make the returns a little bit better --- .../create-glossary-item-response.json | 4 +-- .../update-glossary-item-response.json | 17 +++++++++++ .../glossary.controller.js | 7 +++-- src/controller/glossary.controller/index.js | 4 +-- .../glossary/glossaryCRUDTest.js | 30 +++++++++++-------- 5 files changed, 44 insertions(+), 18 deletions(-) create mode 100644 schemas/glossary/update-glossary-item-response.json diff --git a/schemas/glossary/create-glossary-item-response.json b/schemas/glossary/create-glossary-item-response.json index d4c4b2c38..c4d5a2394 100644 --- a/schemas/glossary/create-glossary-item-response.json +++ b/schemas/glossary/create-glossary-item-response.json @@ -5,13 +5,13 @@ "message": { "type": "string" }, - "glossary_item_added": { + "created": { "$ref": "glossary.json" } }, "required": [ "message", - "glossary_item_added" + "created" ], "additionalProperties": false } diff --git a/schemas/glossary/update-glossary-item-response.json b/schemas/glossary/update-glossary-item-response.json new file mode 100644 index 000000000..64bc27db6 --- /dev/null +++ b/schemas/glossary/update-glossary-item-response.json @@ -0,0 +1,17 @@ +{ + "$id": "https://cve.mitre.org/api-docs/schema/glossary/update-glossary-item-response.json", + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "updated": { + "$ref": "glossary.json" + } + }, + "required": [ + "message", + "updated" + ], + "additionalProperties": false +} diff --git a/src/controller/glossary.controller/glossary.controller.js b/src/controller/glossary.controller/glossary.controller.js index 29410395c..b55d65850 100644 --- a/src/controller/glossary.controller/glossary.controller.js +++ b/src/controller/glossary.controller/glossary.controller.js @@ -69,7 +69,7 @@ async function createGlossaryItem (req, res, next) { return res.status(200).json({ message: 'glossary item successfully added', - glossary_item_added: result + created: result }) } catch (err) { next(err) @@ -99,7 +99,10 @@ async function updateGlossaryItem (req, res, next) { return res.status(404).json(error.notFound()) } - return res.status(200).json(result) + return res.status(200).json({ + message: 'glossary item successfully updated', + updated: result + }) } catch (err) { next(err) } diff --git a/src/controller/glossary.controller/index.js b/src/controller/glossary.controller/index.js index d510651de..8c374131b 100644 --- a/src/controller/glossary.controller/index.js +++ b/src/controller/glossary.controller/index.js @@ -225,11 +225,11 @@ router.put('/glossary/:services_short_name', } } #swagger.responses[200] = { - description: 'Returns the updated glossary item', + description: 'Returns the updated glossary item wrapped in a success message', content: { "application/json": { schema: { - $ref: '../schemas/glossary/glossary.json' + $ref: '../schemas/glossary/update-glossary-item-response.json' } } } diff --git a/test/integration-tests/glossary/glossaryCRUDTest.js b/test/integration-tests/glossary/glossaryCRUDTest.js index 921fc3c10..844f4b4c2 100644 --- a/test/integration-tests/glossary/glossaryCRUDTest.js +++ b/test/integration-tests/glossary/glossaryCRUDTest.js @@ -29,8 +29,8 @@ describe('Testing /glossary endpoints', () => { expect(res.body).to.haveOwnProperty('message') expect(res.body.message).to.equal('glossary item successfully added') - expect(res.body).to.haveOwnProperty('glossary_item_added') - const item = res.body.glossary_item_added + expect(res.body).to.haveOwnProperty('created') + const item = res.body.created expect(item).to.haveOwnProperty('services_short_name') expect(item.services_short_name).to.equal(testGlossaryItem.services_short_name) @@ -123,19 +123,25 @@ describe('Testing /glossary endpoints', () => { expect(err).to.be.undefined expect(res).to.have.status(200) - expect(res.body).to.haveOwnProperty('services_short_name') - expect(res.body.services_short_name).to.equal(testGlossaryItem.services_short_name) + expect(res.body).to.haveOwnProperty('message') + expect(res.body.message).to.equal('glossary item successfully updated') - expect(res.body).to.haveOwnProperty('label') - expect(res.body.label).to.equal('Updated Glossary Item') + expect(res.body).to.haveOwnProperty('updated') + const item = res.body.updated - expect(res.body).to.haveOwnProperty('def') - expect(res.body.def).to.equal('Updated definition') + expect(item).to.haveOwnProperty('services_short_name') + expect(item.services_short_name).to.equal(testGlossaryItem.services_short_name) - expect(res.body).to.not.have.property('_id') - expect(res.body).to.not.have.property('__v') - expect(res.body).to.not.have.property('createdAt') - expect(res.body).to.not.have.property('updatedAt') + expect(item).to.haveOwnProperty('label') + expect(item.label).to.equal('Updated Glossary Item') + + expect(item).to.haveOwnProperty('def') + expect(item.def).to.equal('Updated definition') + + expect(item).to.not.have.property('_id') + expect(item).to.not.have.property('__v') + expect(item).to.not.have.property('createdAt') + expect(item).to.not.have.property('updatedAt') }) }) }) From 626fd5ef8804355298cf0828198f2c16bc771202 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Wed, 29 Apr 2026 12:46:53 -0400 Subject: [PATCH 17/86] Unlock for development --- src/controller/org.controller/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controller/org.controller/index.js b/src/controller/org.controller/index.js index f7805c496..1c5e6f110 100644 --- a/src/controller/org.controller/index.js +++ b/src/controller/org.controller/index.js @@ -640,7 +640,7 @@ router.put('/registry/org/:shortname', */ mw.useRegistry(), mw.validateUser, - mw.onlySecretariat, + // mw.onlySecretariat, parseError, parsePutParams, registryOrgController.UPDATE_ORG From dfd154069e31a1ac38e020abc151add96b9aeeb0 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Thu, 23 Apr 2026 14:46:19 -0400 Subject: [PATCH 18/86] Added restricted fields, changed how data is returned to the user, secretariat only editing fields --- schemas/registry-org/BaseOrg.json | 24 ++++ schemas/registry-org/CNAOrg.json | 7 +- .../get-registry-org-response.json | 28 ++++ .../list-registry-orgs-response.json | 28 ++++ src/constants/index.js | 13 +- src/controller/org.controller/error.js | 7 + .../org.controller/org.controller.js | 11 ++ .../org.controller/org.middleware.js | 30 +++++ .../registry-org.controller/error.js | 7 + .../registry-org.controller.js | 24 +++- .../registry-org.middleware.js | 6 + src/model/baseorg.js | 8 ++ src/repositories/baseOrgRepository.js | 120 +++++++++--------- .../org/registryOrgAsOrgAdmin.js | 15 +++ .../registry-org/registryOrgCRUDTest.js | 26 ++++ 15 files changed, 288 insertions(+), 66 deletions(-) diff --git a/schemas/registry-org/BaseOrg.json b/schemas/registry-org/BaseOrg.json index d2a42bbf5..63f81c9ac 100644 --- a/schemas/registry-org/BaseOrg.json +++ b/schemas/registry-org/BaseOrg.json @@ -133,6 +133,30 @@ } }, "additionalProperties": false + }, + "partner_data": { + "type": "object", + "properties": { + "cve_website_update_date": { + "type": "string", + "format": "date-time" + }, + "cve_website_update_needed": { + "type": "boolean" + }, + "partner_active_date": { + "type": "string", + "format": "date-time" + }, + "partner_inactive_date": { + "type": "string", + "format": "date-time" + }, + "status": { + "type": "string" + } + }, + "additionalProperties": false } }, "required": [ diff --git a/schemas/registry-org/CNAOrg.json b/schemas/registry-org/CNAOrg.json index 367302530..fc8fc5cc9 100644 --- a/schemas/registry-org/CNAOrg.json +++ b/schemas/registry-org/CNAOrg.json @@ -15,6 +15,7 @@ "minLength": 2, "maxLength": 32 }, + "aliases": { "$ref": "/BaseOrg#/properties/aliases" }, "contact_info": { "type": "object", "properties": { @@ -64,12 +65,16 @@ "partner_role": { "type": "string" }, + "partner_number": { + "type": "string" + }, "partner_type": { "type": "string" }, "partner_country": { "type": "string" - } + }, + "partner_data": { "$ref": "/BaseOrg#/properties/partner_data" } }, "required": ["short_name", "hard_quota"] } \ No newline at end of file diff --git a/schemas/registry-org/get-registry-org-response.json b/schemas/registry-org/get-registry-org-response.json index 79ac6a30d..b5390cbaa 100644 --- a/schemas/registry-org/get-registry-org-response.json +++ b/schemas/registry-org/get-registry-org-response.json @@ -109,6 +109,10 @@ "type": "string", "description": "Role of the partner" }, + "partner_number": { + "type": "string", + "description": "Number of the partner" + }, "partner_type": { "type": "string", "description": "Type of the partner" @@ -117,6 +121,30 @@ "type": "string", "description": "Country of the partner" }, + "partner_data": { + "type": "object", + "properties": { + "cve_website_update_date": { + "type": "string", + "format": "date-time" + }, + "cve_website_update_needed": { + "type": "boolean" + }, + "partner_active_date": { + "type": "string", + "format": "date-time" + }, + "partner_inactive_date": { + "type": "string", + "format": "date-time" + }, + "status": { + "type": "string" + } + }, + "description": "Additional partner metadata (restricted)" + }, "vulnerability_advisory_locations": { "type": "array", "items": { diff --git a/schemas/registry-org/list-registry-orgs-response.json b/schemas/registry-org/list-registry-orgs-response.json index c578c6f7a..93eacb702 100644 --- a/schemas/registry-org/list-registry-orgs-response.json +++ b/schemas/registry-org/list-registry-orgs-response.json @@ -138,6 +138,10 @@ "type": "string", "description": "Role of the partner" }, + "partner_number": { + "type": "string", + "description": "Number of the partner" + }, "partner_type": { "type": "string", "description": "Type of the partner" @@ -146,6 +150,30 @@ "type": "string", "description": "Country of the partner" }, + "partner_data": { + "type": "object", + "properties": { + "cve_website_update_date": { + "type": "string", + "format": "date-time" + }, + "cve_website_update_needed": { + "type": "boolean" + }, + "partner_active_date": { + "type": "string", + "format": "date-time" + }, + "partner_inactive_date": { + "type": "string", + "format": "date-time" + }, + "status": { + "type": "string" + } + }, + "description": "Additional partner metadata (restricted)" + }, "vulnerability_advisory_locations": { "type": "array", "items": { diff --git a/src/constants/index.js b/src/constants/index.js index 69f295e9f..40241631a 100644 --- a/src/constants/index.js +++ b/src/constants/index.js @@ -44,8 +44,19 @@ function getConstants () { USER_ROLES: [ 'ADMIN' ], - JOINT_APPROVAL_FIELDS: ['short_name', 'long_name', 'authority', 'aliases', 'oversees', 'root_or_tlr', 'charter_or_scope', 'product_list', 'disclosure_policy', 'contact_info.poc', 'contact_info.poc_email', 'contact_info.poc_phone', 'contact_info.org_email', 'partner_role', 'partner_type', 'partner_country', 'vulnerability_advisory_locations', 'advisory_location_require_credentials', 'industry', 'tl_root_start_date', 'is_cna_discussion_list', 'hard_quota'], + JOINT_APPROVAL_FIELDS: ['short_name', 'long_name', 'authority', 'aliases', 'oversees', 'root_or_tlr', 'charter_or_scope', 'product_list', 'disclosure_policy', 'contact_info.poc', 'contact_info.poc_email', 'contact_info.poc_phone', 'contact_info.org_email', 'partner_role', 'partner_number', 'partner_type', 'partner_country', 'partner_data.cve_website_update_date', 'partner_data.cve_website_update_needed', 'partner_data.partner_active_date', 'partner_data.partner_inactive_date', 'partner_data.status', 'vulnerability_advisory_locations', 'advisory_location_require_credentials', 'industry', 'tl_root_start_date', 'is_cna_discussion_list', 'hard_quota'], JOINT_APPROVAL_FIELDS_LEGACY: ['short_name', 'name', 'authority.active_roles', 'policies.id_quota'], + ORG_EXCLUDED_FIELDS: ['__t', '__v', '_id', 'inUse', 'in_use'], + ORG_RESTRICTED_FIELDS: ['partner_data'], + SECRETARIAT_ONLY_FIELDS: [ + 'partner_number', + 'partner_data', + 'partner_data.cve_website_update_date', + 'partner_data.cve_website_update_needed', + 'partner_data.partner_active_date', + 'partner_data.partner_inactive_date', + 'partner_data.status' + ], USER_ROLE_ENUM: { ADMIN: 'ADMIN' }, diff --git a/src/controller/org.controller/error.js b/src/controller/org.controller/error.js index 4c4df5fd3..cde82381f 100644 --- a/src/controller/org.controller/error.js +++ b/src/controller/org.controller/error.js @@ -91,6 +91,13 @@ class OrgControllerError extends idrErr.IDRError { err.message = 'The requested user can not be created and added to the organization because the organization has hit its limit of 100 users. Contact the Secretariat.' return err } + + secretariatOnlyEditing (fields) { + const err = {} + err.error = 'SECRETARIAT_ONLY' + err.message = `The following fields can only be modified by the Secretariat: ${fields.join(', ')}.` + return err + } } module.exports = { diff --git a/src/controller/org.controller/org.controller.js b/src/controller/org.controller/org.controller.js index 297887707..40f58c0f9 100644 --- a/src/controller/org.controller/org.controller.js +++ b/src/controller/org.controller/org.controller.js @@ -5,6 +5,7 @@ const getConstants = require('../../constants').getConstants const errors = require('./error') const error = new errors.OrgControllerError() const validateUUID = require('uuid').validate +const _ = require('lodash') /** * Get the details of all orgs. @@ -353,6 +354,16 @@ async function updateOrg (req, res, next) { const requestingUserUUID = await userRepo.getUserUUID(req.ctx.user, req.ctx.org, { session }) const isSecretariat = await orgRepository.isSecretariatByShortName(req.ctx.org, { session }) const isAdmin = await userRepo.isAdmin(req.ctx.user, req.ctx.org, { session }) + + if (!isSecretariat) { + const secretariatOnlyFields = getConstants().SECRETARIAT_ONLY_FIELDS + const restrictedFieldsSent = secretariatOnlyFields.filter(field => _.has(queryParametersJson, field)) + if (restrictedFieldsSent.length > 0) { + logger.info({ uuid: req.ctx.uuid, message: `Non-secretariat attempted to edit restricted fields: ${restrictedFieldsSent.join(', ')}` }) + await session.abortTransaction() + return res.status(403).json(error.secretariatOnlyEditing(restrictedFieldsSent)) + } + } const updatedOrg = await orgRepository.updateOrg(shortNameUrlParameter, queryParametersJson, { session }, !req.useRegistry, requestingUserUUID, isAdmin, isSecretariat) responseMessage = { message: `${updatedOrg.short_name} organization was successfully updated.`, updated: updatedOrg } // Clarify message diff --git a/src/controller/org.controller/org.middleware.js b/src/controller/org.controller/org.middleware.js index 204f6066b..78bdba0b5 100644 --- a/src/controller/org.controller/org.middleware.js +++ b/src/controller/org.controller/org.middleware.js @@ -60,6 +60,16 @@ function validateCreateOrgParameters () { body(['is_cna_discussion_list']) .default(false) .isBoolean(), + body([ + 'partner_data.cve_website_update_date', + 'partner_data.partner_active_date', + 'partner_data.partner_inactive_date' + ]) + .optional({ nullable: true }) + .isDate(), + body(['partner_data.cve_website_update_needed']) + .optional() + .isBoolean(), body( [ 'charter_or_scope', @@ -71,8 +81,10 @@ function validateCreateOrgParameters () { 'contact_info.org_email', 'contact_info.website', 'partner_role', + 'partner_number', 'partner_type', 'partner_country', + 'partner_data.status', 'industry' ]) .default('') @@ -135,8 +147,14 @@ function validateCreateOrgParameters () { 'contact_info.additional_contact_users', 'contact_info.website', 'partner_role', + 'partner_number', 'partner_type', 'partner_country', + 'partner_data.cve_website_update_date', + 'partner_data.cve_website_update_needed', + 'partner_data.partner_active_date', + 'partner_data.partner_inactive_date', + 'partner_data.status', 'vulnerability_advisory_locations', 'advisory_location_require_credentials', 'industry', @@ -220,8 +238,14 @@ function validateUpdateOrgParameters () { 'contact_info.org_email', 'contact_info.website', 'partner_role', + 'partner_number', 'partner_type', 'partner_country', + 'partner_data.cve_website_update_date', + 'partner_data.cve_website_update_needed', + 'partner_data.partner_active_date', + 'partner_data.partner_inactive_date', + 'partner_data.status', 'vulnerability_advisory_locations', 'advisory_location_require_credentials', 'industry', @@ -309,8 +333,14 @@ const QUERY_PARAMETERS = { 'contact_info.org_email', 'contact_info.website', 'partner_role', + 'partner_number', 'partner_type', 'partner_country', + 'partner_data.cve_website_update_date', + 'partner_data.cve_website_update_needed', + 'partner_data.partner_active_date', + 'partner_data.partner_inactive_date', + 'partner_data.status', 'vulnerability_advisory_locations', 'advisory_location_require_credentials', 'industry', diff --git a/src/controller/registry-org.controller/error.js b/src/controller/registry-org.controller/error.js index d4af5f1e6..aa54d2fc1 100644 --- a/src/controller/registry-org.controller/error.js +++ b/src/controller/registry-org.controller/error.js @@ -119,6 +119,13 @@ class RegistryOrgControllerError extends idrErr.IDRError { err.message = 'Parameters were invalid: conversation must be an object with a body.' return err } + + secretariatOnlyEditing (fields) { + const err = {} + err.error = 'SECRETARIAT_ONLY' + err.message = `The following fields can only be modified by the Secretariat: ${fields.join(', ')}.` + return err + } } module.exports = { diff --git a/src/controller/registry-org.controller/registry-org.controller.js b/src/controller/registry-org.controller/registry-org.controller.js index 64da4d038..676a10356 100644 --- a/src/controller/registry-org.controller/registry-org.controller.js +++ b/src/controller/registry-org.controller/registry-org.controller.js @@ -37,7 +37,7 @@ async function getAllOrgs (req, res, next) { options.page = req.ctx.query.page ? parseInt(req.ctx.query.page) : CONSTANTS.PAGINATOR_PAGE // if 'page' query parameter is not defined, set 'page' to the default page value try { - returnValue = await repo.getAllOrgs({ ...options }) + returnValue = await repo.getAllOrgs({ ...options }, false, isSecretariat) // fetch conversations for (let i = 0; i < returnValue.organizations.length; i++) { const conversation = await conversationRepo.getAllByTargetUUID(returnValue.organizations[i].UUID, isSecretariat) @@ -89,8 +89,7 @@ async function getOrg (req, res, next) { logger.info({ uuid: req.ctx.uuid, message: identifier + ' organization can only be viewed by the users of the same organization or the Secretariat.' }) return res.status(403).json(error.notSameOrgOrSecretariat()) } - - returnValue = await repo.getOrg(identifier, identifierIsUUID) + returnValue = await repo.getOrg(identifier, identifierIsUUID, {}, false, isSecretariat) if (returnValue) { // fetch conversation @@ -145,6 +144,15 @@ async function createOrg (req, res, next) { return res.status(400).json(error.uuidProvided('org')) } + if (!isSecretariat) { + const secretariatOnlyFields = getConstants().SECRETARIAT_ONLY_FIELDS + const restrictedFieldsSent = secretariatOnlyFields.filter(field => _.has(body, field)) + if (restrictedFieldsSent.length > 0) { + logger.info({ uuid: req.ctx.uuid, message: `Non-secretariat attempted to edit restricted fields: ${restrictedFieldsSent.join(', ')}` }) + return res.status(403).json(error.secretariatOnlyEditing(restrictedFieldsSent)) + } + } + try { session.startTransaction() const result = repo.validateOrg(body, { session }) @@ -258,6 +266,16 @@ async function updateOrg (req, res, next) { return res.status(403).json(error.notSameOrgOrSecretariat()) } + if (!isSecretariat) { + const secretariatOnlyFields = getConstants().SECRETARIAT_ONLY_FIELDS + const restrictedFieldsSent = secretariatOnlyFields.filter(field => _.has(body, field)) + if (restrictedFieldsSent.length > 0) { + logger.info({ uuid: req.ctx.uuid, message: `Non-secretariat attempted to edit restricted fields: ${restrictedFieldsSent.join(', ')}` }) + await session.abortTransaction() + return res.status(403).json(error.secretariatOnlyEditing(restrictedFieldsSent)) + } + } + // Edge Case: if a user has requested an org, but it is not approved yet, then we need to check to see if if there is a review org for the shortname request. if (!org) { diff --git a/src/controller/registry-org.controller/registry-org.middleware.js b/src/controller/registry-org.controller/registry-org.middleware.js index 3b1c51a81..e51511ffb 100644 --- a/src/controller/registry-org.controller/registry-org.middleware.js +++ b/src/controller/registry-org.controller/registry-org.middleware.js @@ -18,8 +18,14 @@ function parsePostParams (req, res, next) { 'contact_info.additional_contact_users', 'contact_info.poc', 'contact_info.poc_email', 'contact_info.poc_phone', 'contact_info.admins', 'contact_info.org_email', 'contact_info.website', 'partner_role', + 'partner_number', 'partner_type', 'partner_country', + 'partner_data.cve_website_update_date', + 'partner_data.cve_website_update_needed', + 'partner_data.partner_active_date', + 'partner_data.partner_inactive_date', + 'partner_data.status', 'vulnerability_advisory_locations', 'advisory_location_require_credentials', 'industry', diff --git a/src/model/baseorg.js b/src/model/baseorg.js index daff859fc..f73218164 100644 --- a/src/model/baseorg.js +++ b/src/model/baseorg.js @@ -23,8 +23,16 @@ const schema = { website: String }, partner_role: String, + partner_number: String, partner_type: String, partner_country: String, + partner_data: { + cve_website_update_date: Date, + cve_website_update_needed: Boolean, + partner_active_date: Date, + partner_inactive_date: Date, + status: String + }, vulnerability_advisory_locations: [String], advisory_location_require_credentials: Boolean, industry: String, diff --git a/src/repositories/baseOrgRepository.js b/src/repositories/baseOrgRepository.js index a95065e7a..d296c41c5 100644 --- a/src/repositories/baseOrgRepository.js +++ b/src/repositories/baseOrgRepository.js @@ -49,7 +49,23 @@ function setAggregateOrgObj (query) { * @param {object} query - The query object to match. * @returns {Array} The aggregation pipeline. */ -function setAggregateRegistryOrgObj (query) { +function setAggregateRegistryOrgObj (query, isSecretariat = false) { + const CONSTANTS = getConstants() + const projection = { + _id: false, + __t: false, + __v: false, + inUse: false, + in_use: false, + parentOrg: false + } + + if (!isSecretariat) { + CONSTANTS.ORG_RESTRICTED_FIELDS.forEach(field => { + projection[field] = false + }) + } + return [ { $match: query @@ -74,17 +90,35 @@ function setAggregateRegistryOrgObj (query) { } }, { - $project: { - _id: false, - __t: false, - inUse: false, - in_use: false, - parentOrg: false - } + $project: projection } ] } +function getOrgProjection (isSecretariat = false) { + const CONSTANTS = getConstants() + const projection = {} + CONSTANTS.ORG_EXCLUDED_FIELDS.forEach(field => { + projection[field] = 0 + }) + if (!isSecretariat) { + CONSTANTS.ORG_RESTRICTED_FIELDS.forEach(field => { + projection[field] = 0 + }) + } + return projection +} + +function filterOrg (orgObj, isSecretariat = false) { + const CONSTANTS = getConstants() + const _ = require('lodash') + let fieldsToOmit = [...CONSTANTS.ORG_EXCLUDED_FIELDS] + if (!isSecretariat) { + fieldsToOmit = [...fieldsToOmit, ...CONSTANTS.ORG_RESTRICTED_FIELDS] + } + return _.omit(orgObj, fieldsToOmit) +} + class BaseOrgRepository extends BaseRepository { constructor () { super(BaseOrg) @@ -115,11 +149,11 @@ class BaseOrgRepository extends BaseRepository { * @param {boolean} [returnLegacyFormat=false] - If true, returns the legacy format. * @returns {Promise} The organization object. */ - async findOneByShortName (shortName, options = {}, returnLegacyFormat = false) { + async findOneByShortName (shortName, options = {}, returnLegacyFormat = false, projection = {}) { const OrgRepository = require('./orgRepository') const legacyOrgRepo = new OrgRepository() if (returnLegacyFormat) return await legacyOrgRepo.findOneByShortName(shortName, options) - const data = await BaseOrgModel.findOne({ short_name: shortName }, null, options) + const data = await BaseOrgModel.findOne({ short_name: shortName }, projection, options) return data } @@ -132,11 +166,11 @@ class BaseOrgRepository extends BaseRepository { * @param {boolean} [returnLegacyFormat=false] - If true, returns the legacy format. * @returns {Promise} The organization object. */ - async findOneByUUID (UUID, options = {}, returnLegacyFormat = false) { + async findOneByUUID (UUID, options = {}, returnLegacyFormat = false, projection = {}) { const OrgRepository = require('./orgRepository') const legacyOrgRepo = new OrgRepository() if (returnLegacyFormat) return await legacyOrgRepo.findOneByUUID(UUID, options) - return await BaseOrgModel.findOne({ UUID: UUID }, null, options) + return await BaseOrgModel.findOne({ UUID: UUID }, projection, options) } /** @@ -261,7 +295,7 @@ class BaseOrgRepository extends BaseRepository { * @param {boolean} [returnLegacyFormat=false] - If true, returns data in legacy format. * @returns {Promise} Paginated result containing organizations and metadata. */ - async getAllOrgs (options = {}, returnLegacyFormat = false) { + async getAllOrgs (options = {}, returnLegacyFormat = false, isSecretariat = false) { const OrgRepository = require('./orgRepository') const orgRepo = new OrgRepository() let pg @@ -269,7 +303,7 @@ class BaseOrgRepository extends BaseRepository { const agt = setAggregateOrgObj({}) pg = await orgRepo.aggregatePaginate(agt, options) } else { - const agt = setAggregateRegistryOrgObj({}) + const agt = setAggregateRegistryOrgObj({}, isSecretariat) pg = await this.aggregatePaginate(agt, options) } @@ -322,11 +356,12 @@ class BaseOrgRepository extends BaseRepository { * @param {boolean} [returnLegacyFormat=false] - If true, returns legacy format. * @returns {Promise} The sanitized organization object. */ - async getOrg (identifier, identifierIsUUID = false, options = {}, returnLegacyFormat = false) { + async getOrg (identifier, identifierIsUUID = false, options = {}, returnLegacyFormat = false, isSecretariat = false) { const { deepRemoveEmpty } = require('../utils/utils') + const projection = getOrgProjection(isSecretariat) const data = identifierIsUUID - ? await this.findOneByUUID(identifier, options, returnLegacyFormat) - : await this.findOneByShortName(identifier, options, returnLegacyFormat) + ? await this.findOneByUUID(identifier, options, returnLegacyFormat, projection) + : await this.findOneByShortName(identifier, options, returnLegacyFormat, projection) if (!data) return null const result = data.toObject() @@ -335,11 +370,6 @@ class BaseOrgRepository extends BaseRepository { result.reports_to = parentOrg.UUID } - delete result.__t - delete result.__v - delete result._id - delete result.inUse - delete result.in_use return deepRemoveEmpty(result) } @@ -534,22 +564,11 @@ class BaseOrgRepository extends BaseRepository { // Convert the actual model, back to a json model const legacyObjectRawJson = postUpdate.toObject() - // Remove private stuff - delete legacyObjectRawJson.__v - delete legacyObjectRawJson._id - delete legacyObjectRawJson.inUse - delete legacyObjectRawJson.in_use - return deepRemoveEmpty(legacyObjectRawJson) + return filterOrg(deepRemoveEmpty(legacyObjectRawJson), isSecretariat) } const rawRegistryOrgObject = registryObject.toObject() - delete rawRegistryOrgObject.__t - delete rawRegistryOrgObject.__v - delete rawRegistryOrgObject._id - delete rawRegistryOrgObject.inUse - delete rawRegistryOrgObject.in_use - - return deepRemoveEmpty(rawRegistryOrgObject) + return filterOrg(deepRemoveEmpty(rawRegistryOrgObject), isSecretariat) } /** @@ -662,6 +681,7 @@ class BaseOrgRepository extends BaseRepository { 'reports_to', 'contact_info', // Handles all nested contact_info fields automatically 'partner_role', + 'partner_number', 'partner_type', 'partner_country', 'vulnerability_advisory_locations', @@ -735,22 +755,10 @@ class BaseOrgRepository extends BaseRepository { await legacyOrg.save(options) await registryOrg.save(options) if (isLegacyObject) { - const plainJavascriptLegacyOrg = legacyOrg.toObject() - delete plainJavascriptLegacyOrg.__v - delete plainJavascriptLegacyOrg._id - delete plainJavascriptLegacyOrg.inUse - delete plainJavascriptLegacyOrg.in_use - return deepRemoveEmpty(plainJavascriptLegacyOrg) + return filterOrg(deepRemoveEmpty(legacyOrg.toObject()), isSecretariat) } - const plainJavascriptRegistryOrg = registryOrg.toObject() - // Remove private things - delete plainJavascriptRegistryOrg.__v - delete plainJavascriptRegistryOrg._id - delete plainJavascriptRegistryOrg.__t - delete plainJavascriptRegistryOrg.inUse - delete plainJavascriptRegistryOrg.in_use - return deepRemoveEmpty(plainJavascriptRegistryOrg) + return filterOrg(deepRemoveEmpty(registryOrg.toObject()), isSecretariat) } /** @@ -968,24 +976,14 @@ class BaseOrgRepository extends BaseRepository { if (isLegacyObject) { const plainJavascriptLegacyOrg = updatedLegacyOrg.toObject() - delete plainJavascriptLegacyOrg.__v - delete plainJavascriptLegacyOrg._id - delete plainJavascriptLegacyOrg.inUse - delete plainJavascriptLegacyOrg.in_use plainJavascriptLegacyOrg.joint_approval_required = !(isSecretariat || _.isEmpty(jointApprovalFieldsRegistry)) - return deepRemoveEmpty(plainJavascriptLegacyOrg) + return filterOrg(deepRemoveEmpty(plainJavascriptLegacyOrg), isSecretariat) } const plainJavascriptRegistryOrg = updatedRegistryOrg.toObject() plainJavascriptRegistryOrg.conversation = conversationArray - // Remove private things - delete plainJavascriptRegistryOrg.__v - delete plainJavascriptRegistryOrg._id - delete plainJavascriptRegistryOrg.__t - delete plainJavascriptRegistryOrg.inUse - delete plainJavascriptRegistryOrg.in_use plainJavascriptRegistryOrg.joint_approval_required = !(isSecretariat || _.isEmpty(jointApprovalFieldsRegistry)) - return deepRemoveEmpty(plainJavascriptRegistryOrg) + return filterOrg(deepRemoveEmpty(plainJavascriptRegistryOrg), isSecretariat) } /** diff --git a/test/integration-tests/org/registryOrgAsOrgAdmin.js b/test/integration-tests/org/registryOrgAsOrgAdmin.js index ef2356bce..6ec234157 100644 --- a/test/integration-tests/org/registryOrgAsOrgAdmin.js +++ b/test/integration-tests/org/registryOrgAsOrgAdmin.js @@ -377,6 +377,21 @@ describe('Testing Registry Org as org admin', () => { expect(res.body.error).to.be.equal('SECRETARIAT_ONLY') }) }) + it('Registry: Services api does not allow org admins to update their own orgs partner_data', async () => { + await chai.request(app) + .put('/api/registry/org/beat_10') + .set(adminHeaders) + .send({ + partner_data: { + status: 'active' + } + }) + .then((res, err) => { + expect(err).to.be.undefined + expect(res).to.have.status(403) + expect(res.body.error).to.be.equal('SECRETARIAT_ONLY') + }) + }) it('Registry: Services api does not allow org admins to create other orgs', async () => { await chai.request(app) .post('/api/registry/org') diff --git a/test/integration-tests/registry-org/registryOrgCRUDTest.js b/test/integration-tests/registry-org/registryOrgCRUDTest.js index a36b6a419..695d8d616 100644 --- a/test/integration-tests/registry-org/registryOrgCRUDTest.js +++ b/test/integration-tests/registry-org/registryOrgCRUDTest.js @@ -14,6 +14,7 @@ const testRegistryOrg = { authority: ['CNA'], hard_quota: 1000, partner_role: 'Initial Partner Role', + partner_number: 'Initial Partner Number', partner_type: 'Initial Partner Type', partner_country: 'US' } @@ -53,6 +54,9 @@ describe('Testing /registryOrg endpoints', () => { expect(res.body.created).to.haveOwnProperty('partner_role') expect(res.body.created.partner_role).to.equal(testRegistryOrg.partner_role) + expect(res.body.created).to.haveOwnProperty('partner_number') + expect(res.body.created.partner_number).to.equal(testRegistryOrg.partner_number) + expect(res.body.created).to.haveOwnProperty('partner_type') expect(res.body.created.partner_type).to.equal(testRegistryOrg.partner_type) @@ -147,6 +151,7 @@ describe('Testing /registryOrg endpoints', () => { expect(res.body).to.have.property('short_name', createdOrg.short_name) expect(res.body.authority).to.be.an('array').that.includes('CNA') expect(res.body).to.have.property('partner_role', createdOrg.partner_role) + expect(res.body).to.have.property('partner_number', createdOrg.partner_number) expect(res.body).to.have.property('partner_type', createdOrg.partner_type) expect(res.body).to.have.property('partner_country', createdOrg.partner_country) }) @@ -226,6 +231,7 @@ describe('Testing /registryOrg endpoints', () => { ...createdOrg, long_name: 'Registry Org Test Updated', partner_role: 'Updated Partner Role', + partner_number: 'Updated Partner Number', partner_type: 'Updated Partner Type', partner_country: 'UK' }) @@ -256,6 +262,9 @@ describe('Testing /registryOrg endpoints', () => { expect(res.body.updated).to.haveOwnProperty('partner_role') expect(res.body.updated.partner_role).to.equal('Updated Partner Role') + expect(res.body.updated).to.haveOwnProperty('partner_number') + expect(res.body.updated.partner_number).to.equal('Updated Partner Number') + expect(res.body.updated).to.haveOwnProperty('partner_type') expect(res.body.updated.partner_type).to.equal('Updated Partner Type') @@ -263,6 +272,23 @@ describe('Testing /registryOrg endpoints', () => { expect(res.body.updated.partner_country).to.equal('UK') }) }) + it('Allows Secretariat to update partner_data', async () => { + await chai.request(app) + .put('/api/registry/org/registry_org_test') + .set(secretariatHeaders) + .send({ + ...createdOrg, + partner_data: { + status: 'active' + } + }) + .then((res, err) => { + expect(err).to.be.undefined + expect(res).to.have.status(200) + expect(res.body.updated).to.haveOwnProperty('partner_data') + expect(res.body.updated.partner_data.status).to.equal('active') + }) + }) it('Updates a registry organization\'s short name and role simultaneously to verify read-after-write audit logic', async () => { // First create a temporary org const tempOrg = { From b3a57258430ec6216f73adb9302241fecccd9a19 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Thu, 23 Apr 2026 16:00:53 -0400 Subject: [PATCH 19/86] Adding root org --- schemas/registry-org/BaseOrg.json | 3 +- schemas/registry-org/RootOrg.json | 60 +++++++++++ .../create-registry-org-request.json | 2 +- .../update-registry-org-request.json | 2 +- src/constants/index.js | 4 +- src/model/rootorg.js | 32 ++++++ src/repositories/baseOrgRepository.js | 21 +++- .../audit/registryOrgCreatesAuditTest.js | 2 +- .../org/roleValidationTest.js | 4 +- .../registry-org/rootOrgTest.js | 102 ++++++++++++++++++ 10 files changed, 223 insertions(+), 9 deletions(-) create mode 100644 schemas/registry-org/RootOrg.json create mode 100644 src/model/rootorg.js create mode 100644 test/integration-tests/registry-org/rootOrgTest.js diff --git a/schemas/registry-org/BaseOrg.json b/schemas/registry-org/BaseOrg.json index 63f81c9ac..df094ec13 100644 --- a/schemas/registry-org/BaseOrg.json +++ b/schemas/registry-org/BaseOrg.json @@ -39,7 +39,8 @@ "CNA", "SECRETARIAT", "BULK_DOWNLOAD", - "ADP" + "ADP", + "ROOT" ] } }, diff --git a/schemas/registry-org/RootOrg.json b/schemas/registry-org/RootOrg.json new file mode 100644 index 000000000..fa7b27e51 --- /dev/null +++ b/schemas/registry-org/RootOrg.json @@ -0,0 +1,60 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "RootOrg", + "type": "object", + "title": "CVE Root Organization", + "description": "Schema for a CVE Root Organization", + "additionalProperties": false, + "properties": { + "UUID": { "$ref": "/BaseOrg#/definitions/uuidType" }, + "short_name": { "$ref": "/BaseOrg#/definitions/shortName" }, + "long_name": { "$ref": "/BaseOrg#/definitions/longName" }, + "new_short_name": { + "description": "Used to rename an organization's short name during an update.", + "type": "string", + "minLength": 2, + "maxLength": 32 + }, + "aliases": { "$ref": "/BaseOrg#/properties/aliases" }, + "contact_info": { + "type": "object", + "properties": { + "poc": { "type": "string" }, + "poc_email": { "type": "string" }, + "poc_phone": { "type": "string" }, + "org_email": { "type": "string" }, + "website": { "type": "string" } + }, + "additionalProperties": false + }, + "authority": { + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "/BaseOrg#/definitions/authority" + } + }, + "oversees": { + "type": "array", + "uniqueItems": true, + "items": { + "type": "string", + "format": "uuid" + } + }, + "partner_role": { + "type": "string" + }, + "partner_number": { + "type": "string" + }, + "partner_type": { + "type": "string" + }, + "partner_country": { + "type": "string" + }, + "partner_data": { "$ref": "/BaseOrg#/properties/partner_data" } + }, + "required": ["short_name"] +} diff --git a/schemas/registry-org/create-registry-org-request.json b/schemas/registry-org/create-registry-org-request.json index 9ce81dc61..51a0c7ef1 100644 --- a/schemas/registry-org/create-registry-org-request.json +++ b/schemas/registry-org/create-registry-org-request.json @@ -24,7 +24,7 @@ "type": "array", "items": { "type": "string", - "enum": ["CNA", "ADP", "BULK_DOWNLOAD", "SECRETARIAT"] + "enum": ["CNA", "ADP", "BULK_DOWNLOAD", "SECRETARIAT", "ROOT"] } }, "oversees": { diff --git a/schemas/registry-org/update-registry-org-request.json b/schemas/registry-org/update-registry-org-request.json index 38d210e96..9c94fc290 100644 --- a/schemas/registry-org/update-registry-org-request.json +++ b/schemas/registry-org/update-registry-org-request.json @@ -32,7 +32,7 @@ "type": "array", "items": { "type": "string", - "enum": ["CNA", "ADP", "Secretariat"] + "enum": ["CNA", "ADP", "Secretariat", "ROOT"] } } }, diff --git a/src/constants/index.js b/src/constants/index.js index 40241631a..26c6afe10 100644 --- a/src/constants/index.js +++ b/src/constants/index.js @@ -31,14 +31,14 @@ function getConstants () { SECRETARIAT: 'SECRETARIAT', CNA: 'CNA', BULK_DOWNLOAD: 'BULK_DOWNLOAD', - ROOT_CNA: 'ROOT_CNA', + ROOT: 'ROOT', ADP: 'ADP' }, ORG_ROLES: [ 'CNA', 'SECRETARIAT', 'BULK_DOWNLOAD', - 'ROOT_CNA', + 'ROOT', 'ADP' ], USER_ROLES: [ diff --git a/src/model/rootorg.js b/src/model/rootorg.js new file mode 100644 index 000000000..8a798c8e3 --- /dev/null +++ b/src/model/rootorg.js @@ -0,0 +1,32 @@ +const mongoose = require('mongoose') +const BaseOrg = require('./baseorg') +const fs = require('fs') +const Ajv = require('ajv') +const addFormats = require('ajv-formats') +const BaseOrgSchema = JSON.parse(fs.readFileSync('schemas/registry-org/BaseOrg.json')) +const RootOrgSchema = JSON.parse(fs.readFileSync('schemas/registry-org/RootOrg.json')) +const ajv = new Ajv({ allErrors: true }) +addFormats(ajv) +ajv.addSchema(BaseOrgSchema) + +const validate = ajv.compile(RootOrgSchema) + +const schema = { + authority: [String], + oversees: [String] +} + +const options = { discriminatorKey: 'kind' } +const ROOTSchema = new mongoose.Schema(schema, options) +ROOTSchema.statics.validateOrg = function (record) { + const validateObject = {} + validateObject.isValid = validate(record) + + if (!validateObject.isValid) { + validateObject.errors = validate.errors + } + return validateObject +} +const RootOrg = BaseOrg.discriminator('RootOrg', ROOTSchema, options) + +module.exports = RootOrg diff --git a/src/repositories/baseOrgRepository.js b/src/repositories/baseOrgRepository.js index d296c41c5..6a9c2c72f 100644 --- a/src/repositories/baseOrgRepository.js +++ b/src/repositories/baseOrgRepository.js @@ -4,6 +4,7 @@ const CNAOrgModel = require('../model/cnaorg') const ADPOrgModel = require('../model/adporg') const BulkDownloadModel = require('../model/bulkdownloadorg') const SecretariatOrgModel = require('../model/secretariatorg') +const RootOrgModel = require('../model/rootorg') const CveIdRepository = require('./cveIdRepository') const uuid = require('uuid') const _ = require('lodash') @@ -507,9 +508,16 @@ class BaseOrgRepository extends BaseRepository { } else { await reviewObjectRepo.createReviewOrgObject(registryObjectRaw, requestingUsername, options) } + } else if (registryObjectRaw.authority.includes('ROOT')) { + const rootObjectToSave = new RootOrgModel(registryObjectRaw) + if (isSecretariat) { + registryObject = await rootObjectToSave.save(options) + } else { + await reviewObjectRepo.createReviewOrgObject(registryObjectRaw, requestingUsername, options) + } } else { // Throw an Error instance so callers can catch and handle it properly - throw new Error("Unknown Org type requested. Please use either 'SECRETARIAT', 'CNA', 'ADP', or 'BULK_DOWNLOAD' as the authority role.") + throw new Error("Unknown Org type requested. Please use either 'SECRETARIAT', 'CNA', 'ADP', 'BULK_DOWNLOAD', or 'ROOT' as the authority role.") } // ADD AUDIT ENTRY AUTOMATICALLY for the registry object @@ -653,6 +661,8 @@ class BaseOrgRepository extends BaseRepository { TargetModel = ADPOrgModel } else if (finalRoles.includes('BULK_DOWNLOAD')) { TargetModel = BulkDownloadModel + } else if (finalRoles.includes('ROOT')) { + TargetModel = RootOrgModel } // Save changes - handle possible model type change @@ -950,6 +960,8 @@ class BaseOrgRepository extends BaseRepository { TargetModel = ADPOrgModel } else if (updatedRegistryOrg.authority?.includes('BULK_DOWNLOAD')) { TargetModel = BulkDownloadModel + } else if (updatedRegistryOrg.authority?.includes('ROOT')) { + TargetModel = RootOrgModel } // If the model type has changed, replace the document with a new one of the correct type @@ -1034,6 +1046,10 @@ class BaseOrgRepository extends BaseRepository { org.authority = ['BULK_DOWNLOAD'] validateObject = BulkDownloadModel.validateOrg(org) } + if (org.authority.includes('ROOT')) { + org.authority = ['ROOT'] + validateObject = RootOrgModel.validateOrg(org) + } } } else { if (org.authority === 'ADP') { @@ -1042,6 +1058,9 @@ class BaseOrgRepository extends BaseRepository { if (org.authority === 'SECRETARIAT') { validateObject = SecretariatOrgModel.validateOrg(org) } + if (org.authority === 'ROOT') { + validateObject = RootOrgModel.validateOrg(org) + } // We will default to CNA if a type is not given if (org.authority === 'CNA' || !org.authority) { validateObject = CNAOrgModel.validateOrg(org) diff --git a/test/integration-tests/audit/registryOrgCreatesAuditTest.js b/test/integration-tests/audit/registryOrgCreatesAuditTest.js index 74cfc8fb2..92adb7849 100644 --- a/test/integration-tests/audit/registryOrgCreatesAuditTest.js +++ b/test/integration-tests/audit/registryOrgCreatesAuditTest.js @@ -25,7 +25,7 @@ async function createTestOrg (customProps = {}) { .post('/api/registry/org') .set(secretariatHeaders) .send(orgData) - + if (res.status === 500) console.log('500 ERROR:', JSON.stringify(res.body, null, 2)) expect(res).to.have.status(200) return { diff --git a/test/integration-tests/org/roleValidationTest.js b/test/integration-tests/org/roleValidationTest.js index aca35c817..929387b42 100644 --- a/test/integration-tests/org/roleValidationTest.js +++ b/test/integration-tests/org/roleValidationTest.js @@ -32,8 +32,8 @@ describe('BaseOrgRepository Role Validation', () => { it('should add valid roles to an organization', async () => { // Setup: assume MITRE is CNA. Let's try to add ROOT_CNA if not present, or just ensure it accepts valid enums. - // CONSTANTS.AUTH_ROLE_ENUM.ROOT_CNA - const validRole = 'ROOT_CNA' + // CONSTANTS.AUTH_ROLE_ENUM.ROOT + const validRole = 'ROOT' const res = await chai.request(app) .put(`/api/org/${orgShortName}?active_roles.add=${validRole}`) diff --git a/test/integration-tests/registry-org/rootOrgTest.js b/test/integration-tests/registry-org/rootOrgTest.js new file mode 100644 index 000000000..c78896ad8 --- /dev/null +++ b/test/integration-tests/registry-org/rootOrgTest.js @@ -0,0 +1,102 @@ +/* eslint-disable no-unused-expressions */ +const chai = require('chai') +const expect = chai.expect +chai.use(require('chai-http')) + +const constants = require('../constants.js') +const app = require('../../../src/index.js') + +const secretariatHeaders = { ...constants.headers, 'content-type': 'application/json' } +// Create headers for a root admin (we'll create this user in the test) +let rootAdminHeaders + +const testRootOrg = { + short_name: 'root_org_test_4', + long_name: 'Root Org Test', + authority: ['ROOT'] +} +let createdOrg + +describe('Testing ROOT Organization Type', () => { + context('Creating a ROOT org', () => { + it('Secretariat creates a new ROOT org (without quotas)', async () => { + await chai.request(app) + .post('/api/registry/org') + .set(secretariatHeaders) + .send(testRootOrg) + .then((res, err) => { + expect(err).to.be.undefined + expect(res).to.have.status(200) + expect(res.body.message).to.equal(testRootOrg.short_name + ' organization was successfully created.') + expect(res.body.created.authority).to.deep.equal(['ROOT']) + createdOrg = res.body.created + delete createdOrg.created + delete createdOrg.last_updated + }) + }) + + it('Fails to create ROOT org if quotas are provided', async () => { + await chai.request(app) + .post('/api/registry/org') + .set(secretariatHeaders) + .send({ + ...testRootOrg, + short_name: 'root_org_fail', + hard_quota: 100 + }) + .then((res) => { + expect(res).to.have.status(400) + expect(res.body.message).to.equal('Parameters were invalid') + }) + }) + }) + + context('ROOT admin permissions', () => { + before(async () => { + // Create a Root Admin user + await chai.request(app) + .post(`/api/registry/org/${testRootOrg.short_name}/user`) + .set(secretariatHeaders) + .send({ + username: 'root_admin_test', + name: { + first: 'Root', + last: 'Admin' + }, + role: 'ADMIN' + }) + .then((res) => { + rootAdminHeaders = { + 'content-type': 'application/json', + 'CVE-API-ORG': testRootOrg.short_name, + 'CVE-API-USER': 'root_admin_test', + 'CVE-API-KEY': res.body.created.secret + } + }) + }) + + it('ROOT admin can update their own org', async () => { + await chai.request(app) + .put(`/api/registry/org/${testRootOrg.short_name}`) + .set(rootAdminHeaders) + .send({ + ...createdOrg, + long_name: 'Updated Root Org Test' + }) + .then((res) => { + if (res.status === 400) console.log(JSON.stringify(res.body, null, 2)) + expect(res).to.have.status(200) + }) + }) + + it('ROOT admin cannot reserve CVE IDs', async () => { + await chai.request(app) + .post('/api/cve-id') + .set(rootAdminHeaders) + .query({ amount: 1, cve_year: 2026, short_name: testRootOrg.short_name }) + .then((res) => { + expect(res).to.have.status(403) + }) + }) + }) +}) From 5df3607c974396bfbacce213ae54f6d9e2e51c39 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Thu, 23 Apr 2026 16:09:27 -0400 Subject: [PATCH 20/86] updating root_or_tlr to top level root --- api-docs/openapi.json | 2 +- schemas/registry-org/BaseOrg.json | 4 ++-- schemas/registry-org/create-registry-org-request.json | 4 ++-- schemas/registry-org/create-registry-org-response.json | 4 ++-- schemas/registry-org/get-registry-org-response.json | 4 ++-- schemas/registry-org/list-registry-orgs-response.json | 4 ++-- schemas/registry-org/update-registry-org-request.json | 4 ++-- schemas/registry-org/update-registry-org-response.json | 4 ++-- src/constants/index.js | 2 +- src/controller/org.controller/index.js | 2 +- src/controller/org.controller/org.middleware.js | 10 +++++----- .../registry-org.controller/registry-org.middleware.js | 2 +- src/model/baseorg.js | 2 +- src/repositories/baseOrgRepository.js | 4 ++-- src/scripts/migrate.js | 6 +++--- .../conversation/editConversationTest.js | 2 +- 16 files changed, 30 insertions(+), 30 deletions(-) diff --git a/api-docs/openapi.json b/api-docs/openapi.json index eaf2feb02..39f3a9372 100644 --- a/api-docs/openapi.json +++ b/api-docs/openapi.json @@ -2605,7 +2605,7 @@ "Registry Organization" ], "summary": "Updates information about the organization specified by short name (accessible Temporarily to Secretariat only)", - "description": "

Access Control

User must belong to an organization with the Secretariat role temporarily.

In the future, only the organization's admin will be able to request changes to its information.

With Joint Approval required for the following fields:

Expected Behavior

This endpoint expects a full organization object in the request body.

Secretariat: Updates any organization's information

Organization Admin: Requests changes to its organization's information

  • short_name
  • long_name
  • authority
  • aliases
  • oversees
  • root_or_tlr
  • charter_or_scope
  • product_list
  • disclosure_policy
  • contact_info.poc
  • contact_info.poc_email
  • contact_info.poc_phone
  • contact_info.org_email
  • partner_role
  • partner_type
  • partner_country
  • vulnerability_advisory_locations
  • advisory_location_require_credentials
  • industry
  • tl_root_start_date
  • is_cna_discussion_list
", + "description": "

Access Control

User must belong to an organization with the Secretariat role temporarily.

In the future, only the organization's admin will be able to request changes to its information.

With Joint Approval required for the following fields:

Expected Behavior

This endpoint expects a full organization object in the request body.

Secretariat: Updates any organization's information

Organization Admin: Requests changes to its organization's information

  • short_name
  • long_name
  • authority
  • aliases
  • oversees
  • top_level_root
  • charter_or_scope
  • product_list
  • disclosure_policy
  • contact_info.poc
  • contact_info.poc_email
  • contact_info.poc_phone
  • contact_info.org_email
  • partner_role
  • partner_type
  • partner_country
  • vulnerability_advisory_locations
  • advisory_location_require_credentials
  • industry
  • tl_root_start_date
  • is_cna_discussion_list
", "operationId": "orgUpdateSingle", "parameters": [ { diff --git a/schemas/registry-org/BaseOrg.json b/schemas/registry-org/BaseOrg.json index df094ec13..72770ecdc 100644 --- a/schemas/registry-org/BaseOrg.json +++ b/schemas/registry-org/BaseOrg.json @@ -71,8 +71,8 @@ "$ref": "#/definitions/authority" } }, - "root_or_tlr": { - "type": "boolean" + "top_level_root": { + "type": "string" }, "reports_to": { "$ref": "#/definitions/uuidType" diff --git a/schemas/registry-org/create-registry-org-request.json b/schemas/registry-org/create-registry-org-request.json index 51a0c7ef1..10c182b50 100644 --- a/schemas/registry-org/create-registry-org-request.json +++ b/schemas/registry-org/create-registry-org-request.json @@ -34,8 +34,8 @@ }, "description": "UUIDs of organizations overseen by this organization" }, - "root_or_tlr": { - "type": "boolean", + "top_level_root": { + "type": "string", "description": "Indicates if the organization is a root or top-level root" }, "users": { diff --git a/schemas/registry-org/create-registry-org-response.json b/schemas/registry-org/create-registry-org-response.json index 6f0bfb0ec..43dd86f1c 100644 --- a/schemas/registry-org/create-registry-org-response.json +++ b/schemas/registry-org/create-registry-org-response.json @@ -60,8 +60,8 @@ }, "description": "UUIDs of organizations overseen by this organization" }, - "root_or_tlr": { - "type": "boolean", + "top_level_root": { + "type": "string", "description": "Indicates if the organization is a root or top-level root" }, "users": { diff --git a/schemas/registry-org/get-registry-org-response.json b/schemas/registry-org/get-registry-org-response.json index b5390cbaa..ce4e5720e 100644 --- a/schemas/registry-org/get-registry-org-response.json +++ b/schemas/registry-org/get-registry-org-response.json @@ -37,8 +37,8 @@ }, "description": "The organization's function within the CVE program" }, - "root_or_tlr": { - "type": "boolean", + "top_level_root": { + "type": "string", "description": "Indicates if the organization is a root or top-level root" }, "reports_to": { diff --git a/schemas/registry-org/list-registry-orgs-response.json b/schemas/registry-org/list-registry-orgs-response.json index 93eacb702..12f6d6622 100644 --- a/schemas/registry-org/list-registry-orgs-response.json +++ b/schemas/registry-org/list-registry-orgs-response.json @@ -66,8 +66,8 @@ }, "description": "The organization's function within the CVE program" }, - "root_or_tlr": { - "type": "boolean", + "top_level_root": { + "type": "string", "description": "Indicates if the organization is a root or top-level root" }, "reports_to": { diff --git a/schemas/registry-org/update-registry-org-request.json b/schemas/registry-org/update-registry-org-request.json index 9c94fc290..40bdb7bef 100644 --- a/schemas/registry-org/update-registry-org-request.json +++ b/schemas/registry-org/update-registry-org-request.json @@ -45,8 +45,8 @@ }, "description": "UUIDs of organizations overseen by this organization" }, - "root_or_tlr": { - "type": "boolean", + "top_level_root": { + "type": "string", "description": "Indicates if the organization is a root or top-level root" }, "users": { diff --git a/schemas/registry-org/update-registry-org-response.json b/schemas/registry-org/update-registry-org-response.json index cbf41d925..6494839f2 100644 --- a/schemas/registry-org/update-registry-org-response.json +++ b/schemas/registry-org/update-registry-org-response.json @@ -55,8 +55,8 @@ }, "description": "UUIDs of organizations overseen by this organization" }, - "root_or_tlr": { - "type": "boolean", + "top_level_root": { + "type": "string", "description": "Indicates if the organization is a root or top-level root" }, "users": { diff --git a/src/constants/index.js b/src/constants/index.js index 26c6afe10..c649441f4 100644 --- a/src/constants/index.js +++ b/src/constants/index.js @@ -44,7 +44,7 @@ function getConstants () { USER_ROLES: [ 'ADMIN' ], - JOINT_APPROVAL_FIELDS: ['short_name', 'long_name', 'authority', 'aliases', 'oversees', 'root_or_tlr', 'charter_or_scope', 'product_list', 'disclosure_policy', 'contact_info.poc', 'contact_info.poc_email', 'contact_info.poc_phone', 'contact_info.org_email', 'partner_role', 'partner_number', 'partner_type', 'partner_country', 'partner_data.cve_website_update_date', 'partner_data.cve_website_update_needed', 'partner_data.partner_active_date', 'partner_data.partner_inactive_date', 'partner_data.status', 'vulnerability_advisory_locations', 'advisory_location_require_credentials', 'industry', 'tl_root_start_date', 'is_cna_discussion_list', 'hard_quota'], + JOINT_APPROVAL_FIELDS: ['short_name', 'long_name', 'authority', 'aliases', 'oversees', 'top_level_root', 'charter_or_scope', 'product_list', 'disclosure_policy', 'contact_info.poc', 'contact_info.poc_email', 'contact_info.poc_phone', 'contact_info.org_email', 'partner_role', 'partner_number', 'partner_type', 'partner_country', 'partner_data.cve_website_update_date', 'partner_data.cve_website_update_needed', 'partner_data.partner_active_date', 'partner_data.partner_inactive_date', 'partner_data.status', 'vulnerability_advisory_locations', 'advisory_location_require_credentials', 'industry', 'tl_root_start_date', 'is_cna_discussion_list', 'hard_quota'], JOINT_APPROVAL_FIELDS_LEGACY: ['short_name', 'name', 'authority.active_roles', 'policies.id_quota'], ORG_EXCLUDED_FIELDS: ['__t', '__v', '_id', 'inUse', 'in_use'], ORG_RESTRICTED_FIELDS: ['partner_data'], diff --git a/src/controller/org.controller/index.js b/src/controller/org.controller/index.js index 1c5e6f110..95e314b0f 100644 --- a/src/controller/org.controller/index.js +++ b/src/controller/org.controller/index.js @@ -548,7 +548,7 @@ router.put('/registry/org/:shortname',
  • authority
  • aliases
  • oversees
  • -
  • root_or_tlr
  • +
  • top_level_root
  • charter_or_scope
  • product_list
  • disclosure_policy
  • diff --git a/src/controller/org.controller/org.middleware.js b/src/controller/org.controller/org.middleware.js index 78bdba0b5..08e1b8164 100644 --- a/src/controller/org.controller/org.middleware.js +++ b/src/controller/org.controller/org.middleware.js @@ -46,8 +46,8 @@ function validateCreateOrgParameters () { .isIn(orgOptions), body(['oversees']).default([]) .isArray(), - body(['root_or_tlr']).default(false) - .isBoolean(), + body(['top_level_root']).default('') + .isString(), body(['vulnerability_advisory_locations']) .default([]) .custom(isFlatStringArray), @@ -129,7 +129,7 @@ function validateCreateOrgParameters () { 'contact_info.admins', 'in_use', 'created', - 'root_or_tlr', + 'top_level_root', 'soft_quota', 'aliases', 'hard_quota', @@ -227,7 +227,7 @@ function validateUpdateOrgParameters () { if (useRegistry) { validations.push( query(['oversees']).optional().isArray(), - query(['root_or_tlr']).optional().isBoolean(), + query(['top_level_root']).optional().isString(), query([ 'charter_or_scope', 'disclosure_policy', @@ -321,7 +321,7 @@ const QUERY_PARAMETERS = { ], // Registry-only parameters registryOnly: [ - 'root_or_tlr', + 'top_level_root', 'charter_or_scope', 'disclosure_policy', 'product_list', diff --git a/src/controller/registry-org.controller/registry-org.middleware.js b/src/controller/registry-org.controller/registry-org.middleware.js index e51511ffb..e981733fa 100644 --- a/src/controller/registry-org.controller/registry-org.middleware.js +++ b/src/controller/registry-org.controller/registry-org.middleware.js @@ -12,7 +12,7 @@ function parsePostParams (req, res, next) { 'long_name', 'short_name', 'aliases', 'cve_program_org_function', 'authority.active_roles', 'oversees', - 'root_or_tlr', 'users', + 'top_level_root', 'users', 'charter_or_scope', 'disclosure_policy', 'product_list', 'soft_quota', 'hard_quota', 'contact_info.additional_contact_users', 'contact_info.poc', 'contact_info.poc_email', 'contact_info.poc_phone', diff --git a/src/model/baseorg.js b/src/model/baseorg.js index f73218164..e405d39f9 100644 --- a/src/model/baseorg.js +++ b/src/model/baseorg.js @@ -11,7 +11,7 @@ const schema = { short_name: String, aliases: [String], authority: [String], - root_or_tlr: Boolean, + top_level_root: String, users: { type: [String], set: toUndefined }, admins: [String], contact_info: { diff --git a/src/repositories/baseOrgRepository.js b/src/repositories/baseOrgRepository.js index 6a9c2c72f..517f7e810 100644 --- a/src/repositories/baseOrgRepository.js +++ b/src/repositories/baseOrgRepository.js @@ -592,7 +592,7 @@ class BaseOrgRepository extends BaseRepository { * @param {string[]} [incomingParameters.active_roles.add] - An array of role strings to add. * @param {string[]} [incomingParameters.active_roles.remove] - An array of role strings to remove. * @param {number} [incomingParameters.id_quota] - The ID quota for the organization. (Applied to legacy and CNA-type registry orgs) - * @param {string} [incomingParameters.root_or_tlr] - The root or Top-Level Root (TLR) status. (Registry only) + * @param {string} [incomingParameters.top_level_root] - The root or Top-Level Root (TLR) status. (Registry only) * @param {string} [incomingParameters.charter_or_scope] - The charter or scope description. (Registry only) * @param {string} [incomingParameters.disclosure_policy] - The disclosure policy. (Registry only) * @param {string[]} [incomingParameters.product_list] - A list of the organization's products. (Registry only) @@ -683,7 +683,7 @@ class BaseOrgRepository extends BaseRepository { _.set(legacyOrg, 'authority.active_roles', finalRoles) const directRegistryKeys = [ - 'root_or_tlr', + 'top_level_root', 'charter_or_scope', 'disclosure_policy', 'product_list', diff --git a/src/scripts/migrate.js b/src/scripts/migrate.js index a0097c1d6..b4dc9ee0b 100644 --- a/src/scripts/migrate.js +++ b/src/scripts/migrate.js @@ -101,7 +101,7 @@ async function addCVEBoard (db) { authority: null, reports_to: null, oversees: [mitreUUID], - root_or_tlr: true, + top_level_root: 'true', users: null, charter_or_scope: null, disclosure_policy: null, @@ -174,7 +174,7 @@ async function orgHelper (db) { // parent = mitreUUID // } - // Set root_or_tlr, charter_or_scope, disclosure_policy, org_email, website + // Set top_level_root, charter_or_scope, disclosure_policy, org_email, website let rootTlr = false let charterScope = null let disclosure = null @@ -206,7 +206,7 @@ async function orgHelper (db) { authority: doc.authority.active_roles, reports_to: parent, oversees: children, - root_or_tlr: rootTlr, + top_level_root: rootTlr ? 'true' : 'false', users: orgUsers, charter_or_scope: charterScope, disclosure_policy: disclosure, diff --git a/test/integration-tests/conversation/editConversationTest.js b/test/integration-tests/conversation/editConversationTest.js index 08e101f6e..dbb59eda7 100644 --- a/test/integration-tests/conversation/editConversationTest.js +++ b/test/integration-tests/conversation/editConversationTest.js @@ -31,7 +31,7 @@ describe('Testing Conversation endpoints', () => { delete org.last_updated delete org.admins delete org.users - delete org.root_or_tlr + delete org.top_level_root }) await chai From 2a80e4d14c82d0390789127be171e3fdfad1e51a Mon Sep 17 00:00:00 2001 From: david-rocca Date: Fri, 24 Apr 2026 12:25:29 -0400 Subject: [PATCH 21/86] Partner type / cna active / inactive date --- api-docs/openapi.json | 2 +- schemas/registry-org/BaseOrg.json | 15 +++ schemas/registry-org/CNAOrg.json | 7 +- schemas/registry-org/RootOrg.json | 7 +- .../get-registry-org-response.json | 9 +- .../list-registry-orgs-response.json | 9 +- src/constants/index.js | 8 +- src/controller/org.controller/index.js | 3 +- .../org.controller/org.middleware.js | 18 +-- .../registry-org.middleware.js | 5 +- src/model/baseorg.js | 3 +- src/repositories/baseOrgRepository.js | 51 +++++++- .../registry-org/registryOrgCRUDTest.js | 109 +++++++++++------- .../registry-org/rootOrgTest.js | 29 +++++ test/integration-tests/user/updateUserTest.js | 1 + 15 files changed, 184 insertions(+), 92 deletions(-) diff --git a/api-docs/openapi.json b/api-docs/openapi.json index 39f3a9372..6dece2a8e 100644 --- a/api-docs/openapi.json +++ b/api-docs/openapi.json @@ -2605,7 +2605,7 @@ "Registry Organization" ], "summary": "Updates information about the organization specified by short name (accessible Temporarily to Secretariat only)", - "description": "

    Access Control

    User must belong to an organization with the Secretariat role temporarily.

    In the future, only the organization's admin will be able to request changes to its information.

    With Joint Approval required for the following fields:

    Expected Behavior

    This endpoint expects a full organization object in the request body.

    Secretariat: Updates any organization's information

    Organization Admin: Requests changes to its organization's information

    • short_name
    • long_name
    • authority
    • aliases
    • oversees
    • top_level_root
    • charter_or_scope
    • product_list
    • disclosure_policy
    • contact_info.poc
    • contact_info.poc_email
    • contact_info.poc_phone
    • contact_info.org_email
    • partner_role
    • partner_type
    • partner_country
    • vulnerability_advisory_locations
    • advisory_location_require_credentials
    • industry
    • tl_root_start_date
    • is_cna_discussion_list
    ", + "description": "

    Access Control

    User must belong to an organization with the Secretariat role temporarily.

    In the future, only the organization's admin will be able to request changes to its information.

    With Joint Approval required for the following fields:

    Expected Behavior

    This endpoint expects a full organization object in the request body.

    Secretariat: Updates any organization's information

    Organization Admin: Requests changes to its organization's information

    • short_name
    • long_name
    • authority
    • aliases
    • oversees
    • top_level_root
    • charter_or_scope
    • product_list
    • disclosure_policy
    • contact_info.poc
    • contact_info.poc_email
    • contact_info.poc_phone
    • contact_info.org_email
    • partner_role_type
    • partner_country
    • vulnerability_advisory_locations
    • advisory_location_require_credentials
    • industry
    • tl_root_start_date
    • is_cna_discussion_list
    ", "operationId": "orgUpdateSingle", "parameters": [ { diff --git a/schemas/registry-org/BaseOrg.json b/schemas/registry-org/BaseOrg.json index 72770ecdc..085cdab1a 100644 --- a/schemas/registry-org/BaseOrg.json +++ b/schemas/registry-org/BaseOrg.json @@ -42,6 +42,21 @@ "ADP", "ROOT" ] + }, + "partnerRoleType": { + "description": "The type of role a partner holds", + "type": "string", + "enum": [ + "", + "Bug Bounty Provider", + "CERT", + "Consortium", + "Hosted Service", + "N/A", + "Open Source", + "Researcher", + "Vendor" + ] } }, "properties": { diff --git a/schemas/registry-org/CNAOrg.json b/schemas/registry-org/CNAOrg.json index fc8fc5cc9..a4d676b2d 100644 --- a/schemas/registry-org/CNAOrg.json +++ b/schemas/registry-org/CNAOrg.json @@ -62,15 +62,12 @@ "format": "uuid" } }, - "partner_role": { - "type": "string" + "partner_role_type": { + "$ref": "/BaseOrg#/definitions/partnerRoleType" }, "partner_number": { "type": "string" }, - "partner_type": { - "type": "string" - }, "partner_country": { "type": "string" }, diff --git a/schemas/registry-org/RootOrg.json b/schemas/registry-org/RootOrg.json index fa7b27e51..3c28e49eb 100644 --- a/schemas/registry-org/RootOrg.json +++ b/schemas/registry-org/RootOrg.json @@ -42,15 +42,12 @@ "format": "uuid" } }, - "partner_role": { - "type": "string" + "partner_role_type": { + "$ref": "/BaseOrg#/definitions/partnerRoleType" }, "partner_number": { "type": "string" }, - "partner_type": { - "type": "string" - }, "partner_country": { "type": "string" }, diff --git a/schemas/registry-org/get-registry-org-response.json b/schemas/registry-org/get-registry-org-response.json index ce4e5720e..2a2824f51 100644 --- a/schemas/registry-org/get-registry-org-response.json +++ b/schemas/registry-org/get-registry-org-response.json @@ -105,18 +105,13 @@ "org_email" ] }, - "partner_role": { - "type": "string", - "description": "Role of the partner" + "partner_role_type": { + "$ref": "/BaseOrg#/definitions/partnerRoleType" }, "partner_number": { "type": "string", "description": "Number of the partner" }, - "partner_type": { - "type": "string", - "description": "Type of the partner" - }, "partner_country": { "type": "string", "description": "Country of the partner" diff --git a/schemas/registry-org/list-registry-orgs-response.json b/schemas/registry-org/list-registry-orgs-response.json index 12f6d6622..a074e701e 100644 --- a/schemas/registry-org/list-registry-orgs-response.json +++ b/schemas/registry-org/list-registry-orgs-response.json @@ -134,18 +134,13 @@ "org_email" ] }, - "partner_role": { - "type": "string", - "description": "Role of the partner" + "partner_role_type": { + "$ref": "/BaseOrg#/definitions/partnerRoleType" }, "partner_number": { "type": "string", "description": "Number of the partner" }, - "partner_type": { - "type": "string", - "description": "Type of the partner" - }, "partner_country": { "type": "string", "description": "Country of the partner" diff --git a/src/constants/index.js b/src/constants/index.js index c649441f4..c2264ecd9 100644 --- a/src/constants/index.js +++ b/src/constants/index.js @@ -44,7 +44,7 @@ function getConstants () { USER_ROLES: [ 'ADMIN' ], - JOINT_APPROVAL_FIELDS: ['short_name', 'long_name', 'authority', 'aliases', 'oversees', 'top_level_root', 'charter_or_scope', 'product_list', 'disclosure_policy', 'contact_info.poc', 'contact_info.poc_email', 'contact_info.poc_phone', 'contact_info.org_email', 'partner_role', 'partner_number', 'partner_type', 'partner_country', 'partner_data.cve_website_update_date', 'partner_data.cve_website_update_needed', 'partner_data.partner_active_date', 'partner_data.partner_inactive_date', 'partner_data.status', 'vulnerability_advisory_locations', 'advisory_location_require_credentials', 'industry', 'tl_root_start_date', 'is_cna_discussion_list', 'hard_quota'], + JOINT_APPROVAL_FIELDS: ['short_name', 'long_name', 'authority', 'aliases', 'oversees', 'top_level_root', 'charter_or_scope', 'product_list', 'disclosure_policy', 'contact_info.poc', 'contact_info.poc_email', 'contact_info.poc_phone', 'contact_info.org_email', 'partner_role_type', 'partner_number', 'partner_country', 'partner_data.cve_website_update_date', 'partner_data.cve_website_update_needed', 'partner_data.status', 'vulnerability_advisory_locations', 'advisory_location_require_credentials', 'industry', 'tl_root_start_date', 'is_cna_discussion_list', 'hard_quota'], JOINT_APPROVAL_FIELDS_LEGACY: ['short_name', 'name', 'authority.active_roles', 'policies.id_quota'], ORG_EXCLUDED_FIELDS: ['__t', '__v', '_id', 'inUse', 'in_use'], ORG_RESTRICTED_FIELDS: ['partner_data'], @@ -53,9 +53,9 @@ function getConstants () { 'partner_data', 'partner_data.cve_website_update_date', 'partner_data.cve_website_update_needed', - 'partner_data.partner_active_date', - 'partner_data.partner_inactive_date', - 'partner_data.status' + 'partner_data.status', + 'top_level_root', + 'oversees' ], USER_ROLE_ENUM: { ADMIN: 'ADMIN' diff --git a/src/controller/org.controller/index.js b/src/controller/org.controller/index.js index 95e314b0f..0a0caa936 100644 --- a/src/controller/org.controller/index.js +++ b/src/controller/org.controller/index.js @@ -556,8 +556,7 @@ router.put('/registry/org/:shortname',
  • contact_info.poc_email
  • contact_info.poc_phone
  • contact_info.org_email
  • -
  • partner_role
  • -
  • partner_type
  • +
  • partner_role_type
  • partner_country
  • vulnerability_advisory_locations
  • advisory_location_require_credentials
  • diff --git a/src/controller/org.controller/org.middleware.js b/src/controller/org.controller/org.middleware.js index 08e1b8164..b5e485eb6 100644 --- a/src/controller/org.controller/org.middleware.js +++ b/src/controller/org.controller/org.middleware.js @@ -80,9 +80,8 @@ function validateCreateOrgParameters () { 'contact_info.poc_phone', 'contact_info.org_email', 'contact_info.website', - 'partner_role', + 'partner_role_type', 'partner_number', - 'partner_type', 'partner_country', 'partner_data.status', 'industry' @@ -146,14 +145,11 @@ function validateCreateOrgParameters () { 'contact_info.org_email', 'contact_info.additional_contact_users', 'contact_info.website', - 'partner_role', + 'partner_role_type', 'partner_number', - 'partner_type', 'partner_country', 'partner_data.cve_website_update_date', 'partner_data.cve_website_update_needed', - 'partner_data.partner_active_date', - 'partner_data.partner_inactive_date', 'partner_data.status', 'vulnerability_advisory_locations', 'advisory_location_require_credentials', @@ -237,14 +233,11 @@ function validateUpdateOrgParameters () { 'contact_info.poc_phone', 'contact_info.org_email', 'contact_info.website', - 'partner_role', + 'partner_role_type', 'partner_number', - 'partner_type', 'partner_country', 'partner_data.cve_website_update_date', 'partner_data.cve_website_update_needed', - 'partner_data.partner_active_date', - 'partner_data.partner_inactive_date', 'partner_data.status', 'vulnerability_advisory_locations', 'advisory_location_require_credentials', @@ -332,14 +325,11 @@ const QUERY_PARAMETERS = { 'contact_info.poc_phone', 'contact_info.org_email', 'contact_info.website', - 'partner_role', + 'partner_role_type', 'partner_number', - 'partner_type', 'partner_country', 'partner_data.cve_website_update_date', 'partner_data.cve_website_update_needed', - 'partner_data.partner_active_date', - 'partner_data.partner_inactive_date', 'partner_data.status', 'vulnerability_advisory_locations', 'advisory_location_require_credentials', diff --git a/src/controller/registry-org.controller/registry-org.middleware.js b/src/controller/registry-org.controller/registry-org.middleware.js index e981733fa..4d51d54dc 100644 --- a/src/controller/registry-org.controller/registry-org.middleware.js +++ b/src/controller/registry-org.controller/registry-org.middleware.js @@ -17,14 +17,11 @@ function parsePostParams (req, res, next) { 'soft_quota', 'hard_quota', 'contact_info.additional_contact_users', 'contact_info.poc', 'contact_info.poc_email', 'contact_info.poc_phone', 'contact_info.admins', 'contact_info.org_email', 'contact_info.website', - 'partner_role', + 'partner_role_type', 'partner_number', - 'partner_type', 'partner_country', 'partner_data.cve_website_update_date', 'partner_data.cve_website_update_needed', - 'partner_data.partner_active_date', - 'partner_data.partner_inactive_date', 'partner_data.status', 'vulnerability_advisory_locations', 'advisory_location_require_credentials', diff --git a/src/model/baseorg.js b/src/model/baseorg.js index e405d39f9..0c72e8837 100644 --- a/src/model/baseorg.js +++ b/src/model/baseorg.js @@ -22,9 +22,8 @@ const schema = { org_email: String, website: String }, - partner_role: String, + partner_role_type: String, partner_number: String, - partner_type: String, partner_country: String, partner_data: { cve_website_update_date: Date, diff --git a/src/repositories/baseOrgRepository.js b/src/repositories/baseOrgRepository.js index 517f7e810..dad32b10b 100644 --- a/src/repositories/baseOrgRepository.js +++ b/src/repositories/baseOrgRepository.js @@ -460,6 +460,26 @@ class BaseOrgRepository extends BaseRepository { // Registry stuff // Add uuid to org object registryObjectRaw.UUID = sharedUUID + + // Automate partner_data dates based on status + if (!registryObjectRaw.partner_data) { + registryObjectRaw.partner_data = {} + } + + // Default to 'inactive' if not provided + if (!registryObjectRaw.partner_data.status) { + registryObjectRaw.partner_data.status = 'inactive' + } + + if (registryObjectRaw.partner_data.status === 'active') { + registryObjectRaw.partner_data.partner_active_date = new Date() + // ensure inactive is not set + delete registryObjectRaw.partner_data.partner_inactive_date + } else if (registryObjectRaw.partner_data.status === 'inactive') { + registryObjectRaw.partner_data.partner_inactive_date = new Date() + // ensure active is not set + delete registryObjectRaw.partner_data.partner_active_date + } // Figure out why this is not working.... // registryObjectRaw = _.omitBy(registryObjectRaw, value => _.isNil(value) || _.isEmpty(value)) @@ -690,9 +710,8 @@ class BaseOrgRepository extends BaseRepository { 'oversees', 'reports_to', 'contact_info', // Handles all nested contact_info fields automatically - 'partner_role', + 'partner_role_type', 'partner_number', - 'partner_type', 'partner_country', 'vulnerability_advisory_locations', 'advisory_location_require_credentials', @@ -858,6 +877,34 @@ class BaseOrgRepository extends BaseRepository { delete legacyObjectRaw.new_short_name delete incomingOrg.new_short_name // Keeping for existing logic } + + // Automate partner_data dates based on status + if (registryObjectRaw.partner_data && registryObjectRaw.partner_data.status) { + const incomingStatus = registryObjectRaw.partner_data.status + const currentStatus = registryOrg.partner_data?.status || 'inactive' + if (incomingStatus !== currentStatus) { + if (incomingStatus === 'active') { + registryObjectRaw.partner_data.partner_active_date = new Date() + if (registryObjectRaw.partner_data.partner_inactive_date !== undefined) { + delete registryObjectRaw.partner_data.partner_inactive_date + } + } else if (incomingStatus === 'inactive') { + registryObjectRaw.partner_data.partner_inactive_date = new Date() + if (registryObjectRaw.partner_data.partner_active_date !== undefined) { + delete registryObjectRaw.partner_data.partner_active_date + } + } + } else { + // Keep existing dates if status didn't change + if (registryOrg.partner_data?.partner_active_date) { + registryObjectRaw.partner_data.partner_active_date = registryOrg.partner_data.partner_active_date + } + if (registryOrg.partner_data?.partner_inactive_date) { + registryObjectRaw.partner_data.partner_inactive_date = registryOrg.partner_data.partner_inactive_date + } + } + } + // Checking for joint approval fields const jointApprovalFieldsRegistry = this.getJointApprovalFields(registryOrg, registryObjectRaw) const jointApprovalFieldsLegacy = this.getJointApprovalFields(legacyOrg, legacyObjectRaw, true) diff --git a/test/integration-tests/registry-org/registryOrgCRUDTest.js b/test/integration-tests/registry-org/registryOrgCRUDTest.js index 695d8d616..c85b0eb66 100644 --- a/test/integration-tests/registry-org/registryOrgCRUDTest.js +++ b/test/integration-tests/registry-org/registryOrgCRUDTest.js @@ -13,9 +13,8 @@ const testRegistryOrg = { long_name: 'Registry Org Test', authority: ['CNA'], hard_quota: 1000, - partner_role: 'Initial Partner Role', + partner_role_type: 'Vendor', partner_number: 'Initial Partner Number', - partner_type: 'Initial Partner Type', partner_country: 'US' } let createdOrg @@ -25,7 +24,7 @@ describe('Testing /registryOrg endpoints', () => { context('Positive Tests', () => { it('Creates a new registry org', async () => { await chai.request(app) - .post('/api/registryOrg') + .post('/api/registry/org') .set(secretariatHeaders) .send(testRegistryOrg) .then((res, err) => { @@ -51,18 +50,20 @@ describe('Testing /registryOrg endpoints', () => { expect(res.body.created).to.haveOwnProperty('hard_quota') expect(res.body.created.hard_quota).to.equal(testRegistryOrg.hard_quota) - expect(res.body.created).to.haveOwnProperty('partner_role') - expect(res.body.created.partner_role).to.equal(testRegistryOrg.partner_role) + expect(res.body.created).to.haveOwnProperty('partner_role_type') + expect(res.body.created.partner_role_type).to.equal(testRegistryOrg.partner_role_type) expect(res.body.created).to.haveOwnProperty('partner_number') expect(res.body.created.partner_number).to.equal(testRegistryOrg.partner_number) - expect(res.body.created).to.haveOwnProperty('partner_type') - expect(res.body.created.partner_type).to.equal(testRegistryOrg.partner_type) - expect(res.body.created).to.haveOwnProperty('partner_country') expect(res.body.created.partner_country).to.equal(testRegistryOrg.partner_country) + expect(res.body.created).to.haveOwnProperty('partner_data') + expect(res.body.created.partner_data.status).to.equal('inactive') + expect(res.body.created.partner_data).to.haveOwnProperty('partner_inactive_date') + expect(res.body.created.partner_data).to.not.haveOwnProperty('partner_active_date') + createdOrg = res.body.created delete createdOrg.created delete createdOrg.last_updated @@ -72,7 +73,7 @@ describe('Testing /registryOrg endpoints', () => { context('Negative Tests', () => { it('Fails to create a new registry organization with an existing short name', async () => { await chai.request(app) - .post('/api/registryOrg') + .post('/api/registry/org') .set(secretariatHeaders) .send(testRegistryOrg) .then((res) => { @@ -82,7 +83,7 @@ describe('Testing /registryOrg endpoints', () => { }) it('Fails to create a new registry organization with invalid data', async () => { await chai.request(app) - .post('/api/registryOrg') + .post('/api/registry/org') .set(secretariatHeaders) .send({ ...testRegistryOrg, @@ -95,7 +96,7 @@ describe('Testing /registryOrg endpoints', () => { }) it('Fails to create a new registry organization with reports_to manually provided', async () => { await chai.request(app) - .post('/api/registryOrg') + .post('/api/registry/org') .set(secretariatHeaders) .send({ ...testRegistryOrg, @@ -105,12 +106,13 @@ describe('Testing /registryOrg endpoints', () => { .then((res) => { expect(res).to.have.status(400) expect(res.body.message).to.equal('Parameters were invalid') - expect(res.body.details[0].msg).to.equal('reports_to must not be present') + expect(res.body.errors[0].message).to.equal('must NOT have additional properties') + expect(res.body.errors[0].params.additionalProperty).to.equal('reports_to') }) }) it('Fails to create a new registry organization with an erroneous key not found in the schema', async () => { await chai.request(app) - .post('/api/registryOrg') + .post('/api/registry/org') .set(secretariatHeaders) .send({ ...testRegistryOrg, @@ -122,13 +124,29 @@ describe('Testing /registryOrg endpoints', () => { expect(res.body.errors[0].message).to.equal('must NOT have additional properties') }) }) + it('Fails to create a new registry organization with an invalid partner_role_type enum value', async () => { + await chai.request(app) + .post('/api/registry/org') + .set(secretariatHeaders) + .send({ + ...testRegistryOrg, + short_name: 'test_create_invalid_enum', + partner_role_type: 'Invalid Enum Value' + }) + .then((res) => { + expect(res).to.have.status(400) + expect(res.body.message).to.equal('Parameters were invalid') + expect(res.body.errors[0].message).to.equal('must be equal to one of the allowed values') + expect(res.body.errors[0].instancePath).to.equal('/partner_role_type') + }) + }) }) }) context('Testing GET /registryOrg endpoints', () => { context('Positive Tests', () => { it('Gets a list of all registry organizations', async () => { await chai.request(app) - .get('/api/registryOrg') + .get('/api/registry/org') .set(secretariatHeaders) .then((res) => { expect(res).to.have.status(200) @@ -143,16 +161,15 @@ describe('Testing /registryOrg endpoints', () => { }) it('Gets a registry organization by short name', async () => { await chai.request(app) - .get('/api/registryOrg/registry_org_test') + .get('/api/registry/org/registry_org_test') .set(secretariatHeaders) .then((res) => { expect(res).to.have.status(200) expect(res.body).to.have.property('long_name', createdOrg.long_name) expect(res.body).to.have.property('short_name', createdOrg.short_name) expect(res.body.authority).to.be.an('array').that.includes('CNA') - expect(res.body).to.have.property('partner_role', createdOrg.partner_role) + expect(res.body).to.have.property('partner_role_type', createdOrg.partner_role_type) expect(res.body).to.have.property('partner_number', createdOrg.partner_number) - expect(res.body).to.have.property('partner_type', createdOrg.partner_type) expect(res.body).to.have.property('partner_country', createdOrg.partner_country) }) }) @@ -160,7 +177,7 @@ describe('Testing /registryOrg endpoints', () => { // 1. Get win_5 org UUID let win5UUID await chai.request(app) - .get('/api/registryOrg/win_5') + .get('/api/registry/org/win_5') .set(secretariatHeaders) .then((res) => { expect(res).to.have.status(200) @@ -184,7 +201,7 @@ describe('Testing /registryOrg endpoints', () => { // 3. GET win_5 as Secretariat, verify author_id is present await chai.request(app) - .get('/api/registryOrg/win_5') + .get('/api/registry/org/win_5') .set(secretariatHeaders) .then((res) => { expect(res).to.have.status(200) @@ -212,7 +229,7 @@ describe('Testing /registryOrg endpoints', () => { context('Negative Tests', () => { it('Fails to get a registry organization that does not exist', async () => { await chai.request(app) - .get('/api/registryOrg/registry_org_test2') + .get('/api/registry/org/registry_org_test2') .set(secretariatHeaders) .then((res) => { expect(res).to.have.status(404) @@ -225,14 +242,13 @@ describe('Testing /registryOrg endpoints', () => { context('Positive Tests', () => { it('Updates a registry organization providing a full organization object', async () => { await chai.request(app) - .put('/api/registryOrg/registry_org_test') + .put('/api/registry/org/registry_org_test') .set(secretariatHeaders) .send({ ...createdOrg, long_name: 'Registry Org Test Updated', - partner_role: 'Updated Partner Role', + partner_role_type: 'Researcher', partner_number: 'Updated Partner Number', - partner_type: 'Updated Partner Type', partner_country: 'UK' }) .then((res, err) => { @@ -259,15 +275,12 @@ describe('Testing /registryOrg endpoints', () => { expect(res.body.updated).to.haveOwnProperty('hard_quota') expect(res.body.updated.hard_quota).to.equal(createdOrg.hard_quota) - expect(res.body.updated).to.haveOwnProperty('partner_role') - expect(res.body.updated.partner_role).to.equal('Updated Partner Role') + expect(res.body.updated).to.haveOwnProperty('partner_role_type') + expect(res.body.updated.partner_role_type).to.equal('Researcher') expect(res.body.updated).to.haveOwnProperty('partner_number') expect(res.body.updated.partner_number).to.equal('Updated Partner Number') - expect(res.body.updated).to.haveOwnProperty('partner_type') - expect(res.body.updated.partner_type).to.equal('Updated Partner Type') - expect(res.body.updated).to.haveOwnProperty('partner_country') expect(res.body.updated.partner_country).to.equal('UK') }) @@ -287,6 +300,8 @@ describe('Testing /registryOrg endpoints', () => { expect(res).to.have.status(200) expect(res.body.updated).to.haveOwnProperty('partner_data') expect(res.body.updated.partner_data.status).to.equal('active') + expect(res.body.updated.partner_data).to.haveOwnProperty('partner_active_date') + expect(res.body.updated.partner_data).to.not.haveOwnProperty('partner_inactive_date') }) }) it('Updates a registry organization\'s short name and role simultaneously to verify read-after-write audit logic', async () => { @@ -334,7 +349,7 @@ describe('Testing /registryOrg endpoints', () => { } let createdSubOrgUUID await chai.request(app) - .post('/api/registryOrg') + .post('/api/registry/org') .set(secretariatHeaders) .send(subOrg) .then(res => { @@ -344,7 +359,7 @@ describe('Testing /registryOrg endpoints', () => { // Update the main org to oversee it await chai.request(app) - .put(`/api/registryOrg/${createdOrg.short_name}`) + .put(`/api/registry/org/${createdOrg.short_name}`) .set(secretariatHeaders) .send({ ...createdOrg, @@ -357,7 +372,7 @@ describe('Testing /registryOrg endpoints', () => { // Assert that the sub org dynamically returns reports_to matching the main org's UUID await chai.request(app) - .get(`/api/registryOrg/${subOrg.short_name}`) + .get(`/api/registry/org/${subOrg.short_name}`) .set(secretariatHeaders) .then(res => { expect(res).to.have.status(200) @@ -373,7 +388,7 @@ describe('Testing /registryOrg endpoints', () => { context('Negative Tests', () => { it('Fails to update a registry organization that does not exist', async () => { await chai.request(app) - .put('/api/registryOrg/registry_org_test2') + .put('/api/registry/org/registry_org_test2') .set(secretariatHeaders) .send({ ...createdOrg, @@ -386,7 +401,7 @@ describe('Testing /registryOrg endpoints', () => { }) it("Fails to update a registry organization's short name to one that already exists", async () => { await chai.request(app) - .put('/api/registryOrg/registry_org_test') + .put('/api/registry/org/registry_org_test') .set(secretariatHeaders) .send({ ...createdOrg, @@ -399,7 +414,7 @@ describe('Testing /registryOrg endpoints', () => { }) it('Fails to update a registry organization providing invalid data', async () => { await chai.request(app) - .put('/api/registryOrg/registry_org_test') + .put('/api/registry/org/registry_org_test') .set(secretariatHeaders) .send({ ...createdOrg, @@ -410,12 +425,27 @@ describe('Testing /registryOrg endpoints', () => { expect(res.body.message).to.equal('Parameters were invalid') }) }) + it('Fails to update a registry organization providing an invalid partner_role_type enum value', async () => { + await chai.request(app) + .put('/api/registry/org/registry_org_test') + .set(secretariatHeaders) + .send({ + ...createdOrg, + partner_role_type: 'Invalid Enum Value' + }) + .then((res) => { + expect(res).to.have.status(400) + expect(res.body.message).to.equal('Parameters were invalid') + expect(res.body.errors[0].message).to.equal('must be equal to one of the allowed values') + expect(res.body.errors[0].instancePath).to.equal('/partner_role_type') + }) + }) it('Ignores protected fields such as users and admins during an update', async () => { const maliciousUsers = ['d41d8cd9-8f00-3204-a980-0998ecf8427e'] const maliciousAdmins = ['d41d8cd9-8f00-3204-a980-0998ecf8427e'] await chai.request(app) - .put(`/api/registryOrg/${createdOrg.short_name}`) + .put(`/api/registry/org/${createdOrg.short_name}`) .set(secretariatHeaders) .send({ ...createdOrg, @@ -429,7 +459,7 @@ describe('Testing /registryOrg endpoints', () => { }) it('Fails to update a registry organization with reports_to manually provided', async () => { await chai.request(app) - .put(`/api/registryOrg/${createdOrg.short_name}`) + .put(`/api/registry/org/${createdOrg.short_name}`) .set(secretariatHeaders) .send({ ...createdOrg, @@ -438,12 +468,13 @@ describe('Testing /registryOrg endpoints', () => { .then((res) => { expect(res).to.have.status(400) expect(res.body.message).to.equal('Parameters were invalid') - expect(res.body.details[0].msg).to.equal('reports_to must not be present') + expect(res.body.errors[0].message).to.equal('must NOT have additional properties') + expect(res.body.errors[0].params.additionalProperty).to.equal('reports_to') }) }) it('Fails to update a registry organization providing an erroneous key not found in the schema', async () => { await chai.request(app) - .put('/api/registryOrg/registry_org_test') + .put('/api/registry/org/registry_org_test') .set(secretariatHeaders) .send({ ...createdOrg, @@ -457,7 +488,7 @@ describe('Testing /registryOrg endpoints', () => { }) it('Fails to update a registry organization with an invalidly high quota', async () => { await chai.request(app) - .put(`/api/registryOrg/${createdOrg.short_name}`) + .put(`/api/registry/org/${createdOrg.short_name}`) .set(secretariatHeaders) .send({ ...createdOrg, diff --git a/test/integration-tests/registry-org/rootOrgTest.js b/test/integration-tests/registry-org/rootOrgTest.js index c78896ad8..d1b03836c 100644 --- a/test/integration-tests/registry-org/rootOrgTest.js +++ b/test/integration-tests/registry-org/rootOrgTest.js @@ -32,6 +32,7 @@ describe('Testing ROOT Organization Type', () => { createdOrg = res.body.created delete createdOrg.created delete createdOrg.last_updated + delete createdOrg.partner_data }) }) @@ -89,6 +90,34 @@ describe('Testing ROOT Organization Type', () => { }) }) + it('ROOT admin cannot edit top_level_root', async () => { + await chai.request(app) + .put(`/api/registry/org/${testRootOrg.short_name}`) + .set(rootAdminHeaders) + .send({ + ...createdOrg, + top_level_root: 'true' + }) + .then((res) => { + expect(res).to.have.status(403) + expect(res.body.error).to.equal('SECRETARIAT_ONLY') + }) + }) + + it('ROOT admin cannot edit oversees', async () => { + await chai.request(app) + .put(`/api/registry/org/${testRootOrg.short_name}`) + .set(rootAdminHeaders) + .send({ + ...createdOrg, + oversees: ['some_other_uuid'] + }) + .then((res) => { + expect(res).to.have.status(403) + expect(res.body.error).to.equal('SECRETARIAT_ONLY') + }) + }) + it('ROOT admin cannot reserve CVE IDs', async () => { await chai.request(app) .post('/api/cve-id') diff --git a/test/integration-tests/user/updateUserTest.js b/test/integration-tests/user/updateUserTest.js index 3871a4b2a..e03007bc5 100644 --- a/test/integration-tests/user/updateUserTest.js +++ b/test/integration-tests/user/updateUserTest.js @@ -326,6 +326,7 @@ describe('Testing Edit user endpoint', () => { test: 'additional key not in schema' }) .then((res) => { + if (res.status === 403) console.log(JSON.stringify(res.body, null, 2)) expect(res).to.have.status(400) expect(res.body.message).to.equal('Parameters were invalid') expect(res.body.errors[0].message).to.equal('must NOT have additional properties') From bca191b846116eb864f3e55d7688c436cd268c7f Mon Sep 17 00:00:00 2001 From: david-rocca Date: Fri, 24 Apr 2026 14:02:08 -0400 Subject: [PATCH 22/86] More tests, and work on advisory location changes --- api-docs/openapi.json | 2 +- schemas/registry-org/BaseOrg.json | 11 +++++ schemas/registry-org/CNAOrg.json | 4 ++ schemas/registry-org/RootOrg.json | 1 + .../create-registry-org-request.json | 5 ++ .../create-registry-org-response.json | 5 ++ .../get-registry-org-response.json | 15 ++++-- .../list-registry-orgs-response.json | 15 ++++-- .../update-registry-org-request.json | 5 ++ .../update-registry-org-response.json | 5 ++ src/constants/index.js | 4 +- src/controller/org.controller/index.js | 3 +- .../org.controller/org.middleware.js | 22 +++++---- .../registry-org.middleware.js | 5 +- src/model/baseorg.js | 7 +-- src/repositories/baseOrgRepository.js | 8 ++-- .../registry-org/registryOrgCRUDTest.js | 46 +++++++++++++++++-- 17 files changed, 129 insertions(+), 34 deletions(-) diff --git a/api-docs/openapi.json b/api-docs/openapi.json index 6dece2a8e..1044a3c4a 100644 --- a/api-docs/openapi.json +++ b/api-docs/openapi.json @@ -2605,7 +2605,7 @@ "Registry Organization" ], "summary": "Updates information about the organization specified by short name (accessible Temporarily to Secretariat only)", - "description": "

    Access Control

    User must belong to an organization with the Secretariat role temporarily.

    In the future, only the organization's admin will be able to request changes to its information.

    With Joint Approval required for the following fields:

    Expected Behavior

    This endpoint expects a full organization object in the request body.

    Secretariat: Updates any organization's information

    Organization Admin: Requests changes to its organization's information

    • short_name
    • long_name
    • authority
    • aliases
    • oversees
    • top_level_root
    • charter_or_scope
    • product_list
    • disclosure_policy
    • contact_info.poc
    • contact_info.poc_email
    • contact_info.poc_phone
    • contact_info.org_email
    • partner_role_type
    • partner_country
    • vulnerability_advisory_locations
    • advisory_location_require_credentials
    • industry
    • tl_root_start_date
    • is_cna_discussion_list
    ", + "description": "

    Access Control

    User must belong to an organization with the Secretariat role temporarily.

    In the future, only the organization's admin will be able to request changes to its information.

    With Joint Approval required for the following fields:

    Expected Behavior

    This endpoint expects a full organization object in the request body.

    Secretariat: Updates any organization's information

    Organization Admin: Requests changes to its organization's information

    • short_name
    • long_name
    • authority
    • aliases
    • oversees
    • top_level_root
    • charter_or_scope
    • product_list
    • disclosure_policy
    • contact_info.poc
    • contact_info.poc_email
    • contact_info.poc_phone
    • contact_info.org_email
    • partner_role_type
    • partner_country
    • advisory_locations
    • industry
    • tl_root_start_date
    • is_cna_discussion_list
    ", "operationId": "orgUpdateSingle", "parameters": [ { diff --git a/schemas/registry-org/BaseOrg.json b/schemas/registry-org/BaseOrg.json index 085cdab1a..835bf3697 100644 --- a/schemas/registry-org/BaseOrg.json +++ b/schemas/registry-org/BaseOrg.json @@ -170,9 +170,20 @@ }, "status": { "type": "string" + }, + "advisory_location_require_credentials": { + "type": "boolean" + }, + "vulnerability_advisory_location_for_web_scraping": { + "type": "array", + "items": { "type": "string" } } }, "additionalProperties": false + }, + "advisory_locations": { + "type": "array", + "items": { "type": "string" } } }, "required": [ diff --git a/schemas/registry-org/CNAOrg.json b/schemas/registry-org/CNAOrg.json index a4d676b2d..e5e6b8687 100644 --- a/schemas/registry-org/CNAOrg.json +++ b/schemas/registry-org/CNAOrg.json @@ -62,6 +62,9 @@ "format": "uuid" } }, + "charter_or_scope": { "type": "string" }, + "disclosure_policy": { "type": "string" }, + "product_list": { "type": "string" }, "partner_role_type": { "$ref": "/BaseOrg#/definitions/partnerRoleType" }, @@ -71,6 +74,7 @@ "partner_country": { "type": "string" }, + "advisory_locations": { "$ref": "/BaseOrg#/properties/advisory_locations" }, "partner_data": { "$ref": "/BaseOrg#/properties/partner_data" } }, "required": ["short_name", "hard_quota"] diff --git a/schemas/registry-org/RootOrg.json b/schemas/registry-org/RootOrg.json index 3c28e49eb..41cd83827 100644 --- a/schemas/registry-org/RootOrg.json +++ b/schemas/registry-org/RootOrg.json @@ -51,6 +51,7 @@ "partner_country": { "type": "string" }, + "advisory_locations": { "$ref": "/BaseOrg#/properties/advisory_locations" }, "partner_data": { "$ref": "/BaseOrg#/properties/partner_data" } }, "required": ["short_name"] diff --git a/schemas/registry-org/create-registry-org-request.json b/schemas/registry-org/create-registry-org-request.json index 10c182b50..6d9efc4cf 100644 --- a/schemas/registry-org/create-registry-org-request.json +++ b/schemas/registry-org/create-registry-org-request.json @@ -98,6 +98,11 @@ } }, "required": ["poc", "poc_email", "admins", "org_email"] + }, + "advisory_locations": { + "type": "array", + "items": { "type": "string" }, + "description": "Locations of vulnerability advisories" } }, "required": [ diff --git a/schemas/registry-org/create-registry-org-response.json b/schemas/registry-org/create-registry-org-response.json index 43dd86f1c..fb93b4e25 100644 --- a/schemas/registry-org/create-registry-org-response.json +++ b/schemas/registry-org/create-registry-org-response.json @@ -133,6 +133,11 @@ }, "required": ["poc", "poc_email", "admins", "org_email"] }, + "advisory_locations": { + "type": "array", + "items": { "type": "string" }, + "description": "Locations of vulnerability advisories" + }, "in_use": { "type": "boolean", "description": "Indicates if the organization is currently active" diff --git a/schemas/registry-org/get-registry-org-response.json b/schemas/registry-org/get-registry-org-response.json index 2a2824f51..0d66b39a3 100644 --- a/schemas/registry-org/get-registry-org-response.json +++ b/schemas/registry-org/get-registry-org-response.json @@ -136,21 +136,26 @@ }, "status": { "type": "string" + }, + "advisory_location_require_credentials": { + "type": "boolean", + "description": "Indicates if advisory locations require credentials" + }, + "vulnerability_advisory_location_for_web_scraping": { + "type": "array", + "items": { "type": "string" }, + "description": "Advisory locations for web scraping" } }, "description": "Additional partner metadata (restricted)" }, - "vulnerability_advisory_locations": { + "advisory_locations": { "type": "array", "items": { "type": "string" }, "description": "Locations of vulnerability advisories" }, - "advisory_location_require_credentials": { - "type": "boolean", - "description": "Indicates if advisory locations require credentials" - }, "industry": { "type": "string", "description": "Industry sector of the organization" diff --git a/schemas/registry-org/list-registry-orgs-response.json b/schemas/registry-org/list-registry-orgs-response.json index a074e701e..3ce381ff1 100644 --- a/schemas/registry-org/list-registry-orgs-response.json +++ b/schemas/registry-org/list-registry-orgs-response.json @@ -165,21 +165,26 @@ }, "status": { "type": "string" + }, + "advisory_location_require_credentials": { + "type": "boolean", + "description": "Indicates if advisory locations require credentials" + }, + "vulnerability_advisory_location_for_web_scraping": { + "type": "array", + "items": { "type": "string" }, + "description": "Advisory locations for web scraping" } }, "description": "Additional partner metadata (restricted)" }, - "vulnerability_advisory_locations": { + "advisory_locations": { "type": "array", "items": { "type": "string" }, "description": "Locations of vulnerability advisories" }, - "advisory_location_require_credentials": { - "type": "boolean", - "description": "Indicates if advisory locations require credentials" - }, "industry": { "type": "string", "description": "Industry sector of the organization" diff --git a/schemas/registry-org/update-registry-org-request.json b/schemas/registry-org/update-registry-org-request.json index 40bdb7bef..b4dca49ab 100644 --- a/schemas/registry-org/update-registry-org-request.json +++ b/schemas/registry-org/update-registry-org-request.json @@ -108,6 +108,11 @@ "description": "Organization's website URL" } } + }, + "advisory_locations": { + "type": "array", + "items": { "type": "string" }, + "description": "Locations of vulnerability advisories" } } } diff --git a/schemas/registry-org/update-registry-org-response.json b/schemas/registry-org/update-registry-org-response.json index 6494839f2..e2d2aee57 100644 --- a/schemas/registry-org/update-registry-org-response.json +++ b/schemas/registry-org/update-registry-org-response.json @@ -128,6 +128,11 @@ }, "required": ["poc", "poc_email", "admins", "org_email"] }, + "advisory_locations": { + "type": "array", + "items": { "type": "string" }, + "description": "Locations of vulnerability advisories" + }, "in_use": { "type": "boolean", "description": "Indicates if the organization is currently active" diff --git a/src/constants/index.js b/src/constants/index.js index c2264ecd9..6b4494d28 100644 --- a/src/constants/index.js +++ b/src/constants/index.js @@ -44,7 +44,7 @@ function getConstants () { USER_ROLES: [ 'ADMIN' ], - JOINT_APPROVAL_FIELDS: ['short_name', 'long_name', 'authority', 'aliases', 'oversees', 'top_level_root', 'charter_or_scope', 'product_list', 'disclosure_policy', 'contact_info.poc', 'contact_info.poc_email', 'contact_info.poc_phone', 'contact_info.org_email', 'partner_role_type', 'partner_number', 'partner_country', 'partner_data.cve_website_update_date', 'partner_data.cve_website_update_needed', 'partner_data.status', 'vulnerability_advisory_locations', 'advisory_location_require_credentials', 'industry', 'tl_root_start_date', 'is_cna_discussion_list', 'hard_quota'], + JOINT_APPROVAL_FIELDS: ['short_name', 'long_name', 'authority', 'aliases', 'oversees', 'top_level_root', 'charter_or_scope', 'product_list', 'disclosure_policy', 'contact_info.poc', 'contact_info.poc_email', 'contact_info.poc_phone', 'contact_info.org_email', 'partner_role_type', 'partner_number', 'partner_country', 'partner_data.cve_website_update_date', 'partner_data.cve_website_update_needed', 'partner_data.status', 'advisory_locations', 'industry', 'tl_root_start_date', 'is_cna_discussion_list', 'hard_quota'], JOINT_APPROVAL_FIELDS_LEGACY: ['short_name', 'name', 'authority.active_roles', 'policies.id_quota'], ORG_EXCLUDED_FIELDS: ['__t', '__v', '_id', 'inUse', 'in_use'], ORG_RESTRICTED_FIELDS: ['partner_data'], @@ -54,6 +54,8 @@ function getConstants () { 'partner_data.cve_website_update_date', 'partner_data.cve_website_update_needed', 'partner_data.status', + 'partner_data.advisory_location_require_credentials', + 'partner_data.vulnerability_advisory_location_for_web_scraping', 'top_level_root', 'oversees' ], diff --git a/src/controller/org.controller/index.js b/src/controller/org.controller/index.js index 0a0caa936..1dd51a767 100644 --- a/src/controller/org.controller/index.js +++ b/src/controller/org.controller/index.js @@ -558,8 +558,7 @@ router.put('/registry/org/:shortname',
  • contact_info.org_email
  • partner_role_type
  • partner_country
  • -
  • vulnerability_advisory_locations
  • -
  • advisory_location_require_credentials
  • +
  • advisory_locations
  • industry
  • tl_root_start_date
  • is_cna_discussion_list
  • diff --git a/src/controller/org.controller/org.middleware.js b/src/controller/org.controller/org.middleware.js index b5e485eb6..3ff24f84b 100644 --- a/src/controller/org.controller/org.middleware.js +++ b/src/controller/org.controller/org.middleware.js @@ -48,12 +48,15 @@ function validateCreateOrgParameters () { .isArray(), body(['top_level_root']).default('') .isString(), - body(['vulnerability_advisory_locations']) + body(['advisory_locations']) .default([]) .custom(isFlatStringArray), - body(['advisory_location_require_credentials']) + body(['partner_data.advisory_location_require_credentials']) .default(false) .isBoolean(), + body(['partner_data.vulnerability_advisory_location_for_web_scraping']) + .default([]) + .custom(isFlatStringArray), body(['tl_root_start_date']) .default(null) .isDate(), @@ -151,8 +154,9 @@ function validateCreateOrgParameters () { 'partner_data.cve_website_update_date', 'partner_data.cve_website_update_needed', 'partner_data.status', - 'vulnerability_advisory_locations', - 'advisory_location_require_credentials', + 'advisory_locations', + 'partner_data.advisory_location_require_credentials', + 'partner_data.vulnerability_advisory_location_for_web_scraping', 'industry', 'tl_root_start_date', 'is_cna_discussion_list') @@ -239,8 +243,9 @@ function validateUpdateOrgParameters () { 'partner_data.cve_website_update_date', 'partner_data.cve_website_update_needed', 'partner_data.status', - 'vulnerability_advisory_locations', - 'advisory_location_require_credentials', + 'partner_data.advisory_location_require_credentials', + 'partner_data.vulnerability_advisory_location_for_web_scraping', + 'advisory_locations', 'industry', 'tl_root_start_date', 'is_cna_discussion_list' @@ -331,8 +336,9 @@ const QUERY_PARAMETERS = { 'partner_data.cve_website_update_date', 'partner_data.cve_website_update_needed', 'partner_data.status', - 'vulnerability_advisory_locations', - 'advisory_location_require_credentials', + 'partner_data.advisory_location_require_credentials', + 'partner_data.vulnerability_advisory_location_for_web_scraping', + 'advisory_locations', 'industry', 'tl_root_start_date', 'is_cna_discussion_list', diff --git a/src/controller/registry-org.controller/registry-org.middleware.js b/src/controller/registry-org.controller/registry-org.middleware.js index 4d51d54dc..2adf36675 100644 --- a/src/controller/registry-org.controller/registry-org.middleware.js +++ b/src/controller/registry-org.controller/registry-org.middleware.js @@ -23,8 +23,9 @@ function parsePostParams (req, res, next) { 'partner_data.cve_website_update_date', 'partner_data.cve_website_update_needed', 'partner_data.status', - 'vulnerability_advisory_locations', - 'advisory_location_require_credentials', + 'advisory_locations', + 'partner_data.advisory_location_require_credentials', + 'partner_data.vulnerability_advisory_location_for_web_scraping', 'industry', 'tl_root_start_date', 'is_cna_discussion_list' diff --git a/src/model/baseorg.js b/src/model/baseorg.js index 0c72e8837..4b0d057d5 100644 --- a/src/model/baseorg.js +++ b/src/model/baseorg.js @@ -30,10 +30,11 @@ const schema = { cve_website_update_needed: Boolean, partner_active_date: Date, partner_inactive_date: Date, - status: String + status: String, + advisory_location_require_credentials: Boolean, + vulnerability_advisory_location_for_web_scraping: [String] }, - vulnerability_advisory_locations: [String], - advisory_location_require_credentials: Boolean, + advisory_locations: [String], industry: String, tl_root_start_date: Date, is_cna_discussion_list: Boolean, diff --git a/src/repositories/baseOrgRepository.js b/src/repositories/baseOrgRepository.js index dad32b10b..cd58833eb 100644 --- a/src/repositories/baseOrgRepository.js +++ b/src/repositories/baseOrgRepository.js @@ -625,8 +625,9 @@ class BaseOrgRepository extends BaseRepository { * @param {string} [incomingParameters.contact_info.website] - The organization's website URL. (Registry only) * @param {string} [incomingParameters.cna_role_type] - (Registry only) * @param {string} [incomingParameters.cna_country] - (Registry only) - * @param {string[]} [incomingParameters.vulnerability_advisory_locations] - (Registry only) - * @param {boolean} [incomingParameters.advisory_location_require_credentials] - (Registry only) + * @param {string[]} [incomingParameters.advisory_locations] - (Registry only) + * @param {boolean} [incomingParameters.partner_data.advisory_location_require_credentials] - (Registry only) + * @param {string[]} [incomingParameters.partner_data.vulnerability_advisory_location_for_web_scraping] - (Registry only) * @param {string} [incomingParameters.industry] - (Registry only) * @param {string} [incomingParameters.tl_root_start_date] - (Registry only) * @param {boolean} [incomingParameters.is_cna_discussion_list] - (Registry only) @@ -713,8 +714,7 @@ class BaseOrgRepository extends BaseRepository { 'partner_role_type', 'partner_number', 'partner_country', - 'vulnerability_advisory_locations', - 'advisory_location_require_credentials', + 'advisory_locations', 'industry', 'tl_root_start_date', 'is_cna_discussion_list' diff --git a/test/integration-tests/registry-org/registryOrgCRUDTest.js b/test/integration-tests/registry-org/registryOrgCRUDTest.js index c85b0eb66..8870616b7 100644 --- a/test/integration-tests/registry-org/registryOrgCRUDTest.js +++ b/test/integration-tests/registry-org/registryOrgCRUDTest.js @@ -15,7 +15,8 @@ const testRegistryOrg = { hard_quota: 1000, partner_role_type: 'Vendor', partner_number: 'Initial Partner Number', - partner_country: 'US' + partner_country: 'US', + advisory_locations: ['https://example.com/advisories'] } let createdOrg @@ -59,6 +60,9 @@ describe('Testing /registryOrg endpoints', () => { expect(res.body.created).to.haveOwnProperty('partner_country') expect(res.body.created.partner_country).to.equal(testRegistryOrg.partner_country) + expect(res.body.created).to.haveOwnProperty('advisory_locations') + expect(res.body.created.advisory_locations).to.deep.equal(testRegistryOrg.advisory_locations) + expect(res.body.created).to.haveOwnProperty('partner_data') expect(res.body.created.partner_data.status).to.equal('inactive') expect(res.body.created.partner_data).to.haveOwnProperty('partner_inactive_date') @@ -171,6 +175,8 @@ describe('Testing /registryOrg endpoints', () => { expect(res.body).to.have.property('partner_role_type', createdOrg.partner_role_type) expect(res.body).to.have.property('partner_number', createdOrg.partner_number) expect(res.body).to.have.property('partner_country', createdOrg.partner_country) + expect(res.body).to.have.property('partner_country', createdOrg.partner_country) + expect(res.body.advisory_locations).to.deep.equal(createdOrg.advisory_locations) }) }) it('Strips author_id from conversations for non-secretariats', async () => { @@ -249,7 +255,8 @@ describe('Testing /registryOrg endpoints', () => { long_name: 'Registry Org Test Updated', partner_role_type: 'Researcher', partner_number: 'Updated Partner Number', - partner_country: 'UK' + partner_country: 'UK', + advisory_locations: ['https://example.com/updated_advisories'] }) .then((res, err) => { expect(err).to.be.undefined @@ -283,6 +290,9 @@ describe('Testing /registryOrg endpoints', () => { expect(res.body.updated).to.haveOwnProperty('partner_country') expect(res.body.updated.partner_country).to.equal('UK') + + expect(res.body.updated).to.haveOwnProperty('advisory_locations') + expect(res.body.updated.advisory_locations).to.deep.equal(['https://example.com/updated_advisories']) }) }) it('Allows Secretariat to update partner_data', async () => { @@ -292,7 +302,9 @@ describe('Testing /registryOrg endpoints', () => { .send({ ...createdOrg, partner_data: { - status: 'active' + status: 'active', + advisory_location_require_credentials: true, + vulnerability_advisory_location_for_web_scraping: ['https://example.com/scraping'] } }) .then((res, err) => { @@ -302,6 +314,8 @@ describe('Testing /registryOrg endpoints', () => { expect(res.body.updated.partner_data.status).to.equal('active') expect(res.body.updated.partner_data).to.haveOwnProperty('partner_active_date') expect(res.body.updated.partner_data).to.not.haveOwnProperty('partner_inactive_date') + expect(res.body.updated.partner_data.advisory_location_require_credentials).to.be.true + expect(res.body.updated.partner_data.vulnerability_advisory_location_for_web_scraping).to.deep.equal(['https://example.com/scraping']) }) }) it('Updates a registry organization\'s short name and role simultaneously to verify read-after-write audit logic', async () => { @@ -472,6 +486,32 @@ describe('Testing /registryOrg endpoints', () => { expect(res.body.errors[0].params.additionalProperty).to.equal('reports_to') }) }) + it('Fails to allow an admin to set a secretariat-only field', async () => { + let win5Org + await chai.request(app) + .get('/api/registry/org/win_5') + .set(secretariatHeaders) + .then((res) => { + win5Org = res.body + }) + + await chai.request(app) + .put('/api/registry/org/win_5') + .set(constants.nonSecretariatUserHeaders2) + .send({ + short_name: win5Org.short_name, + long_name: win5Org.long_name, + authority: win5Org.authority, + hard_quota: win5Org.hard_quota, + partner_data: { + status: 'active' + } + }) + .then((res) => { + expect(res).to.have.status(403) + expect(res.body.message).to.equal('The following fields can only be modified by the Secretariat: partner_data, partner_data.status.') + }) + }) it('Fails to update a registry organization providing an erroneous key not found in the schema', async () => { await chai.request(app) .put('/api/registry/org/registry_org_test') From a5d40578b6c87ca755917b895bac65242c08e7fb Mon Sep 17 00:00:00 2001 From: david-rocca Date: Fri, 24 Apr 2026 14:29:36 -0400 Subject: [PATCH 23/86] moved poc, probably will need more work --- schemas/registry-org/BaseOrg.json | 37 ++-- schemas/registry-org/CNAOrg.json | 76 ++++++-- .../create-registry-org-request.json | 69 ++++--- .../create-registry-org-response.json | 78 +++++--- .../get-registry-org-response.json | 176 +++++++++--------- .../list-registry-orgs-response.json | 64 ++++--- .../update-registry-org-request.json | 66 ++++--- .../update-registry-org-response.json | 68 ++++--- .../org.controller/org.middleware.js | 30 +-- .../registry-org.middleware.js | 3 +- src/model/baseorg.js | 9 +- test/integration-tests/constants.js | 9 +- 12 files changed, 405 insertions(+), 280 deletions(-) diff --git a/schemas/registry-org/BaseOrg.json b/schemas/registry-org/BaseOrg.json index 835bf3697..91969b4e6 100644 --- a/schemas/registry-org/BaseOrg.json +++ b/schemas/registry-org/BaseOrg.json @@ -123,29 +123,36 @@ "properties": { "additional_contact_users": { "type": "array", - "uniqueItems": true, "items": { - "$ref": "#/definitions/uuidType" + "type": "object", + "properties": { + "phone": { + "type": "string" + }, + "poc": { + "type": "string" + }, + "poc_email": { + "type": "string", + "format": "email" + } + }, + "additionalProperties": false } }, - "poc": { + "phone": { "type": "string" }, - "poc_email": { - "type": "string", - "format": "email" - }, - "poc_phone": { + "poc": { "type": "string" }, - "org_email": { + "poc_email": { "type": "string", "format": "email" }, "website": { "type": "string", - "format": "uri", - "description": "Organization's website URL" + "format": "uri" } }, "additionalProperties": false @@ -176,14 +183,18 @@ }, "vulnerability_advisory_location_for_web_scraping": { "type": "array", - "items": { "type": "string" } + "items": { + "type": "string" + } } }, "additionalProperties": false }, "advisory_locations": { "type": "array", - "items": { "type": "string" } + "items": { + "type": "string" + } } }, "required": [ diff --git a/schemas/registry-org/CNAOrg.json b/schemas/registry-org/CNAOrg.json index e5e6b8687..99a3d9e7d 100644 --- a/schemas/registry-org/CNAOrg.json +++ b/schemas/registry-org/CNAOrg.json @@ -6,24 +6,57 @@ "description": "Schema for a CVE CNA Organization", "additionalProperties": false, "properties": { - "UUID": { "$ref": "/BaseOrg#/definitions/uuidType" }, - "short_name": { "$ref": "/BaseOrg#/definitions/shortName" }, - "long_name": { "$ref": "/BaseOrg#/definitions/longName" }, + "UUID": { + "$ref": "/BaseOrg#/definitions/uuidType" + }, + "short_name": { + "$ref": "/BaseOrg#/definitions/shortName" + }, + "long_name": { + "$ref": "/BaseOrg#/definitions/longName" + }, "new_short_name": { "description": "Used to rename an organization's short name during an update.", "type": "string", "minLength": 2, "maxLength": 32 }, - "aliases": { "$ref": "/BaseOrg#/properties/aliases" }, + "aliases": { + "$ref": "/BaseOrg#/properties/aliases" + }, "contact_info": { "type": "object", "properties": { - "poc": { "type": "string" }, - "poc_email": { "type": "string" }, - "poc_phone": { "type": "string" }, - "org_email": { "type": "string" }, - "website": { "type": "string" } + "additional_contact_users": { + "type": "array", + "items": { + "type": "object", + "properties": { + "phone": { + "type": "string" + }, + "poc": { + "type": "string" + }, + "poc_email": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "phone": { + "type": "string" + }, + "poc": { + "type": "string" + }, + "poc_email": { + "type": "string" + }, + "website": { + "type": "string" + } }, "additionalProperties": false }, @@ -62,9 +95,15 @@ "format": "uuid" } }, - "charter_or_scope": { "type": "string" }, - "disclosure_policy": { "type": "string" }, - "product_list": { "type": "string" }, + "charter_or_scope": { + "type": "string" + }, + "disclosure_policy": { + "type": "string" + }, + "product_list": { + "type": "string" + }, "partner_role_type": { "$ref": "/BaseOrg#/definitions/partnerRoleType" }, @@ -74,8 +113,15 @@ "partner_country": { "type": "string" }, - "advisory_locations": { "$ref": "/BaseOrg#/properties/advisory_locations" }, - "partner_data": { "$ref": "/BaseOrg#/properties/partner_data" } + "advisory_locations": { + "$ref": "/BaseOrg#/properties/advisory_locations" + }, + "partner_data": { + "$ref": "/BaseOrg#/properties/partner_data" + } }, - "required": ["short_name", "hard_quota"] + "required": [ + "short_name", + "hard_quota" + ] } \ No newline at end of file diff --git a/schemas/registry-org/create-registry-org-request.json b/schemas/registry-org/create-registry-org-request.json index 6d9efc4cf..1a3fe8d76 100644 --- a/schemas/registry-org/create-registry-org-request.json +++ b/schemas/registry-org/create-registry-org-request.json @@ -22,10 +22,16 @@ }, "authority": { "type": "array", - "items": { - "type": "string", - "enum": ["CNA", "ADP", "BULK_DOWNLOAD", "SECRETARIAT", "ROOT"] - } + "items": { + "type": "string", + "enum": [ + "CNA", + "ADP", + "BULK_DOWNLOAD", + "SECRETARIAT", + "ROOT" + ] + } }, "oversees": { "type": "array", @@ -63,45 +69,48 @@ "additional_contact_users": { "type": "array", "items": { - "type": "string" + "type": "object", + "properties": { + "phone": { + "type": "string" + }, + "poc": { + "type": "string" + }, + "poc_email": { + "type": "string", + "format": "email" + } + }, + "additionalProperties": false } }, + "phone": { + "type": "string" + }, "poc": { - "type": "string", - "description": "Point of contact name" + "type": "string" }, "poc_email": { "type": "string", - "format": "email", - "description": "Point of contact email" - }, - "poc_phone": { - "type": "string", - "description": "Point of contact phone number" - }, - "admins": { - "type": "array", - "items": { - "type": "string" - }, - "description": "UUIDs of admin users" - }, - "org_email": { - "type": "string", - "format": "email", - "description": "Organization's email address" + "format": "email" }, "website": { "type": "string", - "format": "uri", - "description": "Organization's website URL" + "format": "uri" } }, - "required": ["poc", "poc_email", "admins", "org_email"] + "required": [ + "poc", + "poc_email", + "admins" + ] }, "advisory_locations": { "type": "array", - "items": { "type": "string" }, + "items": { + "type": "string" + }, "description": "Locations of vulnerability advisories" } }, @@ -110,4 +119,4 @@ "authority", "long_name" ] -} +} \ No newline at end of file diff --git a/schemas/registry-org/create-registry-org-response.json b/schemas/registry-org/create-registry-org-response.json index fb93b4e25..6fb87bcc7 100644 --- a/schemas/registry-org/create-registry-org-response.json +++ b/schemas/registry-org/create-registry-org-response.json @@ -5,7 +5,7 @@ "title": "CVE Create Registry Org Response", "description": "JSON Schema for CVE Create Registry Org response", "properties": { - "message": { + "message": { "type": "string", "description": "Success description" }, @@ -33,7 +33,11 @@ }, "cve_program_org_function": { "type": "string", - "enum": ["CNA", "ADP", "Secretariat"], + "enum": [ + "CNA", + "ADP", + "Secretariat" + ], "description": "The organization's function within the CVE program" }, "authority": { @@ -43,14 +47,23 @@ "type": "array", "items": { "type": "string", - "enum": ["CNA", "ADP", "Secretariat"] + "enum": [ + "CNA", + "ADP", + "Secretariat" + ] } } }, - "required": ["active_roles"] + "required": [ + "active_roles" + ] }, "reports_to": { - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "description": "UUID of the parent organization, if any" }, "oversees": { @@ -97,45 +110,48 @@ "additional_contact_users": { "type": "array", "items": { - "type": "string" + "type": "object", + "properties": { + "phone": { + "type": "string" + }, + "poc": { + "type": "string" + }, + "poc_email": { + "type": "string", + "format": "email" + } + }, + "additionalProperties": false } }, + "phone": { + "type": "string" + }, "poc": { - "type": "string", - "description": "Point of contact name" + "type": "string" }, "poc_email": { "type": "string", - "format": "email", - "description": "Point of contact email" - }, - "poc_phone": { - "type": "string", - "description": "Point of contact phone number" - }, - "admins": { - "type": "array", - "items": { - "type": "string" - }, - "description": "UUIDs of admin users" - }, - "org_email": { - "type": "string", - "format": "email", - "description": "Organization's email address" + "format": "email" }, "website": { "type": "string", - "format": "uri", - "description": "Organization's website URL" + "format": "uri" } }, - "required": ["poc", "poc_email", "admins", "org_email"] + "required": [ + "poc", + "poc_email", + "admins" + ] }, "advisory_locations": { "type": "array", - "items": { "type": "string" }, + "items": { + "type": "string" + }, "description": "Locations of vulnerability advisories" }, "in_use": { @@ -154,5 +170,5 @@ } } } - } + } } \ No newline at end of file diff --git a/schemas/registry-org/get-registry-org-response.json b/schemas/registry-org/get-registry-org-response.json index 0d66b39a3..e2a9eb467 100644 --- a/schemas/registry-org/get-registry-org-response.json +++ b/schemas/registry-org/get-registry-org-response.json @@ -42,7 +42,10 @@ "description": "Indicates if the organization is a root or top-level root" }, "reports_to": { - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "description": "UUID of the parent organization, if any" }, "oversees": { @@ -53,18 +56,18 @@ "description": "UUIDs of organizations overseen by this organization" }, "users": { - "type": "array", - "items": { - "type": "string" - }, - "description": "UUIDs of users associated with this organization" + "type": "array", + "items": { + "type": "string" + }, + "description": "UUIDs of users associated with this organization" }, "admins": { - "type": "array", - "items": { - "type": "string" - }, - "description": "UUIDs of admin users" + "type": "array", + "items": { + "type": "string" + }, + "description": "UUIDs of admin users" }, "contact_info": { "type": "object", @@ -72,102 +75,107 @@ "additional_contact_users": { "type": "array", "items": { - "type": "string" + "type": "object", + "properties": { + "phone": { + "type": "string" + }, + "poc": { + "type": "string" + }, + "poc_email": { + "type": "string", + "format": "email" + } + }, + "additionalProperties": false } }, + "phone": { + "type": "string" + }, "poc": { - "type": "string", - "description": "Point of contact name" + "type": "string" }, "poc_email": { "type": "string", - "format": "email", - "description": "Point of contact email" - }, - "poc_phone": { - "type": "string", - "description": "Point of contact phone number" - }, - "org_email": { - "type": "string", - "format": "email", - "description": "Organization's email address" + "format": "email" }, "website": { "type": "string", - "format": "uri", - "description": "Organization's website URL" + "format": "uri" } }, "required": [ "poc", - "poc_email", - "org_email" + "poc_email" ] }, "partner_role_type": { - "$ref": "/BaseOrg#/definitions/partnerRoleType" + "$ref": "/BaseOrg#/definitions/partnerRoleType" }, "partner_number": { - "type": "string", - "description": "Number of the partner" + "type": "string", + "description": "Number of the partner" }, "partner_country": { - "type": "string", - "description": "Country of the partner" + "type": "string", + "description": "Country of the partner" }, "partner_data": { - "type": "object", - "properties": { - "cve_website_update_date": { - "type": "string", - "format": "date-time" - }, - "cve_website_update_needed": { - "type": "boolean" - }, - "partner_active_date": { - "type": "string", - "format": "date-time" - }, - "partner_inactive_date": { - "type": "string", - "format": "date-time" - }, - "status": { + "type": "object", + "properties": { + "cve_website_update_date": { + "type": "string", + "format": "date-time" + }, + "cve_website_update_needed": { + "type": "boolean" + }, + "partner_active_date": { + "type": "string", + "format": "date-time" + }, + "partner_inactive_date": { + "type": "string", + "format": "date-time" + }, + "status": { + "type": "string" + }, + "advisory_location_require_credentials": { + "type": "boolean", + "description": "Indicates if advisory locations require credentials" + }, + "vulnerability_advisory_location_for_web_scraping": { + "type": "array", + "items": { "type": "string" }, - "advisory_location_require_credentials": { - "type": "boolean", - "description": "Indicates if advisory locations require credentials" - }, - "vulnerability_advisory_location_for_web_scraping": { - "type": "array", - "items": { "type": "string" }, - "description": "Advisory locations for web scraping" - } - }, - "description": "Additional partner metadata (restricted)" + "description": "Advisory locations for web scraping" + } + }, + "description": "Additional partner metadata (restricted)" }, "advisory_locations": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Locations of vulnerability advisories" + "type": "array", + "items": { + "type": "string" + }, + "description": "Locations of vulnerability advisories" }, "industry": { - "type": "string", - "description": "Industry sector of the organization" + "type": "string", + "description": "Industry sector of the organization" }, "tl_root_start_date": { - "type": "string", - "format": "date-time", - "description": "Start date for Top-Level Root role" + "type": "string", + "format": "date-time", + "description": "Start date for Top-Level Root role" }, "is_cna_discussion_list": { - "type": "boolean", - "description": "Indicates if part of the CNA discussion list" + "type": "boolean", + "description": "Indicates if part of the CNA discussion list" }, "in_use": { "type": "boolean", @@ -204,14 +212,16 @@ "description": "List of products associated with the organization" }, "conversation": { - "type": "array", - "items": { - "type": "object", - "properties": { - "body": { "type": "string" } - } - }, - "description": "List of conversation messages associated with the organization" + "type": "array", + "items": { + "type": "object", + "properties": { + "body": { + "type": "string" + } + } + }, + "description": "List of conversation messages associated with the organization" } } } \ No newline at end of file diff --git a/schemas/registry-org/list-registry-orgs-response.json b/schemas/registry-org/list-registry-orgs-response.json index 3ce381ff1..61c02ce4a 100644 --- a/schemas/registry-org/list-registry-orgs-response.json +++ b/schemas/registry-org/list-registry-orgs-response.json @@ -71,7 +71,10 @@ "description": "Indicates if the organization is a root or top-level root" }, "reports_to": { - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "description": "UUID of the parent organization, if any" }, "oversees": { @@ -101,37 +104,40 @@ "additional_contact_users": { "type": "array", "items": { - "type": "string" + "type": "object", + "properties": { + "phone": { + "type": "string" + }, + "poc": { + "type": "string" + }, + "poc_email": { + "type": "string", + "format": "email" + } + }, + "additionalProperties": false } }, + "phone": { + "type": "string" + }, "poc": { - "type": "string", - "description": "Point of contact name" + "type": "string" }, "poc_email": { "type": "string", - "format": "email", - "description": "Point of contact email" - }, - "poc_phone": { - "type": "string", - "description": "Point of contact phone number" - }, - "org_email": { - "type": "string", - "format": "email", - "description": "Organization's email address" + "format": "email" }, "website": { "type": "string", - "format": "uri", - "description": "Organization's website URL" + "format": "uri" } }, "required": [ "poc", - "poc_email", - "org_email" + "poc_email" ] }, "partner_role_type": { @@ -172,7 +178,9 @@ }, "vulnerability_advisory_location_for_web_scraping": { "type": "array", - "items": { "type": "string" }, + "items": { + "type": "string" + }, "description": "Advisory locations for web scraping" } }, @@ -233,14 +241,16 @@ "description": "List of products associated with the organization" }, "conversation": { - "type": "array", - "items": { - "type": "object", - "properties": { - "body": { "type": "string" } + "type": "array", + "items": { + "type": "object", + "properties": { + "body": { + "type": "string" } - }, - "description": "List of conversation messages associated with the organization" + } + }, + "description": "List of conversation messages associated with the organization" } } } diff --git a/schemas/registry-org/update-registry-org-request.json b/schemas/registry-org/update-registry-org-request.json index b4dca49ab..48746b8c6 100644 --- a/schemas/registry-org/update-registry-org-request.json +++ b/schemas/registry-org/update-registry-org-request.json @@ -22,7 +22,11 @@ }, "cve_program_org_function": { "type": "string", - "enum": ["CNA", "ADP", "Secretariat"], + "enum": [ + "CNA", + "ADP", + "Secretariat" + ], "description": "The organization's function within the CVE program" }, "authority": { @@ -32,11 +36,18 @@ "type": "array", "items": { "type": "string", - "enum": ["CNA", "ADP", "Secretariat", "ROOT"] + "enum": [ + "CNA", + "ADP", + "Secretariat", + "ROOT" + ] } } }, - "required": ["active_roles"] + "required": [ + "active_roles" + ] }, "oversees": { "type": "array", @@ -74,45 +85,44 @@ "additional_contact_users": { "type": "array", "items": { - "type": "string" + "type": "object", + "properties": { + "phone": { + "type": "string" + }, + "poc": { + "type": "string" + }, + "poc_email": { + "type": "string", + "format": "email" + } + }, + "additionalProperties": false } }, + "phone": { + "type": "string" + }, "poc": { - "type": "string", - "description": "Point of contact name" + "type": "string" }, "poc_email": { "type": "string", - "format": "email", - "description": "Point of contact email" - }, - "poc_phone": { - "type": "string", - "description": "Point of contact phone number" - }, - "admins": { - "type": "array", - "items": { - "type": "string" - }, - "description": "UUIDs of admin users" - }, - "org_email": { - "type": "string", - "format": "email", - "description": "Organization's email address" + "format": "email" }, "website": { "type": "string", - "format": "uri", - "description": "Organization's website URL" + "format": "uri" } } }, "advisory_locations": { "type": "array", - "items": { "type": "string" }, + "items": { + "type": "string" + }, "description": "Locations of vulnerability advisories" } } -} +} \ No newline at end of file diff --git a/schemas/registry-org/update-registry-org-response.json b/schemas/registry-org/update-registry-org-response.json index e2d2aee57..d73cca451 100644 --- a/schemas/registry-org/update-registry-org-response.json +++ b/schemas/registry-org/update-registry-org-response.json @@ -5,7 +5,7 @@ "title": "CVE Update Registry Org Response", "description": "JSON Schema for CVE Update Registry Org response", "properties": { - "message": { + "message": { "type": "string", "description": "Success description" }, @@ -38,11 +38,18 @@ "type": "array", "items": { "type": "string", - "enum": ["CNA", "ADP", "Root", "Secretariat"] + "enum": [ + "CNA", + "ADP", + "Root", + "Secretariat" + ] } } }, - "required": ["active_roles"] + "required": [ + "active_roles" + ] }, "reports_to": { "type": "string", @@ -92,45 +99,48 @@ "additional_contact_users": { "type": "array", "items": { - "type": "string" + "type": "object", + "properties": { + "phone": { + "type": "string" + }, + "poc": { + "type": "string" + }, + "poc_email": { + "type": "string", + "format": "email" + } + }, + "additionalProperties": false } }, + "phone": { + "type": "string" + }, "poc": { - "type": "string", - "description": "Point of contact name" + "type": "string" }, "poc_email": { "type": "string", - "format": "email", - "description": "Point of contact email" - }, - "poc_phone": { - "type": "string", - "description": "Point of contact phone number" - }, - "admins": { - "type": "array", - "items": { - "type": "string" - }, - "description": "UUIDs of admin users" - }, - "org_email": { - "type": "string", - "format": "email", - "description": "Organization's email address" + "format": "email" }, "website": { "type": "string", - "format": "uri", - "description": "Organization's website URL" + "format": "uri" } }, - "required": ["poc", "poc_email", "admins", "org_email"] + "required": [ + "poc", + "poc_email", + "admins" + ] }, "advisory_locations": { "type": "array", - "items": { "type": "string" }, + "items": { + "type": "string" + }, "description": "Locations of vulnerability advisories" }, "in_use": { @@ -149,5 +159,5 @@ } } } - } + } } \ No newline at end of file diff --git a/src/controller/org.controller/org.middleware.js b/src/controller/org.controller/org.middleware.js index 3ff24f84b..8f05be3bd 100644 --- a/src/controller/org.controller/org.middleware.js +++ b/src/controller/org.controller/org.middleware.js @@ -30,7 +30,7 @@ function validateCreateOrgParameters () { // Optional // soft_quota, // Not allowed - // users, contact_info.admins, in_use, created, last_updated + // users, , in_use, created, last_updated const orgOptions = ['CNA', 'Secretariat', 'Bulk Download', 'ADP'] validations = [ body(['short_name']).isString() @@ -80,9 +80,10 @@ function validateCreateOrgParameters () { 'product_list', 'contact_info.poc', 'contact_info.poc_email', - 'contact_info.poc_phone', - 'contact_info.org_email', + 'contact_info.phone', 'contact_info.website', + '', + '', 'partner_role_type', 'partner_number', 'partner_country', @@ -102,7 +103,7 @@ function validateCreateOrgParameters () { .isArray() .isInt({ min: CONSTANTS.MONGOOSE_VALIDATION.Org_policies_id_quota_min, max: CONSTANTS.MONGOOSE_VALIDATION.Org_policies_id_quota_max }) .withMessage(errorMsgs.ID_QUOTA), - ...isNotAllowed('reports_to', 'name', 'users', 'contact_info.admins', 'in_use', 'created', 'last_updated', 'policies.id_quota') + ...isNotAllowed('reports_to', 'name', 'users', '', 'in_use', 'created', 'last_updated', 'policies.id_quota') ] } else { validations = [ @@ -128,14 +129,14 @@ function validateCreateOrgParameters () { 'oversees', 'long_name', 'cve_program_org_function', - 'contact_info.admins', + '', 'in_use', 'created', 'top_level_root', 'soft_quota', 'aliases', 'hard_quota', - 'contact_info.org_email', + 'contact_info.phone', 'contact_info.website', 'contact_info', 'users', @@ -144,10 +145,11 @@ function validateCreateOrgParameters () { 'product_list', 'contact_info.poc', 'contact_info.poc_email', - 'contact_info.poc_phone', - 'contact_info.org_email', - 'contact_info.additional_contact_users', + 'contact_info.phone', 'contact_info.website', + '', + 'contact_info.additional_contact_users', + '', 'partner_role_type', 'partner_number', 'partner_country', @@ -234,9 +236,10 @@ function validateUpdateOrgParameters () { 'product_list', 'contact_info.poc', 'contact_info.poc_email', - 'contact_info.poc_phone', - 'contact_info.org_email', + 'contact_info.phone', 'contact_info.website', + '', + '', 'partner_role_type', 'partner_number', 'partner_country', @@ -327,9 +330,10 @@ const QUERY_PARAMETERS = { 'contact_info', 'contact_info.poc', 'contact_info.poc_email', - 'contact_info.poc_phone', - 'contact_info.org_email', + 'contact_info.phone', 'contact_info.website', + '', + '', 'partner_role_type', 'partner_number', 'partner_country', diff --git a/src/controller/registry-org.controller/registry-org.middleware.js b/src/controller/registry-org.controller/registry-org.middleware.js index 2adf36675..48a278e83 100644 --- a/src/controller/registry-org.controller/registry-org.middleware.js +++ b/src/controller/registry-org.controller/registry-org.middleware.js @@ -15,8 +15,7 @@ function parsePostParams (req, res, next) { 'top_level_root', 'users', 'charter_or_scope', 'disclosure_policy', 'product_list', 'soft_quota', 'hard_quota', - 'contact_info.additional_contact_users', 'contact_info.poc', 'contact_info.poc_email', 'contact_info.poc_phone', - 'contact_info.admins', 'contact_info.org_email', 'contact_info.website', + 'contact_info.additional_contact_users', 'contact_info.poc', 'contact_info.poc_email', 'contact_info.phone', 'contact_info.website', 'partner_role_type', 'partner_number', 'partner_country', diff --git a/src/model/baseorg.js b/src/model/baseorg.js index 4b0d057d5..9e6021b3d 100644 --- a/src/model/baseorg.js +++ b/src/model/baseorg.js @@ -15,11 +15,14 @@ const schema = { users: { type: [String], set: toUndefined }, admins: [String], contact_info: { - additional_contact_users: [String], + additional_contact_users: [{ + phone: String, + poc: String, + poc_email: String + }], + phone: String, poc: String, poc_email: String, - poc_phone: String, - org_email: String, website: String }, partner_role_type: String, diff --git a/test/integration-tests/constants.js b/test/integration-tests/constants.js index 80700e31e..0fc8f9d68 100644 --- a/test/integration-tests/constants.js +++ b/test/integration-tests/constants.js @@ -383,8 +383,7 @@ const testRegistryOrg = { contact_info: { poc: 'Dave', poc_email: 'dave@test.org', - poc_phone: '555-1234', - org_email: 'contact@test.org', + phone: '555-1234', website: 'https://test.org' }, authority: ['CNA'], @@ -397,8 +396,7 @@ const testRegistryOrg2 = { contact_info: { poc: 'Dave', poc_email: 'dave@test.org', - poc_phone: '555-1234', - org_email: 'contact@test.org', + phone: '555-1234', website: 'https://test.org' }, authority: ['CNA'], @@ -425,8 +423,7 @@ const existingRegistryOrg = { contact_info: { poc: 'Dave', poc_email: 'dave@test.org', - poc_phone: '555-1234', - org_email: 'contact@test.org', + phone: '555-1234', website: 'https://test.org' }, authority: ['CNA'], From f26577424e24713c0b903cc49be7c8226d0bd8c1 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Fri, 24 Apr 2026 14:45:04 -0400 Subject: [PATCH 24/86] remove some oddities --- src/controller/org.controller/org.middleware.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/controller/org.controller/org.middleware.js b/src/controller/org.controller/org.middleware.js index 8f05be3bd..7809afb84 100644 --- a/src/controller/org.controller/org.middleware.js +++ b/src/controller/org.controller/org.middleware.js @@ -129,7 +129,6 @@ function validateCreateOrgParameters () { 'oversees', 'long_name', 'cve_program_org_function', - '', 'in_use', 'created', 'top_level_root', @@ -147,9 +146,7 @@ function validateCreateOrgParameters () { 'contact_info.poc_email', 'contact_info.phone', 'contact_info.website', - '', 'contact_info.additional_contact_users', - '', 'partner_role_type', 'partner_number', 'partner_country', From 4cab75e6333baf123a037164c4c98bf70bcb846c Mon Sep 17 00:00:00 2001 From: david-rocca Date: Mon, 27 Apr 2026 11:17:03 -0400 Subject: [PATCH 25/86] Update partner_active_date and partner_inactive_date to be date not datetime, and rename partner_data to program_data --- schemas/registry-org/BaseOrg.json | 6 +-- schemas/registry-org/CNAOrg.json | 4 +- schemas/registry-org/RootOrg.json | 2 +- .../get-registry-org-response.json | 6 +-- .../list-registry-orgs-response.json | 6 +-- src/constants/index.js | 16 +++--- .../org.controller/org.middleware.js | 44 +++++++-------- .../registry-org.middleware.js | 10 ++-- src/model/baseorg.js | 6 +-- src/repositories/baseOrgRepository.js | 54 +++++++++---------- .../org/registryOrgAsOrgAdmin.js | 4 +- .../registry-org/registryOrgCRUDTest.js | 28 +++++----- .../registry-org/rootOrgTest.js | 2 +- 13 files changed, 94 insertions(+), 94 deletions(-) diff --git a/schemas/registry-org/BaseOrg.json b/schemas/registry-org/BaseOrg.json index 91969b4e6..d04301fb3 100644 --- a/schemas/registry-org/BaseOrg.json +++ b/schemas/registry-org/BaseOrg.json @@ -157,7 +157,7 @@ }, "additionalProperties": false }, - "partner_data": { + "program_data": { "type": "object", "properties": { "cve_website_update_date": { @@ -169,11 +169,11 @@ }, "partner_active_date": { "type": "string", - "format": "date-time" + "format": "date" }, "partner_inactive_date": { "type": "string", - "format": "date-time" + "format": "date" }, "status": { "type": "string" diff --git a/schemas/registry-org/CNAOrg.json b/schemas/registry-org/CNAOrg.json index 99a3d9e7d..982e62f38 100644 --- a/schemas/registry-org/CNAOrg.json +++ b/schemas/registry-org/CNAOrg.json @@ -116,8 +116,8 @@ "advisory_locations": { "$ref": "/BaseOrg#/properties/advisory_locations" }, - "partner_data": { - "$ref": "/BaseOrg#/properties/partner_data" + "program_data": { + "$ref": "/BaseOrg#/properties/program_data" } }, "required": [ diff --git a/schemas/registry-org/RootOrg.json b/schemas/registry-org/RootOrg.json index 41cd83827..664b432a6 100644 --- a/schemas/registry-org/RootOrg.json +++ b/schemas/registry-org/RootOrg.json @@ -52,7 +52,7 @@ "type": "string" }, "advisory_locations": { "$ref": "/BaseOrg#/properties/advisory_locations" }, - "partner_data": { "$ref": "/BaseOrg#/properties/partner_data" } + "program_data": { "$ref": "/BaseOrg#/properties/program_data" } }, "required": ["short_name"] } diff --git a/schemas/registry-org/get-registry-org-response.json b/schemas/registry-org/get-registry-org-response.json index e2a9eb467..31f6bdd47 100644 --- a/schemas/registry-org/get-registry-org-response.json +++ b/schemas/registry-org/get-registry-org-response.json @@ -122,7 +122,7 @@ "type": "string", "description": "Country of the partner" }, - "partner_data": { + "program_data": { "type": "object", "properties": { "cve_website_update_date": { @@ -134,11 +134,11 @@ }, "partner_active_date": { "type": "string", - "format": "date-time" + "format": "date" }, "partner_inactive_date": { "type": "string", - "format": "date-time" + "format": "date" }, "status": { "type": "string" diff --git a/schemas/registry-org/list-registry-orgs-response.json b/schemas/registry-org/list-registry-orgs-response.json index 61c02ce4a..75e372907 100644 --- a/schemas/registry-org/list-registry-orgs-response.json +++ b/schemas/registry-org/list-registry-orgs-response.json @@ -151,7 +151,7 @@ "type": "string", "description": "Country of the partner" }, - "partner_data": { + "program_data": { "type": "object", "properties": { "cve_website_update_date": { @@ -163,11 +163,11 @@ }, "partner_active_date": { "type": "string", - "format": "date-time" + "format": "date" }, "partner_inactive_date": { "type": "string", - "format": "date-time" + "format": "date" }, "status": { "type": "string" diff --git a/src/constants/index.js b/src/constants/index.js index 6b4494d28..efbe374aa 100644 --- a/src/constants/index.js +++ b/src/constants/index.js @@ -44,18 +44,18 @@ function getConstants () { USER_ROLES: [ 'ADMIN' ], - JOINT_APPROVAL_FIELDS: ['short_name', 'long_name', 'authority', 'aliases', 'oversees', 'top_level_root', 'charter_or_scope', 'product_list', 'disclosure_policy', 'contact_info.poc', 'contact_info.poc_email', 'contact_info.poc_phone', 'contact_info.org_email', 'partner_role_type', 'partner_number', 'partner_country', 'partner_data.cve_website_update_date', 'partner_data.cve_website_update_needed', 'partner_data.status', 'advisory_locations', 'industry', 'tl_root_start_date', 'is_cna_discussion_list', 'hard_quota'], + JOINT_APPROVAL_FIELDS: ['short_name', 'long_name', 'authority', 'aliases', 'oversees', 'top_level_root', 'charter_or_scope', 'product_list', 'disclosure_policy', 'contact_info.poc', 'contact_info.poc_email', 'contact_info.poc_phone', 'contact_info.org_email', 'partner_role_type', 'partner_number', 'partner_country', 'program_data.cve_website_update_date', 'program_data.cve_website_update_needed', 'program_data.status', 'advisory_locations', 'industry', 'tl_root_start_date', 'is_cna_discussion_list', 'hard_quota'], JOINT_APPROVAL_FIELDS_LEGACY: ['short_name', 'name', 'authority.active_roles', 'policies.id_quota'], ORG_EXCLUDED_FIELDS: ['__t', '__v', '_id', 'inUse', 'in_use'], - ORG_RESTRICTED_FIELDS: ['partner_data'], + ORG_RESTRICTED_FIELDS: ['program_data'], SECRETARIAT_ONLY_FIELDS: [ 'partner_number', - 'partner_data', - 'partner_data.cve_website_update_date', - 'partner_data.cve_website_update_needed', - 'partner_data.status', - 'partner_data.advisory_location_require_credentials', - 'partner_data.vulnerability_advisory_location_for_web_scraping', + 'program_data', + 'program_data.cve_website_update_date', + 'program_data.cve_website_update_needed', + 'program_data.status', + 'program_data.advisory_location_require_credentials', + 'program_data.vulnerability_advisory_location_for_web_scraping', 'top_level_root', 'oversees' ], diff --git a/src/controller/org.controller/org.middleware.js b/src/controller/org.controller/org.middleware.js index 7809afb84..1d29764b4 100644 --- a/src/controller/org.controller/org.middleware.js +++ b/src/controller/org.controller/org.middleware.js @@ -51,10 +51,10 @@ function validateCreateOrgParameters () { body(['advisory_locations']) .default([]) .custom(isFlatStringArray), - body(['partner_data.advisory_location_require_credentials']) + body(['program_data.advisory_location_require_credentials']) .default(false) .isBoolean(), - body(['partner_data.vulnerability_advisory_location_for_web_scraping']) + body(['program_data.vulnerability_advisory_location_for_web_scraping']) .default([]) .custom(isFlatStringArray), body(['tl_root_start_date']) @@ -64,13 +64,13 @@ function validateCreateOrgParameters () { .default(false) .isBoolean(), body([ - 'partner_data.cve_website_update_date', - 'partner_data.partner_active_date', - 'partner_data.partner_inactive_date' + 'program_data.cve_website_update_date', + 'program_data.partner_active_date', + 'program_data.partner_inactive_date' ]) .optional({ nullable: true }) .isDate(), - body(['partner_data.cve_website_update_needed']) + body(['program_data.cve_website_update_needed']) .optional() .isBoolean(), body( @@ -87,7 +87,7 @@ function validateCreateOrgParameters () { 'partner_role_type', 'partner_number', 'partner_country', - 'partner_data.status', + 'program_data.status', 'industry' ]) .default('') @@ -150,12 +150,12 @@ function validateCreateOrgParameters () { 'partner_role_type', 'partner_number', 'partner_country', - 'partner_data.cve_website_update_date', - 'partner_data.cve_website_update_needed', - 'partner_data.status', + 'program_data.cve_website_update_date', + 'program_data.cve_website_update_needed', + 'program_data.status', 'advisory_locations', - 'partner_data.advisory_location_require_credentials', - 'partner_data.vulnerability_advisory_location_for_web_scraping', + 'program_data.advisory_location_require_credentials', + 'program_data.vulnerability_advisory_location_for_web_scraping', 'industry', 'tl_root_start_date', 'is_cna_discussion_list') @@ -240,11 +240,11 @@ function validateUpdateOrgParameters () { 'partner_role_type', 'partner_number', 'partner_country', - 'partner_data.cve_website_update_date', - 'partner_data.cve_website_update_needed', - 'partner_data.status', - 'partner_data.advisory_location_require_credentials', - 'partner_data.vulnerability_advisory_location_for_web_scraping', + 'program_data.cve_website_update_date', + 'program_data.cve_website_update_needed', + 'program_data.status', + 'program_data.advisory_location_require_credentials', + 'program_data.vulnerability_advisory_location_for_web_scraping', 'advisory_locations', 'industry', 'tl_root_start_date', @@ -334,11 +334,11 @@ const QUERY_PARAMETERS = { 'partner_role_type', 'partner_number', 'partner_country', - 'partner_data.cve_website_update_date', - 'partner_data.cve_website_update_needed', - 'partner_data.status', - 'partner_data.advisory_location_require_credentials', - 'partner_data.vulnerability_advisory_location_for_web_scraping', + 'program_data.cve_website_update_date', + 'program_data.cve_website_update_needed', + 'program_data.status', + 'program_data.advisory_location_require_credentials', + 'program_data.vulnerability_advisory_location_for_web_scraping', 'advisory_locations', 'industry', 'tl_root_start_date', diff --git a/src/controller/registry-org.controller/registry-org.middleware.js b/src/controller/registry-org.controller/registry-org.middleware.js index 48a278e83..b7c40e1fa 100644 --- a/src/controller/registry-org.controller/registry-org.middleware.js +++ b/src/controller/registry-org.controller/registry-org.middleware.js @@ -19,12 +19,12 @@ function parsePostParams (req, res, next) { 'partner_role_type', 'partner_number', 'partner_country', - 'partner_data.cve_website_update_date', - 'partner_data.cve_website_update_needed', - 'partner_data.status', + 'program_data.cve_website_update_date', + 'program_data.cve_website_update_needed', + 'program_data.status', 'advisory_locations', - 'partner_data.advisory_location_require_credentials', - 'partner_data.vulnerability_advisory_location_for_web_scraping', + 'program_data.advisory_location_require_credentials', + 'program_data.vulnerability_advisory_location_for_web_scraping', 'industry', 'tl_root_start_date', 'is_cna_discussion_list' diff --git a/src/model/baseorg.js b/src/model/baseorg.js index 9e6021b3d..a92887902 100644 --- a/src/model/baseorg.js +++ b/src/model/baseorg.js @@ -28,11 +28,11 @@ const schema = { partner_role_type: String, partner_number: String, partner_country: String, - partner_data: { + program_data: { cve_website_update_date: Date, cve_website_update_needed: Boolean, - partner_active_date: Date, - partner_inactive_date: Date, + partner_active_date: String, + partner_inactive_date: String, status: String, advisory_location_require_credentials: Boolean, vulnerability_advisory_location_for_web_scraping: [String] diff --git a/src/repositories/baseOrgRepository.js b/src/repositories/baseOrgRepository.js index cd58833eb..bca9b0929 100644 --- a/src/repositories/baseOrgRepository.js +++ b/src/repositories/baseOrgRepository.js @@ -461,24 +461,24 @@ class BaseOrgRepository extends BaseRepository { // Add uuid to org object registryObjectRaw.UUID = sharedUUID - // Automate partner_data dates based on status - if (!registryObjectRaw.partner_data) { - registryObjectRaw.partner_data = {} + // Automate program_data dates based on status + if (!registryObjectRaw.program_data) { + registryObjectRaw.program_data = {} } // Default to 'inactive' if not provided - if (!registryObjectRaw.partner_data.status) { - registryObjectRaw.partner_data.status = 'inactive' + if (!registryObjectRaw.program_data.status) { + registryObjectRaw.program_data.status = 'inactive' } - if (registryObjectRaw.partner_data.status === 'active') { - registryObjectRaw.partner_data.partner_active_date = new Date() + if (registryObjectRaw.program_data.status === 'active') { + registryObjectRaw.program_data.partner_active_date = new Date().toISOString().split('T')[0] // ensure inactive is not set - delete registryObjectRaw.partner_data.partner_inactive_date - } else if (registryObjectRaw.partner_data.status === 'inactive') { - registryObjectRaw.partner_data.partner_inactive_date = new Date() + delete registryObjectRaw.program_data.partner_inactive_date + } else if (registryObjectRaw.program_data.status === 'inactive') { + registryObjectRaw.program_data.partner_inactive_date = new Date().toISOString().split('T')[0] // ensure active is not set - delete registryObjectRaw.partner_data.partner_active_date + delete registryObjectRaw.program_data.partner_active_date } // Figure out why this is not working.... // registryObjectRaw = _.omitBy(registryObjectRaw, value => _.isNil(value) || _.isEmpty(value)) @@ -626,8 +626,8 @@ class BaseOrgRepository extends BaseRepository { * @param {string} [incomingParameters.cna_role_type] - (Registry only) * @param {string} [incomingParameters.cna_country] - (Registry only) * @param {string[]} [incomingParameters.advisory_locations] - (Registry only) - * @param {boolean} [incomingParameters.partner_data.advisory_location_require_credentials] - (Registry only) - * @param {string[]} [incomingParameters.partner_data.vulnerability_advisory_location_for_web_scraping] - (Registry only) + * @param {boolean} [incomingParameters.program_data.advisory_location_require_credentials] - (Registry only) + * @param {string[]} [incomingParameters.program_data.vulnerability_advisory_location_for_web_scraping] - (Registry only) * @param {string} [incomingParameters.industry] - (Registry only) * @param {string} [incomingParameters.tl_root_start_date] - (Registry only) * @param {boolean} [incomingParameters.is_cna_discussion_list] - (Registry only) @@ -878,29 +878,29 @@ class BaseOrgRepository extends BaseRepository { delete incomingOrg.new_short_name // Keeping for existing logic } - // Automate partner_data dates based on status - if (registryObjectRaw.partner_data && registryObjectRaw.partner_data.status) { - const incomingStatus = registryObjectRaw.partner_data.status - const currentStatus = registryOrg.partner_data?.status || 'inactive' + // Automate program_data dates based on status + if (registryObjectRaw.program_data && registryObjectRaw.program_data.status) { + const incomingStatus = registryObjectRaw.program_data.status + const currentStatus = registryOrg.program_data?.status || 'inactive' if (incomingStatus !== currentStatus) { if (incomingStatus === 'active') { - registryObjectRaw.partner_data.partner_active_date = new Date() - if (registryObjectRaw.partner_data.partner_inactive_date !== undefined) { - delete registryObjectRaw.partner_data.partner_inactive_date + registryObjectRaw.program_data.partner_active_date = new Date().toISOString().split('T')[0] + if (registryObjectRaw.program_data.partner_inactive_date !== undefined) { + delete registryObjectRaw.program_data.partner_inactive_date } } else if (incomingStatus === 'inactive') { - registryObjectRaw.partner_data.partner_inactive_date = new Date() - if (registryObjectRaw.partner_data.partner_active_date !== undefined) { - delete registryObjectRaw.partner_data.partner_active_date + registryObjectRaw.program_data.partner_inactive_date = new Date().toISOString().split('T')[0] + if (registryObjectRaw.program_data.partner_active_date !== undefined) { + delete registryObjectRaw.program_data.partner_active_date } } } else { // Keep existing dates if status didn't change - if (registryOrg.partner_data?.partner_active_date) { - registryObjectRaw.partner_data.partner_active_date = registryOrg.partner_data.partner_active_date + if (registryOrg.program_data?.partner_active_date) { + registryObjectRaw.program_data.partner_active_date = registryOrg.program_data.partner_active_date } - if (registryOrg.partner_data?.partner_inactive_date) { - registryObjectRaw.partner_data.partner_inactive_date = registryOrg.partner_data.partner_inactive_date + if (registryOrg.program_data?.partner_inactive_date) { + registryObjectRaw.program_data.partner_inactive_date = registryOrg.program_data.partner_inactive_date } } } diff --git a/test/integration-tests/org/registryOrgAsOrgAdmin.js b/test/integration-tests/org/registryOrgAsOrgAdmin.js index 6ec234157..e6a4fb9cd 100644 --- a/test/integration-tests/org/registryOrgAsOrgAdmin.js +++ b/test/integration-tests/org/registryOrgAsOrgAdmin.js @@ -377,12 +377,12 @@ describe('Testing Registry Org as org admin', () => { expect(res.body.error).to.be.equal('SECRETARIAT_ONLY') }) }) - it('Registry: Services api does not allow org admins to update their own orgs partner_data', async () => { + it('Registry: Services api does not allow org admins to update their own orgs program_data', async () => { await chai.request(app) .put('/api/registry/org/beat_10') .set(adminHeaders) .send({ - partner_data: { + program_data: { status: 'active' } }) diff --git a/test/integration-tests/registry-org/registryOrgCRUDTest.js b/test/integration-tests/registry-org/registryOrgCRUDTest.js index 8870616b7..5acf4ee2a 100644 --- a/test/integration-tests/registry-org/registryOrgCRUDTest.js +++ b/test/integration-tests/registry-org/registryOrgCRUDTest.js @@ -63,10 +63,10 @@ describe('Testing /registryOrg endpoints', () => { expect(res.body.created).to.haveOwnProperty('advisory_locations') expect(res.body.created.advisory_locations).to.deep.equal(testRegistryOrg.advisory_locations) - expect(res.body.created).to.haveOwnProperty('partner_data') - expect(res.body.created.partner_data.status).to.equal('inactive') - expect(res.body.created.partner_data).to.haveOwnProperty('partner_inactive_date') - expect(res.body.created.partner_data).to.not.haveOwnProperty('partner_active_date') + expect(res.body.created).to.haveOwnProperty('program_data') + expect(res.body.created.program_data.status).to.equal('inactive') + expect(res.body.created.program_data).to.haveOwnProperty('partner_inactive_date') + expect(res.body.created.program_data).to.not.haveOwnProperty('partner_active_date') createdOrg = res.body.created delete createdOrg.created @@ -295,13 +295,13 @@ describe('Testing /registryOrg endpoints', () => { expect(res.body.updated.advisory_locations).to.deep.equal(['https://example.com/updated_advisories']) }) }) - it('Allows Secretariat to update partner_data', async () => { + it('Allows Secretariat to update program_data', async () => { await chai.request(app) .put('/api/registry/org/registry_org_test') .set(secretariatHeaders) .send({ ...createdOrg, - partner_data: { + program_data: { status: 'active', advisory_location_require_credentials: true, vulnerability_advisory_location_for_web_scraping: ['https://example.com/scraping'] @@ -310,12 +310,12 @@ describe('Testing /registryOrg endpoints', () => { .then((res, err) => { expect(err).to.be.undefined expect(res).to.have.status(200) - expect(res.body.updated).to.haveOwnProperty('partner_data') - expect(res.body.updated.partner_data.status).to.equal('active') - expect(res.body.updated.partner_data).to.haveOwnProperty('partner_active_date') - expect(res.body.updated.partner_data).to.not.haveOwnProperty('partner_inactive_date') - expect(res.body.updated.partner_data.advisory_location_require_credentials).to.be.true - expect(res.body.updated.partner_data.vulnerability_advisory_location_for_web_scraping).to.deep.equal(['https://example.com/scraping']) + expect(res.body.updated).to.haveOwnProperty('program_data') + expect(res.body.updated.program_data.status).to.equal('active') + expect(res.body.updated.program_data).to.haveOwnProperty('partner_active_date') + expect(res.body.updated.program_data).to.not.haveOwnProperty('partner_inactive_date') + expect(res.body.updated.program_data.advisory_location_require_credentials).to.be.true + expect(res.body.updated.program_data.vulnerability_advisory_location_for_web_scraping).to.deep.equal(['https://example.com/scraping']) }) }) it('Updates a registry organization\'s short name and role simultaneously to verify read-after-write audit logic', async () => { @@ -503,13 +503,13 @@ describe('Testing /registryOrg endpoints', () => { long_name: win5Org.long_name, authority: win5Org.authority, hard_quota: win5Org.hard_quota, - partner_data: { + program_data: { status: 'active' } }) .then((res) => { expect(res).to.have.status(403) - expect(res.body.message).to.equal('The following fields can only be modified by the Secretariat: partner_data, partner_data.status.') + expect(res.body.message).to.equal('The following fields can only be modified by the Secretariat: program_data, program_data.status.') }) }) it('Fails to update a registry organization providing an erroneous key not found in the schema', async () => { diff --git a/test/integration-tests/registry-org/rootOrgTest.js b/test/integration-tests/registry-org/rootOrgTest.js index d1b03836c..0ea61393a 100644 --- a/test/integration-tests/registry-org/rootOrgTest.js +++ b/test/integration-tests/registry-org/rootOrgTest.js @@ -32,7 +32,7 @@ describe('Testing ROOT Organization Type', () => { createdOrg = res.body.created delete createdOrg.created delete createdOrg.last_updated - delete createdOrg.partner_data + delete createdOrg.program_data }) }) From 237d7582a0f9dc99c26221f262f2ad309031fd2d Mon Sep 17 00:00:00 2001 From: david-rocca Date: Mon, 27 Apr 2026 14:44:41 -0400 Subject: [PATCH 26/86] Fixing various bugs --- src/constants/index.js | 2 +- src/repositories/baseOrgRepository.js | 9 +++- src/repositories/reviewObjectRepository.js | 17 +++++++ test/integration-tests/audit/auditTest.js | 1 + .../org/registryOrgAsOrgAdmin.js | 44 +++++++++++++++++++ .../review-object/reviewObjectTest.js | 25 +++++++++++ 6 files changed, 95 insertions(+), 3 deletions(-) diff --git a/src/constants/index.js b/src/constants/index.js index efbe374aa..4c0a38b7a 100644 --- a/src/constants/index.js +++ b/src/constants/index.js @@ -44,7 +44,7 @@ function getConstants () { USER_ROLES: [ 'ADMIN' ], - JOINT_APPROVAL_FIELDS: ['short_name', 'long_name', 'authority', 'aliases', 'oversees', 'top_level_root', 'charter_or_scope', 'product_list', 'disclosure_policy', 'contact_info.poc', 'contact_info.poc_email', 'contact_info.poc_phone', 'contact_info.org_email', 'partner_role_type', 'partner_number', 'partner_country', 'program_data.cve_website_update_date', 'program_data.cve_website_update_needed', 'program_data.status', 'advisory_locations', 'industry', 'tl_root_start_date', 'is_cna_discussion_list', 'hard_quota'], + JOINT_APPROVAL_FIELDS: ['short_name', 'long_name', 'authority', 'aliases', 'oversees', 'top_level_root', 'charter_or_scope', 'product_list', 'disclosure_policy', 'partner_role_type', 'partner_number', 'program_data.cve_website_update_date', 'program_data.cve_website_update_needed', 'program_data.status', 'advisory_locations', 'tl_root_start_date', 'is_cna_discussion_list', 'hard_quota'], JOINT_APPROVAL_FIELDS_LEGACY: ['short_name', 'name', 'authority.active_roles', 'policies.id_quota'], ORG_EXCLUDED_FIELDS: ['__t', '__v', '_id', 'inUse', 'in_use'], ORG_RESTRICTED_FIELDS: ['program_data'], diff --git a/src/repositories/baseOrgRepository.js b/src/repositories/baseOrgRepository.js index bca9b0929..da27ff3d6 100644 --- a/src/repositories/baseOrgRepository.js +++ b/src/repositories/baseOrgRepository.js @@ -920,9 +920,14 @@ class BaseOrgRepository extends BaseRepository { const requestingUsername = requestingUser ? requestingUser.username : null const protectedFields = ['_id', 'UUID', '__v', '__t', 'created', 'last_updated', 'createdAt', 'updatedAt', 'users', 'admins'] + let registryProtectedFields = [...protectedFields] + if (!isSecretariat) { + registryProtectedFields = [...registryProtectedFields, ...getConstants().ORG_RESTRICTED_FIELDS] + } + if (isSecretariat || _.isEmpty(jointApprovalFieldsRegistry)) { updatedLegacyOrg = legacyOrg.overwrite(_.mergeWith(_.pick(legacyOrg.toObject(), protectedFields), _.omit(legacyObjectRaw, protectedFields), skipNulls)) - updatedRegistryOrg = registryOrg.overwrite(_.mergeWith(_.pick(registryOrg.toObject(), protectedFields), _.omit(registryObjectRaw, protectedFields), skipNulls)) + updatedRegistryOrg = registryOrg.overwrite(_.mergeWith(_.pick(registryOrg.toObject(), registryProtectedFields), _.omit(registryObjectRaw, registryProtectedFields), skipNulls)) } else { // Check if there are actual changes to joint approval fields compared to current org object (not current review) // Only compare fields that are actually in the incoming data @@ -945,7 +950,7 @@ class BaseOrgRepository extends BaseRepository { await reviewObjectRepo.rejectReviewOrgObject(reviewObject.uuid, requestingUsername, options) } } - updatedRegistryOrg = registryOrg.overwrite(_.mergeWith(_.pick(registryOrg.toObject(), [...protectedFields, ...jointApprovalFieldsRegistry]), _.omit(registryObjectRaw, [...protectedFields, ...jointApprovalFieldsRegistry]), skipNulls)) + updatedRegistryOrg = registryOrg.overwrite(_.mergeWith(_.pick(registryOrg.toObject(), [...registryProtectedFields, ...jointApprovalFieldsRegistry]), _.omit(registryObjectRaw, [...registryProtectedFields, ...jointApprovalFieldsRegistry]), skipNulls)) updatedLegacyOrg = legacyOrg.overwrite(_.mergeWith(_.pick(legacyOrg.toObject(), [...protectedFields, ...jointApprovalFieldsLegacy]), _.omit(legacyObjectRaw, [...protectedFields, ...jointApprovalFieldsLegacy]), skipNulls)) } // handle conversation diff --git a/src/repositories/reviewObjectRepository.js b/src/repositories/reviewObjectRepository.js index 0062da212..61400d343 100644 --- a/src/repositories/reviewObjectRepository.js +++ b/src/repositories/reviewObjectRepository.js @@ -34,6 +34,9 @@ class ReviewObjectRepository extends BaseRepository { reviewObject = reviewObjectRaw.toObject() const conversations = await conversationRepository.getAllByTargetUUID(reviewObject.target_object_uuid, isSecretariat, options) reviewObject.conversation = conversations?.length ? conversations : undefined + if (!isSecretariat && reviewObject.new_review_data && reviewObject.new_review_data.program_data) { + delete reviewObject.new_review_data.program_data + } } return reviewObject || null @@ -103,6 +106,9 @@ class ReviewObjectRepository extends BaseRepository { reviewObject = reviewObjectRaw.toObject() const conversations = await conversationRepository.getAllByTargetUUID(org.UUID, isSecretariat, options) reviewObject.conversation = conversations?.length ? conversations : undefined + if (!isSecretariat && reviewObject.new_review_data && reviewObject.new_review_data.program_data) { + delete reviewObject.new_review_data.program_data + } } return reviewObject || null @@ -132,6 +138,9 @@ class ReviewObjectRepository extends BaseRepository { reviewObject = reviewObjectRaw.toObject() const conversations = await conversationRepository.getAllByTargetUUID(org.UUID, isSecretariat, options) reviewObject.conversation = conversations?.length ? conversations : undefined + if (!isSecretariat && reviewObject.new_review_data && reviewObject.new_review_data.program_data) { + delete reviewObject.new_review_data.program_data + } } return reviewObject || null @@ -212,6 +221,14 @@ class ReviewObjectRepository extends BaseRepository { data.nextPage = pg.nextPage } + if (!isSecretariat && data.reviewObjects) { + for (const review of data.reviewObjects) { + if (review.new_review_data && review.new_review_data.program_data) { + delete review.new_review_data.program_data + } + } + } + // Optionally attach conversations if (includeConversations && pg.itemsList && pg.itemsList.length) { const ConversationRepository = require('./conversationRepository') diff --git a/test/integration-tests/audit/auditTest.js b/test/integration-tests/audit/auditTest.js index 7b8d7daf7..5b23e2a9a 100644 --- a/test/integration-tests/audit/auditTest.js +++ b/test/integration-tests/audit/auditTest.js @@ -247,6 +247,7 @@ describe('Testing Audit Org endpoints', () => { .get(`/api/audit/org/document/${fakeUUID}`) .set(constants.headers) .then((res, err) => { + if (res.status === 403) console.log(JSON.stringify(res.body, null, 2)) expect(err).to.be.undefined expect(res).to.have.status(404) diff --git a/test/integration-tests/org/registryOrgAsOrgAdmin.js b/test/integration-tests/org/registryOrgAsOrgAdmin.js index e6a4fb9cd..dabe763fd 100644 --- a/test/integration-tests/org/registryOrgAsOrgAdmin.js +++ b/test/integration-tests/org/registryOrgAsOrgAdmin.js @@ -234,6 +234,50 @@ describe('Testing Registry Org as org admin', () => { expect(res.body.hard_quota).to.be.greaterThan(0) }) }) + it('Registry: allows admin users to update their own org without losing program_data', async () => { + // Secretariat sets program_data + const secretariatHeaders = { ...constants.headers } + await chai.request(app) + .put(`/api/registry/org/${shortName}`) + .set(secretariatHeaders) + .send({ + short_name: shortName, + long_name: 'Test Org', + authority: ['CNA'], + hard_quota: 1000, + program_data: { + status: 'active' + } + }) + .then((res, err) => { + if (res.status === 400) console.log(JSON.stringify(res.body, null, 2)) + expect(err).to.be.undefined + expect(res).to.have.status(200) + }) + + // Admin updates org + await chai.request(app) + .put(`/api/registry/org/${shortName}`) + .set(adminHeaders) + .send({ + short_name: shortName, + long_name: 'Test Org2', + authority: ['CNA'], + hard_quota: 1000 + }) + .then((res, err) => { + expect(err).to.be.undefined + expect(res).to.have.status(200) + }) + + // Secretariat retrieves org and verifies program_data is preserved + const res = await chai.request(app) + .get(`/api/registry/org/${shortName}`) + .set(secretariatHeaders) + + expect(res.body).to.have.property('program_data') + expect(res.body.program_data).to.have.property('status', 'active') + }) }) context('Negative Tests', () => { it('Registry: reset secret for fails user in other org', async () => { diff --git a/test/integration-tests/review-object/reviewObjectTest.js b/test/integration-tests/review-object/reviewObjectTest.js index 9f8be169c..6fa5fa212 100644 --- a/test/integration-tests/review-object/reviewObjectTest.js +++ b/test/integration-tests/review-object/reviewObjectTest.js @@ -281,6 +281,31 @@ describe('Review Object Controller Integration Tests', () => { expect(res).to.have.status(200) expect(res.body).to.have.property('reviewObjects') expect(res.body.reviewObjects).to.be.an('array') + if (res.body.reviewObjects.length > 0) { + expect(res.body.reviewObjects[0].new_review_data).to.not.have.property('program_data') + } + }) + + it('Secretariat can see program_data in review history', async () => { + const res = await chai + .request(app) + .get(`/api/review/org/${constants.existingOrg.short_name}/reviews`) + .set({ ...constants.headers }) + expect(res).to.have.status(200) + expect(res.body).to.have.property('reviewObjects') + expect(res.body.reviewObjects).to.be.an('array') + if (res.body.reviewObjects.length > 0) { + expect(res.body.reviewObjects[0].new_review_data).to.have.property('program_data') + } + }) + + it('Admin cannot see program_data when getting review object by UUID', async () => { + const res = await chai + .request(app) + .get(`/api/review/byUUID/${approveTestReviewUUID}`) + .set({ ...constants.nonSecretariatUserHeaders2 }) + expect(res).to.have.status(200) + expect(res.body.new_review_data).to.not.have.property('program_data') }) // ------------------------------------------------------------------------------------------------ From 41a3f9bb6b4a28f5ae25485252c55fdf5238f5dc Mon Sep 17 00:00:00 2001 From: david-rocca Date: Tue, 28 Apr 2026 12:48:49 -0400 Subject: [PATCH 27/86] Fixing some CNAOrg missing fields --- schemas/registry-org/BaseOrg.json | 3 +++ schemas/registry-org/CNAOrg.json | 6 ++++++ schemas/registry-org/RootOrg.json | 4 +++- schemas/registry-org/create-registry-org-request.json | 4 ++++ schemas/registry-org/create-registry-org-response.json | 4 ++++ schemas/registry-org/update-registry-org-request.json | 4 ++++ schemas/registry-org/update-registry-org-response.json | 4 ++++ 7 files changed, 28 insertions(+), 1 deletion(-) diff --git a/schemas/registry-org/BaseOrg.json b/schemas/registry-org/BaseOrg.json index d04301fb3..22238a28d 100644 --- a/schemas/registry-org/BaseOrg.json +++ b/schemas/registry-org/BaseOrg.json @@ -195,6 +195,9 @@ "items": { "type": "string" } + }, + "industry": { + "type": "string" } }, "required": [ diff --git a/schemas/registry-org/CNAOrg.json b/schemas/registry-org/CNAOrg.json index 982e62f38..dbb8875d9 100644 --- a/schemas/registry-org/CNAOrg.json +++ b/schemas/registry-org/CNAOrg.json @@ -118,6 +118,12 @@ }, "program_data": { "$ref": "/BaseOrg#/properties/program_data" + }, + "industry": { + "$ref": "/BaseOrg#/properties/industry" + }, + "top_level_root": { + "$ref": "/BaseOrg#/properties/top_level_root" } }, "required": [ diff --git a/schemas/registry-org/RootOrg.json b/schemas/registry-org/RootOrg.json index 664b432a6..d519b2181 100644 --- a/schemas/registry-org/RootOrg.json +++ b/schemas/registry-org/RootOrg.json @@ -52,7 +52,9 @@ "type": "string" }, "advisory_locations": { "$ref": "/BaseOrg#/properties/advisory_locations" }, - "program_data": { "$ref": "/BaseOrg#/properties/program_data" } + "program_data": { "$ref": "/BaseOrg#/properties/program_data" }, + "industry": { "$ref": "/BaseOrg#/properties/industry" }, + "top_level_root": { "$ref": "/BaseOrg#/properties/top_level_root" } }, "required": ["short_name"] } diff --git a/schemas/registry-org/create-registry-org-request.json b/schemas/registry-org/create-registry-org-request.json index 1a3fe8d76..476aa5175 100644 --- a/schemas/registry-org/create-registry-org-request.json +++ b/schemas/registry-org/create-registry-org-request.json @@ -112,6 +112,10 @@ "type": "string" }, "description": "Locations of vulnerability advisories" + }, + "industry": { + "type": "string", + "description": "Industry sector of the organization" } }, "required": [ diff --git a/schemas/registry-org/create-registry-org-response.json b/schemas/registry-org/create-registry-org-response.json index 6fb87bcc7..b57bea9a4 100644 --- a/schemas/registry-org/create-registry-org-response.json +++ b/schemas/registry-org/create-registry-org-response.json @@ -154,6 +154,10 @@ }, "description": "Locations of vulnerability advisories" }, + "industry": { + "type": "string", + "description": "Industry sector of the organization" + }, "in_use": { "type": "boolean", "description": "Indicates if the organization is currently active" diff --git a/schemas/registry-org/update-registry-org-request.json b/schemas/registry-org/update-registry-org-request.json index 48746b8c6..e214f7cb4 100644 --- a/schemas/registry-org/update-registry-org-request.json +++ b/schemas/registry-org/update-registry-org-request.json @@ -123,6 +123,10 @@ "type": "string" }, "description": "Locations of vulnerability advisories" + }, + "industry": { + "type": "string", + "description": "Industry sector of the organization" } } } \ No newline at end of file diff --git a/schemas/registry-org/update-registry-org-response.json b/schemas/registry-org/update-registry-org-response.json index d73cca451..3dd15eb41 100644 --- a/schemas/registry-org/update-registry-org-response.json +++ b/schemas/registry-org/update-registry-org-response.json @@ -143,6 +143,10 @@ }, "description": "Locations of vulnerability advisories" }, + "industry": { + "type": "string", + "description": "Industry sector of the organization" + }, "in_use": { "type": "boolean", "description": "Indicates if the organization is currently active" From 81d6909af4cd5c3fcda5de4a0e5a556d313954f3 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Tue, 28 Apr 2026 13:04:46 -0400 Subject: [PATCH 28/86] more schema updateS --- schemas/registry-org/BaseOrg.json | 16 +++++ schemas/registry-org/CNAOrg.json | 6 ++ schemas/registry-org/RootOrg.json | 4 +- .../create-registry-org-request.json | 67 +++++++++++++++++++ .../create-registry-org-response.json | 9 +++ .../update-registry-org-request.json | 67 +++++++++++++++++++ .../update-registry-org-response.json | 9 +++ 7 files changed, 177 insertions(+), 1 deletion(-) diff --git a/schemas/registry-org/BaseOrg.json b/schemas/registry-org/BaseOrg.json index 22238a28d..29cf5e1ef 100644 --- a/schemas/registry-org/BaseOrg.json +++ b/schemas/registry-org/BaseOrg.json @@ -198,6 +198,22 @@ }, "industry": { "type": "string" + }, + "tl_root_start_date": { + "type": "string", + "format": "date-time" + }, + "is_cna_discussion_list": { + "type": "boolean" + }, + "partner_role_type": { + "$ref": "#/definitions/partnerRoleType" + }, + "partner_number": { + "type": "string" + }, + "partner_country": { + "type": "string" } }, "required": [ diff --git a/schemas/registry-org/CNAOrg.json b/schemas/registry-org/CNAOrg.json index dbb8875d9..ff8881f4c 100644 --- a/schemas/registry-org/CNAOrg.json +++ b/schemas/registry-org/CNAOrg.json @@ -124,6 +124,12 @@ }, "top_level_root": { "$ref": "/BaseOrg#/properties/top_level_root" + }, + "tl_root_start_date": { + "$ref": "/BaseOrg#/properties/tl_root_start_date" + }, + "is_cna_discussion_list": { + "$ref": "/BaseOrg#/properties/is_cna_discussion_list" } }, "required": [ diff --git a/schemas/registry-org/RootOrg.json b/schemas/registry-org/RootOrg.json index d519b2181..9015ebdf5 100644 --- a/schemas/registry-org/RootOrg.json +++ b/schemas/registry-org/RootOrg.json @@ -54,7 +54,9 @@ "advisory_locations": { "$ref": "/BaseOrg#/properties/advisory_locations" }, "program_data": { "$ref": "/BaseOrg#/properties/program_data" }, "industry": { "$ref": "/BaseOrg#/properties/industry" }, - "top_level_root": { "$ref": "/BaseOrg#/properties/top_level_root" } + "top_level_root": { "$ref": "/BaseOrg#/properties/top_level_root" }, + "tl_root_start_date": { "$ref": "/BaseOrg#/properties/tl_root_start_date" }, + "is_cna_discussion_list": { "$ref": "/BaseOrg#/properties/is_cna_discussion_list" } }, "required": ["short_name"] } diff --git a/schemas/registry-org/create-registry-org-request.json b/schemas/registry-org/create-registry-org-request.json index 476aa5175..ee65de54c 100644 --- a/schemas/registry-org/create-registry-org-request.json +++ b/schemas/registry-org/create-registry-org-request.json @@ -116,6 +116,73 @@ "industry": { "type": "string", "description": "Industry sector of the organization" + }, + "tl_root_start_date": { + "type": "string", + "format": "date-time", + "description": "Start date for Top-Level Root role" + }, + "is_cna_discussion_list": { + "type": "boolean", + "description": "Indicates if part of the CNA discussion list" + }, + "partner_role_type": { + "type": "string", + "enum": [ + "", + "Bug Bounty Provider", + "CERT", + "Consortium", + "Hosted Service", + "N/A", + "Open Source", + "Researcher", + "Vendor" + ], + "description": "The type of role a partner holds" + }, + "partner_number": { + "type": "string", + "description": "Number of the partner" + }, + "partner_country": { + "type": "string", + "description": "Country of the partner" + }, + "program_data": { + "type": "object", + "properties": { + "cve_website_update_date": { + "type": "string", + "format": "date-time" + }, + "cve_website_update_needed": { + "type": "boolean" + }, + "partner_active_date": { + "type": "string", + "format": "date" + }, + "partner_inactive_date": { + "type": "string", + "format": "date" + }, + "status": { + "type": "string" + }, + "advisory_location_require_credentials": { + "type": "boolean", + "description": "Indicates if advisory locations require credentials" + }, + "vulnerability_advisory_location_for_web_scraping": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Advisory locations for web scraping" + } + }, + "description": "Additional partner metadata (restricted)" } }, "required": [ diff --git a/schemas/registry-org/create-registry-org-response.json b/schemas/registry-org/create-registry-org-response.json index b57bea9a4..4bb60d9b0 100644 --- a/schemas/registry-org/create-registry-org-response.json +++ b/schemas/registry-org/create-registry-org-response.json @@ -158,6 +158,15 @@ "type": "string", "description": "Industry sector of the organization" }, + "tl_root_start_date": { + "type": "string", + "format": "date-time", + "description": "Start date for Top-Level Root role" + }, + "is_cna_discussion_list": { + "type": "boolean", + "description": "Indicates if part of the CNA discussion list" + }, "in_use": { "type": "boolean", "description": "Indicates if the organization is currently active" diff --git a/schemas/registry-org/update-registry-org-request.json b/schemas/registry-org/update-registry-org-request.json index e214f7cb4..14fe4c3a8 100644 --- a/schemas/registry-org/update-registry-org-request.json +++ b/schemas/registry-org/update-registry-org-request.json @@ -127,6 +127,73 @@ "industry": { "type": "string", "description": "Industry sector of the organization" + }, + "tl_root_start_date": { + "type": "string", + "format": "date-time", + "description": "Start date for Top-Level Root role" + }, + "is_cna_discussion_list": { + "type": "boolean", + "description": "Indicates if part of the CNA discussion list" + }, + "partner_role_type": { + "type": "string", + "enum": [ + "", + "Bug Bounty Provider", + "CERT", + "Consortium", + "Hosted Service", + "N/A", + "Open Source", + "Researcher", + "Vendor" + ], + "description": "The type of role a partner holds" + }, + "partner_number": { + "type": "string", + "description": "Number of the partner" + }, + "partner_country": { + "type": "string", + "description": "Country of the partner" + }, + "program_data": { + "type": "object", + "properties": { + "cve_website_update_date": { + "type": "string", + "format": "date-time" + }, + "cve_website_update_needed": { + "type": "boolean" + }, + "partner_active_date": { + "type": "string", + "format": "date" + }, + "partner_inactive_date": { + "type": "string", + "format": "date" + }, + "status": { + "type": "string" + }, + "advisory_location_require_credentials": { + "type": "boolean", + "description": "Indicates if advisory locations require credentials" + }, + "vulnerability_advisory_location_for_web_scraping": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Advisory locations for web scraping" + } + }, + "description": "Additional partner metadata (restricted)" } } } \ No newline at end of file diff --git a/schemas/registry-org/update-registry-org-response.json b/schemas/registry-org/update-registry-org-response.json index 3dd15eb41..6a2b86adc 100644 --- a/schemas/registry-org/update-registry-org-response.json +++ b/schemas/registry-org/update-registry-org-response.json @@ -147,6 +147,15 @@ "type": "string", "description": "Industry sector of the organization" }, + "tl_root_start_date": { + "type": "string", + "format": "date-time", + "description": "Start date for Top-Level Root role" + }, + "is_cna_discussion_list": { + "type": "boolean", + "description": "Indicates if part of the CNA discussion list" + }, "in_use": { "type": "boolean", "description": "Indicates if the organization is currently active" From 573f7b94e28a4252fe0bccaa6d1f27cbb2286072 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Tue, 28 Apr 2026 13:38:26 -0400 Subject: [PATCH 29/86] renamed to additional contacts --- schemas/registry-org/BaseOrg.json | 2 +- schemas/registry-org/CNAOrg.json | 2 +- schemas/registry-org/create-registry-org-request.json | 2 +- schemas/registry-org/create-registry-org-response.json | 2 +- schemas/registry-org/get-registry-org-response.json | 2 +- schemas/registry-org/list-registry-orgs-response.json | 2 +- schemas/registry-org/update-registry-org-request.json | 2 +- schemas/registry-org/update-registry-org-response.json | 2 +- src/controller/org.controller/org.middleware.js | 2 +- .../registry-org.controller/registry-org.middleware.js | 2 +- src/model/baseorg.js | 2 +- src/scripts/migrate.js | 4 ++-- 12 files changed, 13 insertions(+), 13 deletions(-) diff --git a/schemas/registry-org/BaseOrg.json b/schemas/registry-org/BaseOrg.json index 29cf5e1ef..6c91ce741 100644 --- a/schemas/registry-org/BaseOrg.json +++ b/schemas/registry-org/BaseOrg.json @@ -121,7 +121,7 @@ "contact_info": { "type": "object", "properties": { - "additional_contact_users": { + "additional_contacts": { "type": "array", "items": { "type": "object", diff --git a/schemas/registry-org/CNAOrg.json b/schemas/registry-org/CNAOrg.json index ff8881f4c..7c80f0b95 100644 --- a/schemas/registry-org/CNAOrg.json +++ b/schemas/registry-org/CNAOrg.json @@ -27,7 +27,7 @@ "contact_info": { "type": "object", "properties": { - "additional_contact_users": { + "additional_contacts": { "type": "array", "items": { "type": "object", diff --git a/schemas/registry-org/create-registry-org-request.json b/schemas/registry-org/create-registry-org-request.json index ee65de54c..6b556101d 100644 --- a/schemas/registry-org/create-registry-org-request.json +++ b/schemas/registry-org/create-registry-org-request.json @@ -66,7 +66,7 @@ "contact_info": { "type": "object", "properties": { - "additional_contact_users": { + "additional_contacts": { "type": "array", "items": { "type": "object", diff --git a/schemas/registry-org/create-registry-org-response.json b/schemas/registry-org/create-registry-org-response.json index 4bb60d9b0..899cdf682 100644 --- a/schemas/registry-org/create-registry-org-response.json +++ b/schemas/registry-org/create-registry-org-response.json @@ -107,7 +107,7 @@ "contact_info": { "type": "object", "properties": { - "additional_contact_users": { + "additional_contacts": { "type": "array", "items": { "type": "object", diff --git a/schemas/registry-org/get-registry-org-response.json b/schemas/registry-org/get-registry-org-response.json index 31f6bdd47..5c647ecc0 100644 --- a/schemas/registry-org/get-registry-org-response.json +++ b/schemas/registry-org/get-registry-org-response.json @@ -72,7 +72,7 @@ "contact_info": { "type": "object", "properties": { - "additional_contact_users": { + "additional_contacts": { "type": "array", "items": { "type": "object", diff --git a/schemas/registry-org/list-registry-orgs-response.json b/schemas/registry-org/list-registry-orgs-response.json index 75e372907..0863811d0 100644 --- a/schemas/registry-org/list-registry-orgs-response.json +++ b/schemas/registry-org/list-registry-orgs-response.json @@ -101,7 +101,7 @@ "contact_info": { "type": "object", "properties": { - "additional_contact_users": { + "additional_contacts": { "type": "array", "items": { "type": "object", diff --git a/schemas/registry-org/update-registry-org-request.json b/schemas/registry-org/update-registry-org-request.json index 14fe4c3a8..fb4070acd 100644 --- a/schemas/registry-org/update-registry-org-request.json +++ b/schemas/registry-org/update-registry-org-request.json @@ -82,7 +82,7 @@ "contact_info": { "type": "object", "properties": { - "additional_contact_users": { + "additional_contacts": { "type": "array", "items": { "type": "object", diff --git a/schemas/registry-org/update-registry-org-response.json b/schemas/registry-org/update-registry-org-response.json index 6a2b86adc..805bd961e 100644 --- a/schemas/registry-org/update-registry-org-response.json +++ b/schemas/registry-org/update-registry-org-response.json @@ -96,7 +96,7 @@ "contact_info": { "type": "object", "properties": { - "additional_contact_users": { + "additional_contacts": { "type": "array", "items": { "type": "object", diff --git a/src/controller/org.controller/org.middleware.js b/src/controller/org.controller/org.middleware.js index 1d29764b4..5080e6a89 100644 --- a/src/controller/org.controller/org.middleware.js +++ b/src/controller/org.controller/org.middleware.js @@ -146,7 +146,7 @@ function validateCreateOrgParameters () { 'contact_info.poc_email', 'contact_info.phone', 'contact_info.website', - 'contact_info.additional_contact_users', + 'contact_info.additional_contacts', 'partner_role_type', 'partner_number', 'partner_country', diff --git a/src/controller/registry-org.controller/registry-org.middleware.js b/src/controller/registry-org.controller/registry-org.middleware.js index b7c40e1fa..c9acfc0f3 100644 --- a/src/controller/registry-org.controller/registry-org.middleware.js +++ b/src/controller/registry-org.controller/registry-org.middleware.js @@ -15,7 +15,7 @@ function parsePostParams (req, res, next) { 'top_level_root', 'users', 'charter_or_scope', 'disclosure_policy', 'product_list', 'soft_quota', 'hard_quota', - 'contact_info.additional_contact_users', 'contact_info.poc', 'contact_info.poc_email', 'contact_info.phone', 'contact_info.website', + 'contact_info.additional_contacts', 'contact_info.poc', 'contact_info.poc_email', 'contact_info.phone', 'contact_info.website', 'partner_role_type', 'partner_number', 'partner_country', diff --git a/src/model/baseorg.js b/src/model/baseorg.js index a92887902..82462dd63 100644 --- a/src/model/baseorg.js +++ b/src/model/baseorg.js @@ -15,7 +15,7 @@ const schema = { users: { type: [String], set: toUndefined }, admins: [String], contact_info: { - additional_contact_users: [{ + additional_contacts: [{ phone: String, poc: String, poc_email: String diff --git a/src/scripts/migrate.js b/src/scripts/migrate.js index b4dc9ee0b..d494cf53f 100644 --- a/src/scripts/migrate.js +++ b/src/scripts/migrate.js @@ -109,7 +109,7 @@ async function addCVEBoard (db) { soft_quota: null, hard_quota: null, contact_info: { - additional_contact_users: [], + additional_contacts: [], poc: null, poc_email: null, poc_phone: null, @@ -215,7 +215,7 @@ async function orgHelper (db) { hard_quota: doc.policies?.id_quota, admins: admins, contact_info: { - additional_contact_users: [], // don't have now + additional_contacts: [], // don't have now poc: null, // don't have now poc_email: null, // don't have now poc_phone: null, // don't have now From 959a2c6accb2d952325ccd3500544d6d23ec8f8b Mon Sep 17 00:00:00 2001 From: Andrew Foote Date: Mon, 20 Apr 2026 16:13:16 -0400 Subject: [PATCH 30/86] Adding validation for invalid key input on Org creation/update --- schemas/registry-org/BaseOrg.json | 2 +- schemas/registry-org/CNAOrg.json | 24 +++++++++++++++++++++--- src/model/cnaorg.js | 4 ++-- 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/schemas/registry-org/BaseOrg.json b/schemas/registry-org/BaseOrg.json index 87f1b1e57..d3c4a45a3 100644 --- a/schemas/registry-org/BaseOrg.json +++ b/schemas/registry-org/BaseOrg.json @@ -1,6 +1,6 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "./BaseOrg.json", + "$id": "/BaseOrg", "type": "object", "title": "CVE Base Organization", "description": "Base schema for a CVE Organization", diff --git a/schemas/registry-org/CNAOrg.json b/schemas/registry-org/CNAOrg.json index 0402e8338..32a4bbcc9 100644 --- a/schemas/registry-org/CNAOrg.json +++ b/schemas/registry-org/CNAOrg.json @@ -5,9 +5,26 @@ "title": "CVE CNA Organization", "description": "Schema for a CVE CNA Organization", "allOf": [ - { "$ref": "./BaseOrg.json" }, + { "$ref": "/BaseOrg" }, { "properties": { + "UUID": {}, + "__t": {}, + "short_name": {}, + "long_name": {}, + "aliases": {}, + "root_or_tlr": {}, + "users": {}, + "admins": {}, + "contact_info": {}, + "partner_role": {}, + "partner_type": {}, + "partner_country": {}, + "vulnerability_advisory_locations": {}, + "advisory_location_require_credentials": {}, + "industry": {}, + "tl_root_start_date": {}, + "is_cna_discussion_list": {}, "authority": { "const": ["CNA"] }, @@ -15,7 +32,7 @@ "type": "array", "uniqueItems": true, "items": { - "$ref": "./BaseOrg.json#/definitions/uuidType" + "$ref": "/BaseOrg#/definitions/uuidType" } }, "hard_quota": { @@ -36,7 +53,8 @@ "$ref": "/BaseOrg#/definitions/uriType" } }, + "additionalProperties": false, "required": ["hard_quota"] } ] -} +} \ No newline at end of file diff --git a/src/model/cnaorg.js b/src/model/cnaorg.js index ab17599c9..45f2e0233 100644 --- a/src/model/cnaorg.js +++ b/src/model/cnaorg.js @@ -3,8 +3,8 @@ const BaseOrg = require('./baseorg') const fs = require('fs') const Ajv = require('ajv') const addFormats = require('ajv-formats') -const BaseOrgSchema = JSON.parse(fs.readFileSync('src/middleware/schemas/BaseOrg.json')) -const CnaOrgSchema = JSON.parse(fs.readFileSync('src/middleware/schemas/CNAOrg.json')) +const BaseOrgSchema = JSON.parse(fs.readFileSync('schemas/registry-org/BaseOrg.json')) +const CnaOrgSchema = JSON.parse(fs.readFileSync('schemas/registry-org/CNAOrg.json')) const ajv = new Ajv({ allErrors: true }) addFormats(ajv) ajv.addSchema(BaseOrgSchema) From 7db675cb5d40dcc7b0a383a06ca314bfd375f49e Mon Sep 17 00:00:00 2001 From: david-rocca Date: Wed, 29 Apr 2026 13:39:31 -0400 Subject: [PATCH 31/86] Conflicts --- .../5.2.0_published_cna_container.json | 0 .../5.2.0_rejected_cna_container.json | 0 .../middleware/schemas => schemas}/Audit.json | 0 .../CVE_JSON_5.2.0_bundled.json | 0 schemas/registry-org/ADPOrg.json | 2 +- schemas/registry-org/BaseOrg.json | 23 ++- schemas/registry-org/BulkDownloadOrg.json | 4 +- schemas/registry-org/CNAOrg.json | 115 ++++++----- schemas/registry-org/SecretariatOrg.json | 4 +- schemas/registry-user/BaseUser.json | 104 ++++++++++ src/constants/index.js | 2 +- .../cve.controller/cve.middleware.js | 4 +- src/middleware/middleware.js | 2 +- src/middleware/schemas/ADPOrg.json | 17 -- src/middleware/schemas/BaseOrg.json | 157 -------------- src/middleware/schemas/BaseUser.json | 72 ------- src/middleware/schemas/BulkDownloadOrg.json | 17 -- src/middleware/schemas/RegistryUser.json | 10 - src/model/adporg.js | 4 +- src/model/audit.js | 2 +- src/model/baseuser.js | 2 +- src/model/bulkdownloadorg.js | 4 +- src/model/cve.js | 2 +- src/model/secretariatorg.js | 4 +- test/integration-tests/constants.js | 15 ++ .../conversation/editConversationTest.js | 5 + test/integration-tests/helpers.js | 6 + test/integration-tests/org/postOrgTest.js | 14 ++ .../integration-tests/org/postOrgUsersTest.js | 22 ++ test/integration-tests/org/registryOrg.js | 36 ++-- .../org/registryOrgAsOrgAdmin.js | 4 + .../org/regularUsersTestRegistry.js | 1 + .../registry-org/createUserByOrgTest.js | 191 ++++++++++-------- .../registry-org/registryOrgCRUDTest.js | 23 ++- test/integration-tests/user/createUserTest.js | 137 +++++++------ .../user/regularUserUpdateTest.js | 1 + test/integration-tests/user/updateUserTest.js | 36 +++- 37 files changed, 534 insertions(+), 508 deletions(-) rename {src/middleware/schemas => schemas}/5.2.0_published_cna_container.json (100%) rename {src/middleware/schemas => schemas}/5.2.0_rejected_cna_container.json (100%) rename {src/middleware/schemas => schemas}/Audit.json (100%) rename {src/middleware/schemas => schemas}/CVE_JSON_5.2.0_bundled.json (100%) create mode 100644 schemas/registry-user/BaseUser.json delete mode 100644 src/middleware/schemas/ADPOrg.json delete mode 100644 src/middleware/schemas/BaseOrg.json delete mode 100644 src/middleware/schemas/BaseUser.json delete mode 100644 src/middleware/schemas/BulkDownloadOrg.json delete mode 100644 src/middleware/schemas/RegistryUser.json diff --git a/src/middleware/schemas/5.2.0_published_cna_container.json b/schemas/5.2.0_published_cna_container.json similarity index 100% rename from src/middleware/schemas/5.2.0_published_cna_container.json rename to schemas/5.2.0_published_cna_container.json diff --git a/src/middleware/schemas/5.2.0_rejected_cna_container.json b/schemas/5.2.0_rejected_cna_container.json similarity index 100% rename from src/middleware/schemas/5.2.0_rejected_cna_container.json rename to schemas/5.2.0_rejected_cna_container.json diff --git a/src/middleware/schemas/Audit.json b/schemas/Audit.json similarity index 100% rename from src/middleware/schemas/Audit.json rename to schemas/Audit.json diff --git a/src/middleware/schemas/CVE_JSON_5.2.0_bundled.json b/schemas/CVE_JSON_5.2.0_bundled.json similarity index 100% rename from src/middleware/schemas/CVE_JSON_5.2.0_bundled.json rename to schemas/CVE_JSON_5.2.0_bundled.json diff --git a/schemas/registry-org/ADPOrg.json b/schemas/registry-org/ADPOrg.json index be9829003..7979d1f55 100644 --- a/schemas/registry-org/ADPOrg.json +++ b/schemas/registry-org/ADPOrg.json @@ -5,7 +5,7 @@ "title": "CVE ADP Organization", "description": "Schema for a CVE ADP Organization", "allOf": [ - { "$ref": "./BaseOrg.json" }, + { "$ref": "/BaseOrg" }, { "properties": { "authority": { diff --git a/schemas/registry-org/BaseOrg.json b/schemas/registry-org/BaseOrg.json index d3c4a45a3..d2a42bbf5 100644 --- a/schemas/registry-org/BaseOrg.json +++ b/schemas/registry-org/BaseOrg.json @@ -4,6 +4,7 @@ "type": "object", "title": "CVE Base Organization", "description": "Base schema for a CVE Organization", + "additionalProperties": false, "definitions": { "uuidType": { "description": "A version 4 (random) universally unique identifier (UUID) as defined by [RFC 4122](https://tools.ietf.org/html/rfc4122#section-4.1.3).", @@ -34,7 +35,12 @@ "authority": { "description": "The authority (role) of this organization within the CVE program", "type": "string", - "enum": ["CNA", "SECRETARIAT", "BULK_DOWNLOAD", "ADP"] + "enum": [ + "CNA", + "SECRETARIAT", + "BULK_DOWNLOAD", + "ADP" + ] } }, "properties": { @@ -47,6 +53,9 @@ "long_name": { "$ref": "#/definitions/longName" }, + "new_short_name": { + "$ref": "#/definitions/shortName" + }, "aliases": { "type": "array", "uniqueItems": true, @@ -81,6 +90,18 @@ "$ref": "#/definitions/uuidType" } }, + "hard_quota": { + "description": "The maximum number of CVE IDs this organization can reserve.", + "type": "integer", + "minimum": 0, + "maximum": 100000 + }, + "soft_quota": { + "description": "The threshold for notifying the organization about their remaining CVE ID count.", + "type": "integer", + "minimum": 0, + "maximum": 100000 + }, "contact_info": { "type": "object", "properties": { diff --git a/schemas/registry-org/BulkDownloadOrg.json b/schemas/registry-org/BulkDownloadOrg.json index cabc0777a..526626f17 100644 --- a/schemas/registry-org/BulkDownloadOrg.json +++ b/schemas/registry-org/BulkDownloadOrg.json @@ -1,11 +1,11 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "BaseOrg", + "$id": "BulkDownloadOrg", "type": "object", "title": "CVE Bulk Download Organization", "description": "Schema for a CVE Bulk Download Organization", "allOf": [ - { "$ref": "./BaseOrg.json" }, + { "$ref": "/BaseOrg" }, { "properties": { "authority": { diff --git a/schemas/registry-org/CNAOrg.json b/schemas/registry-org/CNAOrg.json index 32a4bbcc9..367302530 100644 --- a/schemas/registry-org/CNAOrg.json +++ b/schemas/registry-org/CNAOrg.json @@ -1,60 +1,75 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", "$id": "CNAOrg", + "type": "object", "title": "CVE CNA Organization", "description": "Schema for a CVE CNA Organization", - "allOf": [ - { "$ref": "/BaseOrg" }, - { + "additionalProperties": false, + "properties": { + "UUID": { "$ref": "/BaseOrg#/definitions/uuidType" }, + "short_name": { "$ref": "/BaseOrg#/definitions/shortName" }, + "long_name": { "$ref": "/BaseOrg#/definitions/longName" }, + "new_short_name": { + "description": "Used to rename an organization's short name during an update.", + "type": "string", + "minLength": 2, + "maxLength": 32 + }, + "contact_info": { + "type": "object", "properties": { - "UUID": {}, - "__t": {}, - "short_name": {}, - "long_name": {}, - "aliases": {}, - "root_or_tlr": {}, - "users": {}, - "admins": {}, - "contact_info": {}, - "partner_role": {}, - "partner_type": {}, - "partner_country": {}, - "vulnerability_advisory_locations": {}, - "advisory_location_require_credentials": {}, - "industry": {}, - "tl_root_start_date": {}, - "is_cna_discussion_list": {}, - "authority": { - "const": ["CNA"] - }, - "oversees": { - "type": "array", - "uniqueItems": true, - "items": { - "$ref": "/BaseOrg#/definitions/uuidType" - } - }, - "hard_quota": { - "type": "integer", - "minimum": 0 - }, - "soft_quota": { + "poc": { "type": "string" }, + "poc_email": { "type": "string" }, + "poc_phone": { "type": "string" }, + "org_email": { "type": "string" }, + "website": { "type": "string" } + }, + "additionalProperties": false + }, + "authority": { + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "/BaseOrg#/definitions/authority" + } + }, + "policies": { + "type": "object", + "properties": { + "id_quota": { "type": "integer", - "minimum": 0 - }, - "charter_or_scope": { - "$ref": "/BaseOrg#/definitions/uriType" - }, - "disclosure_policy": { - "$ref": "/BaseOrg#/definitions/uriType" - }, - "product_list": { - "$ref": "/BaseOrg#/definitions/uriType" + "minimum": 0, + "maximum": 100000 } - }, - "additionalProperties": false, - "required": ["hard_quota"] + } + }, + "hard_quota": { + "type": "integer", + "minimum": 0, + "maximum": 100000 + }, + "soft_quota": { + "type": "integer", + "minimum": 0, + "maximum": 100000 + }, + "oversees": { + "type": "array", + "uniqueItems": true, + "items": { + "type": "string", + "format": "uuid" + } + }, + "partner_role": { + "type": "string" + }, + "partner_type": { + "type": "string" + }, + "partner_country": { + "type": "string" } - ] + }, + "required": ["short_name", "hard_quota"] } \ No newline at end of file diff --git a/schemas/registry-org/SecretariatOrg.json b/schemas/registry-org/SecretariatOrg.json index 469bd7df5..4e658b571 100644 --- a/schemas/registry-org/SecretariatOrg.json +++ b/schemas/registry-org/SecretariatOrg.json @@ -5,7 +5,7 @@ "title": "CVE Secretariat Organization", "description": "Schema for a CVE Secretariat Organization", "allOf": [ - { "$ref": "./BaseOrg.json" }, + { "$ref": "/BaseOrg" }, { "properties": { "authority": { @@ -15,7 +15,7 @@ "type": "array", "uniqueItems": true, "items": { - "$ref": "./BaseOrg.json#/definitions/uuidType" + "$ref": "/BaseOrg#/definitions/uuidType" } }, "hard_quota": { diff --git a/schemas/registry-user/BaseUser.json b/schemas/registry-user/BaseUser.json new file mode 100644 index 000000000..5fa85687e --- /dev/null +++ b/schemas/registry-user/BaseUser.json @@ -0,0 +1,104 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "/BaseUser", + "type": "object", + "title": "CVE Base User Schema", + "additionalProperties": false, + "description": "The schema for CVE Services Users", + "definitions": { + "uuidType": { + "description": "A version 4 (random) universally unique identifier (UUID) as defined by [RFC 4122](https://tools.ietf.org/html/rfc4122#section-4.1.3).", + "type": "string", + "format": "uuid", + "pattern": "^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$" + }, + "name": { + "description": "User's name components", + "type": "object", + "required": [ + "first", + "last" + ], + "properties": { + "first": { + "type": "string", + "maxLength": 100 + }, + "middle": { + "type": "string", + "maxLength": 100 + }, + "last": { + "type": "string", + "maxLength": 100 + }, + "suffix": { + "type": "string", + "maxLength": 100 + } + }, + "additionalProperties": false + } + }, + "properties": { + "name": { + "$ref": "#/definitions/name" + }, + "username": { + "description": "Username should be 3-128 characters. Allowed characters are alphanumeric and -_@.", + "type": "string", + "minLength": 3, + "maxLength": 128, + "pattern": "^[A-Za-z0-9\\-_@.]{3,128}$" + }, + "active": { + "description": "Whether the user account is active. Supports boolean or string based on legacy test constants.", + "type": [ + "boolean", + "string" + ] + }, + "authority": { + "description": "The user's authority and roles, often used in joint review contexts.", + "type": "object", + "properties": { + "active_roles": { + "type": "array", + "uniqueItems": true, + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "secret": { + "description": "Hashed secret for user authentication", + "type": "string" + }, + "UUID": { + "$ref": "#/definitions/uuidType" + }, + "status": { + "description": "User status: 'active' or 'inactive'", + "type": "string", + "enum": [ + "active", + "inactive" + ] + }, + "role": { + "description": "The user's role in the organization", + "type": "string" + }, + "org_short_name": { + "description": "Used to update the organization association of a user", + "type": "string", + "minLength": 2, + "maxLength": 32 + } + }, + "required": [ + "username" + ] +} \ No newline at end of file diff --git a/src/constants/index.js b/src/constants/index.js index a4c73c910..69f295e9f 100644 --- a/src/constants/index.js +++ b/src/constants/index.js @@ -1,5 +1,5 @@ const fs = require('fs') -const cveSchemaV5 = JSON.parse(fs.readFileSync('src/middleware/schemas/CVE_JSON_5.2.0_bundled.json')) +const cveSchemaV5 = JSON.parse(fs.readFileSync('schemas/CVE_JSON_5.2.0_bundled.json')) /** * Return default values. diff --git a/src/controller/cve.controller/cve.middleware.js b/src/controller/cve.controller/cve.middleware.js index 90b5a64e7..576bdad3f 100644 --- a/src/controller/cve.controller/cve.middleware.js +++ b/src/controller/cve.controller/cve.middleware.js @@ -4,8 +4,8 @@ const errors = require('./error') const error = new errors.CveControllerError() const utils = require('../../utils/utils') const fs = require('fs') -const RejectedSchema = JSON.parse(fs.readFileSync('src/middleware/schemas/5.2.0_rejected_cna_container.json')) -const cnaContainerSchema = JSON.parse(fs.readFileSync('src/middleware/schemas/5.2.0_published_cna_container.json')) +const RejectedSchema = JSON.parse(fs.readFileSync('schemas/5.2.0_rejected_cna_container.json')) +const cnaContainerSchema = JSON.parse(fs.readFileSync('schemas/5.2.0_published_cna_container.json')) const logger = require('../../middleware/logger') const Ajv = require('ajv') const addFormats = require('ajv-formats') diff --git a/src/middleware/middleware.js b/src/middleware/middleware.js index 67e47e7b8..6cd272531 100644 --- a/src/middleware/middleware.js +++ b/src/middleware/middleware.js @@ -1,6 +1,6 @@ const getConstants = require('../constants').getConstants const fs = require('fs') -const cveSchemaV5 = JSON.parse(fs.readFileSync('src/middleware/schemas/CVE_JSON_5.2.0_bundled.json')) +const cveSchemaV5 = JSON.parse(fs.readFileSync('schemas/CVE_JSON_5.2.0_bundled.json')) const argon2 = require('argon2') const logger = require('./logger') const Ajv = require('ajv') diff --git a/src/middleware/schemas/ADPOrg.json b/src/middleware/schemas/ADPOrg.json deleted file mode 100644 index 7979d1f55..000000000 --- a/src/middleware/schemas/ADPOrg.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "ADPOrg", - "type": "object", - "title": "CVE ADP Organization", - "description": "Schema for a CVE ADP Organization", - "allOf": [ - { "$ref": "/BaseOrg" }, - { - "properties": { - "authority": { - "const": ["ADP"] - } - } - } - ] -} diff --git a/src/middleware/schemas/BaseOrg.json b/src/middleware/schemas/BaseOrg.json deleted file mode 100644 index f7039bcca..000000000 --- a/src/middleware/schemas/BaseOrg.json +++ /dev/null @@ -1,157 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "/BaseOrg", - "type": "object", - "title": "CVE Base Organization", - "description": "Base schema for a CVE Organization", - "definitions": { - "uuidType": { - "description": "A version 4 (random) universally unique identifier (UUID) as defined by [RFC 4122](https://tools.ietf.org/html/rfc4122#section-4.1.3).", - "type": "string", - "format": "uuid", - "pattern": "^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$" - }, - "uriType": { - "description": "A universal resource identifier (URI), according to [RFC 3986](https://tools.ietf.org/html/rfc3986).", - "type": "string", - "format": "uri", - "pattern": "^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\\?([^#]*))?(#(.*))?", - "minLength": 1, - "maxLength": 2048 - }, - "shortName": { - "description": "A 2-32 character name that can be used to complement an organization's UUID.", - "type": "string", - "minLength": 2, - "maxLength": 32 - }, - "longName": { - "description": "A 1-256 character name that can be used to complement an organization's short_name.", - "type": "string", - "minLength": 1, - "maxLength": 256 - }, - "authority": { - "description": "The authority (role) of this organization within the CVE program", - "type": "string", - "enum": ["CNA", "SECRETARIAT", "BULK_DOWNLOAD", "ADP"] - }, - "discriminator": { - "description": "Discriminator key used by Mongoose for type inheritance", - "type": "string" - }, - "timestamp": { - "description": "Date/time format based on RFC3339 and ISO ISO8601, with an optional timezone in the format 'yyyy-MM-ddTHH:mm:ss[+-]ZH:ZM'. If timezone offset is not given, GMT (+00:00) is assumed.", - "pattern": "^(((2000|2400|2800|(19|2[0-9](0[48]|[2468][048]|[13579][26])))-02-29)|(((19|2[0-9])[0-9]{2})-02-(0[1-9]|1[0-9]|2[0-8]))|(((19|2[0-9])[0-9]{2})-(0[13578]|10|12)-(0[1-9]|[12][0-9]|3[01]))|(((19|2[0-9])[0-9]{2})-(0[469]|11)-(0[1-9]|[12][0-9]|30)))T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})?$", - "type": "string" - } - }, - "properties": { - "UUID": { - "$ref": "#/definitions/uuidType" - }, - "__t": { - "$ref": "#/definitions/discriminator" - }, - "short_name": { - "$ref": "#/definitions/shortName" - }, - "long_name": { - "$ref": "#/definitions/longName" - }, - "aliases": { - "type": "array", - "uniqueItems": true, - "items": { - "type": "string" - } - }, - "authority": { - "type": "array", - "uniqueItems": true, - "items": { - "$ref": "#/definitions/authority" - } - }, - "root_or_tlr": { - "type": "boolean" - }, - "users": { - "type": "array", - "uniqueItems": true, - "items": { - "$ref": "#/definitions/uuidType" - } - }, - "admins": { - "type": "array", - "uniqueItems": true, - "items": { - "$ref": "#/definitions/uuidType" - } - }, - "contact_info": { - "type": "object", - "properties": { - "additional_contact_users": { - "type": "array", - "uniqueItems": true, - "items": { - "$ref": "#/definitions/uuidType" - } - }, - "poc": { - "type": "string" - }, - "poc_email": { - "type": "string", - "format": "email" - }, - "poc_phone": { - "type": "string" - }, - "org_email": { - "type": "string", - "format": "email" - }, - "website": { - "$ref": "#/definitions/uriType", - "pattern": "^(ftp|http)s?://\\S+$" - } - }, - "additionalProperties": false - }, - "partner_role": { - "type": "string" - }, - "partner_type": { - "type": "string" - }, - "partner_country": { - "type": "string" - }, - "vulnerability_advisory_locations": { - "type": "array", - "uniqueItems": true, - "items": { - "type": "string" - } - }, - "advisory_location_require_credentials": { - "type": "boolean" - }, - "industry": { - "type": "string" - }, - "tl_root_start_date": { - "$ref": "#/definitions/timestamp" - }, - "is_cna_discussion_list": { - "type": "boolean" - } - }, - "required": [ - "short_name", - "long_name" - ] -} \ No newline at end of file diff --git a/src/middleware/schemas/BaseUser.json b/src/middleware/schemas/BaseUser.json deleted file mode 100644 index 2c1bf93de..000000000 --- a/src/middleware/schemas/BaseUser.json +++ /dev/null @@ -1,72 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "/BaseUser", - "type": "object", - "title": "CVE Base User Schema", - "description": "The schema for CVE Services Users", - "definitions": { - "uuidType": { - "description": "A version 4 (random) universally unique identifier (UUID) as defined by [RFC 4122](https://tools.ietf.org/html/rfc4122#section-4.1.3).", - "type": "string", - "format": "uuid", - "pattern": "^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$" - }, - "name": { - "description": "User's name components", - "type": "object", - "required": [ - "first", - "last" - ], - "properties": { - "first": { - "type": "string", - "maxLength": 100 - }, - "middle": { - "type": "string", - "maxLength": 100 - }, - "last": { - "type": "string", - "maxLength": 100 - }, - "suffix": { - "type": "string", - "maxLength": 100 - } - }, - "additionalProperties": false - } - }, - "properties": { - "name": { - "$ref": "#/definitions/name" - }, - "username": { - "description": "Username should be 3-128 characters. Allowed characters are alphanumeric and -_@.", - "type": "string", - "minLength": 3, - "maxLength": 128, - "pattern": "^[A-Za-z0-9\\-_@.]{3,128}$" - }, - "secret": { - "description": "Hashed secret for user authentication", - "type": "string" - }, - "UUID": { - "$ref": "#/definitions/uuidType" - }, - "status": { - "description": "User status: 'active' or 'inactive'", - "type": "string", - "enum": [ - "active", - "inactive" - ] - } - }, - "required": [ - "username" - ] -} \ No newline at end of file diff --git a/src/middleware/schemas/BulkDownloadOrg.json b/src/middleware/schemas/BulkDownloadOrg.json deleted file mode 100644 index ada140853..000000000 --- a/src/middleware/schemas/BulkDownloadOrg.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "BaseOrg", - "type": "object", - "title": "CVE Bulk Download Organization", - "description": "Schema for a CVE Bulk Download Organization", - "allOf": [ - { "$ref": "/BaseOrg" }, - { - "properties": { - "authority": { - "const": ["BULK_DOWNLOAD"] - } - } - } - ] -} diff --git a/src/middleware/schemas/RegistryUser.json b/src/middleware/schemas/RegistryUser.json deleted file mode 100644 index de95595ab..000000000 --- a/src/middleware/schemas/RegistryUser.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "$id": "RegistryUser", - "title": "CVE Registry User Schema", - "description": "Schema for a CVE Registry User", - "allOf": [ - { "$ref": "/BaseUser" } - ] -} diff --git a/src/model/adporg.js b/src/model/adporg.js index f5efa867c..0c5a80799 100644 --- a/src/model/adporg.js +++ b/src/model/adporg.js @@ -3,8 +3,8 @@ const BaseOrg = require('./baseorg') const fs = require('fs') const Ajv = require('ajv') const addFormats = require('ajv-formats') -const BaseOrgSchema = JSON.parse(fs.readFileSync('src/middleware/schemas/BaseOrg.json')) -const AdpOrgSchema = JSON.parse(fs.readFileSync('src/middleware/schemas/ADPOrg.json')) +const BaseOrgSchema = JSON.parse(fs.readFileSync('schemas/registry-org/BaseOrg.json')) +const AdpOrgSchema = JSON.parse(fs.readFileSync('schemas/registry-org/ADPOrg.json')) const ajv = new Ajv({ allErrors: true }) addFormats(ajv) ajv.addSchema(BaseOrgSchema) diff --git a/src/model/audit.js b/src/model/audit.js index 9d346566c..d749b691d 100644 --- a/src/model/audit.js +++ b/src/model/audit.js @@ -4,7 +4,7 @@ const aggregatePaginate = require('mongoose-aggregate-paginate-v2') const MongoPaging = require('mongo-cursor-pagination') const Ajv = require('ajv') const addFormats = require('ajv-formats') -const AuditSchemaJSON = JSON.parse(fs.readFileSync('src/middleware/schemas/Audit.json')) +const AuditSchemaJSON = JSON.parse(fs.readFileSync('schemas/Audit.json')) // Initialize AJV const ajv = new Ajv({ allErrors: true }) diff --git a/src/model/baseuser.js b/src/model/baseuser.js index 260179970..fcd4b1777 100644 --- a/src/model/baseuser.js +++ b/src/model/baseuser.js @@ -6,7 +6,7 @@ const Ajv = require('ajv') const addFormats = require('ajv-formats') // Load BaseUser JSON schema -const BaseUserSchemaJSON = JSON.parse(fs.readFileSync('src/middleware/schemas/BaseUser.json')) +const BaseUserSchemaJSON = JSON.parse(fs.readFileSync('schemas/registry-user/BaseUser.json')) // Initialize AJV const ajv = new Ajv({ allErrors: true }) diff --git a/src/model/bulkdownloadorg.js b/src/model/bulkdownloadorg.js index e196b5ff3..cdf06cf9d 100644 --- a/src/model/bulkdownloadorg.js +++ b/src/model/bulkdownloadorg.js @@ -3,8 +3,8 @@ const BaseOrg = require('./baseorg') const fs = require('fs') const Ajv = require('ajv') const addFormats = require('ajv-formats') -const BaseOrgSchema = JSON.parse(fs.readFileSync('src/middleware/schemas/BaseOrg.json')) -const BulkDownloadOrgSchema = JSON.parse(fs.readFileSync('src/middleware/schemas/BulkDownloadOrg.json')) +const BaseOrgSchema = JSON.parse(fs.readFileSync('schemas/registry-org/BaseOrg.json')) +const BulkDownloadOrgSchema = JSON.parse(fs.readFileSync('schemas/registry-org/BulkDownloadOrg.json')) const ajv = new Ajv({ allErrors: true }) addFormats(ajv) ajv.addSchema(BaseOrgSchema) diff --git a/src/model/cve.js b/src/model/cve.js index 59e23aeee..81f1eee87 100644 --- a/src/model/cve.js +++ b/src/model/cve.js @@ -2,7 +2,7 @@ const mongoose = require('mongoose') const aggregatePaginate = require('mongoose-aggregate-paginate-v2') const MongoPaging = require('mongo-cursor-pagination') const fs = require('fs') -const cveSchemaV5 = JSON.parse(fs.readFileSync('src/middleware/schemas/CVE_JSON_5.2.0_bundled.json')) +const cveSchemaV5 = JSON.parse(fs.readFileSync('schemas/CVE_JSON_5.2.0_bundled.json')) const Ajv = require('ajv') const addFormats = require('ajv-formats') diff --git a/src/model/secretariatorg.js b/src/model/secretariatorg.js index 127d236a6..073f1c9d7 100644 --- a/src/model/secretariatorg.js +++ b/src/model/secretariatorg.js @@ -3,8 +3,8 @@ const BaseOrg = require('./baseorg') const fs = require('fs') const Ajv = require('ajv') const addFormats = require('ajv-formats') -const BaseOrgSchema = JSON.parse(fs.readFileSync('src/middleware/schemas/BaseOrg.json')) -const SecretariatOrgSchema = JSON.parse(fs.readFileSync('src/middleware/schemas/SecretariatOrg.json')) +const BaseOrgSchema = JSON.parse(fs.readFileSync('schemas/registry-org/BaseOrg.json')) +const SecretariatOrgSchema = JSON.parse(fs.readFileSync('schemas/registry-org/SecretariatOrg.json')) const ajv = new Ajv({ allErrors: true }) addFormats(ajv) ajv.addSchema(BaseOrgSchema) diff --git a/test/integration-tests/constants.js b/test/integration-tests/constants.js index de4946825..80700e31e 100644 --- a/test/integration-tests/constants.js +++ b/test/integration-tests/constants.js @@ -363,6 +363,20 @@ const testOrg = { } } +const testOrg2 = { + + short_name: 'test_org2', + name: 'Test Organization 2', + authority: { + active_roles: [ + 'CNA' + ] + }, + policies: { + id_quota: 100000 + } +} + const testRegistryOrg = { short_name: 'test_registry_org', long_name: 'Test Registry Organization', @@ -432,6 +446,7 @@ module.exports = { testAdp, testAdp2, testOrg, + testOrg2, testRegistryOrg, testRegistryOrg2, existingOrg, diff --git a/test/integration-tests/conversation/editConversationTest.js b/test/integration-tests/conversation/editConversationTest.js index 4dc371ddb..08e101f6e 100644 --- a/test/integration-tests/conversation/editConversationTest.js +++ b/test/integration-tests/conversation/editConversationTest.js @@ -27,6 +27,11 @@ describe('Testing Conversation endpoints', () => { expect(err).to.be.undefined expect(res).to.have.status(200) org = res.body + delete org.created + delete org.last_updated + delete org.admins + delete org.users + delete org.root_or_tlr }) await chai diff --git a/test/integration-tests/helpers.js b/test/integration-tests/helpers.js index 2b8685d4e..de16ccfd8 100644 --- a/test/integration-tests/helpers.js +++ b/test/integration-tests/helpers.js @@ -122,6 +122,9 @@ async function userDeactivateAsSecHelper (userName, orgShortName) { .set(constants.headers) .then(res => res.body) + delete user.created + delete user.last_updated + delete user.created_by await chai.request(app) .put(`/api/registry/org/${orgShortName}/user/${userName}`) .set(constants.headers) @@ -139,6 +142,9 @@ async function userReactivateAsSecHelper (userName, orgShortName) { .then(res => res.body) user.status = 'active' + delete user.created + delete user.last_updated + delete user.created_by await chai.request(app) .put(`/api/registry/org/${orgShortName}/user/${userName}`) diff --git a/test/integration-tests/org/postOrgTest.js b/test/integration-tests/org/postOrgTest.js index fbc8b8dde..a94245fd6 100644 --- a/test/integration-tests/org/postOrgTest.js +++ b/test/integration-tests/org/postOrgTest.js @@ -97,5 +97,19 @@ describe('Testing Org post endpoint', () => { expect(res.body.error).to.equal('ORG_EXISTS') }) }) + it('Should fail to create an org with an erroneous key not found in the schema with registry enabled', async () => { + await chai.request(app) + .post('/api/registry/org') + .set({ ...constants.headers }) + .send({ + ...constants.testRegistryOrg, + test: 'additional key not in schema' + }) + .then((res, err) => { + expect(res).to.have.status(400) + expect(res.body.message).to.equal('Parameters were invalid') + expect(res.body.errors[0].message).to.equal('must NOT have additional properties') + }) + }) }) }) diff --git a/test/integration-tests/org/postOrgUsersTest.js b/test/integration-tests/org/postOrgUsersTest.js index 098763b1b..d952c7237 100644 --- a/test/integration-tests/org/postOrgUsersTest.js +++ b/test/integration-tests/org/postOrgUsersTest.js @@ -332,5 +332,27 @@ describe('Testing user post endpoint', () => { ) }) }) + it('Fails creation of user with registry enabled and an erroneous key not found in the schema', async () => { + await chai + .request(app) + .post('/api/registry/org/mitre/user') + .set({ ...constants.headers, ...shortName }) + .send({ + username: 'fakeregistryuser1002', + name: { + first: 'FirstName', + last: 'LastName', + middle: 'MiddleName', + suffix: 'Suffix' + }, + role: 'ADMIN', + test: 'additional key not in schema' + }) + .then((res) => { + expect(res).to.have.status(400) + expect(res.body.message).to.equal('Parameters were invalid') + expect(res.body.errors[0].message).to.equal('must NOT have additional properties') + }) + }) }) }) diff --git a/test/integration-tests/org/registryOrg.js b/test/integration-tests/org/registryOrg.js index 9328f32dc..0dec2824e 100644 --- a/test/integration-tests/org/registryOrg.js +++ b/test/integration-tests/org/registryOrg.js @@ -157,21 +157,6 @@ describe('Testing Secretariat functionality for Orgs', () => { }) }) - it('A new user is created even if extra data is in the body', async () => { - const username = uuidv4() - await chai.request(app) - .post('/api/registry/org/mitre/user') - .set(secretariatHeaders) - .send({ - username, - ubiquitous: 'mendacious' - }) - .then((res) => { - expect(res).to.have.status(200) - expect(res.body.message).to.equal(`${username} was successfully created.`) - }) - }) - it('A users username can be updated', async function () { const { orgShortName, username } = await createNewUserWithNewOrg() const newUsername = uuidv4() @@ -179,6 +164,8 @@ describe('Testing Secretariat functionality for Orgs', () => { await chai.request(app).get(`/api/registry/org/${orgShortName}/user/${username}`).set(secretariatHeaders).then((res) => { user = res.body }) + delete user.created + delete user.last_updated await chai.request(app) .put(`/api/registry/org/${orgShortName}/user/${username}`) .set(secretariatHeaders) @@ -209,6 +196,8 @@ describe('Testing Secretariat functionality for Orgs', () => { let user await chai.request(app).get(`/api/registry/org/${orgShortName}/user/${username}`).set(secretariatHeaders).then((res) => { user = res.body }) + delete user.created + delete user.last_updated await chai.request(app) .put(`/api/registry/org/${orgShortName}/user/${username}`) .set(secretariatHeaders) @@ -244,6 +233,8 @@ describe('Testing Secretariat functionality for Orgs', () => { await chai.request(app).get(`/api/registry/org/${orgShortName}/user/${username}`).set(secretariatHeaders).then((res) => { user = res.body }) + delete user.created + delete user.last_updated await chai.request(app) .put(`/api/registry/org/${orgShortName}/user/${username}`) .set(secretariatHeaders) @@ -319,6 +310,21 @@ describe('Testing Secretariat functionality for Orgs', () => { }) context('Negative Tests', () => { + it('A new user is not created if extra data is in the body', async () => { + const username = uuidv4() + await chai.request(app) + .post('/api/registry/org/mitre/user') + .set(secretariatHeaders) + .send({ + username, + ubiquitous: 'mendacious' + }) + .then((res) => { + expect(res).to.have.status(400) + expect(res.body.message).to.equal('Parameters were invalid') + }) + }) + it('Should not retrieve an org for a non-existent UUID', async () => { const nonExistentUUID = 'nonexistent123' await chai.request(app) diff --git a/test/integration-tests/org/registryOrgAsOrgAdmin.js b/test/integration-tests/org/registryOrgAsOrgAdmin.js index d248215dd..ef2356bce 100644 --- a/test/integration-tests/org/registryOrgAsOrgAdmin.js +++ b/test/integration-tests/org/registryOrgAsOrgAdmin.js @@ -282,6 +282,10 @@ describe('Testing Registry Org as org admin', () => { it('Registry: Services api prevents org admins from updating a users username if that user already exists', async () => { let user await chai.request(app).get('/api/registry/org/beat_10/user/patriciawilliams@beat_10.com').set(adminHeaders).then((res) => { user = res.body }) + + delete user.created + delete user.last_updated + delete user.created_by await chai.request(app) .put('/api/registry/org/beat_10/user/patriciawilliams@beat_10.com') .set(adminHeaders) diff --git a/test/integration-tests/org/regularUsersTestRegistry.js b/test/integration-tests/org/regularUsersTestRegistry.js index 348df2336..b67aee73f 100644 --- a/test/integration-tests/org/regularUsersTestRegistry.js +++ b/test/integration-tests/org/regularUsersTestRegistry.js @@ -24,6 +24,7 @@ describe('Testing regular user permissions for /api/registry/org/ endpoints with .set(constants.nonSecretariatUserHeaders) .then((res) => { previousBody = res.body }) + delete previousBody.created_by await chai.request(app) .put(`/api/registry/org/${org}/user/${user}`) .set(constants.nonSecretariatUserHeaders) diff --git a/test/integration-tests/registry-org/createUserByOrgTest.js b/test/integration-tests/registry-org/createUserByOrgTest.js index 9397eb8db..3cba5a152 100644 --- a/test/integration-tests/registry-org/createUserByOrgTest.js +++ b/test/integration-tests/registry-org/createUserByOrgTest.js @@ -9,98 +9,111 @@ const constants = require('../constants.js') const app = require('../../../src/index.js') describe('Testing POST /api/registryOrg/:shortname/user endpoint', () => { - // Positive test - it('Should create a new user in an organization', (done) => { - const orgShortName = 'mitre' - const newUser = { - username: 'testuser@example.com', - name: { - first: 'Test', - last: 'User' - }, - role: 'ADMIN' - } - - chai.request(app) - .post(`/api/registryOrg/${orgShortName}/user`) - .set(constants.headers) - .send(newUser) - .end((err, res) => { - expect(err).to.be.null - expect(res).to.have.status(200) - expect(res.body).to.have.property('message').equal(`${newUser.username} was successfully created.`) - expect(res.body).to.have.property('created') - expect(res.body.created).to.have.property('username', newUser.username) - expect(res.body.created).to.have.property('secret') - done() - }) - }) - - // Negative test: Organization does not exist - it('Should not create a user in a non-existent organization', (done) => { - const orgShortName = 'nonexistentorg' - const newUser = { - username: 'testuser2@example.com', - name: { - first: 'Test', - last: 'User' + context('Positive Tests', () => { + it('Should create a new user in an organization', (done) => { + const orgShortName = 'mitre' + const newUser = { + username: 'testuser@example.com', + name: { + first: 'Test', + last: 'User' + }, + role: 'ADMIN' } - } - - chai.request(app) - .post(`/api/registryOrg/${orgShortName}/user`) - .set(constants.headers) - .send(newUser) - .end((err, res) => { - expect(err).to.be.null - expect(res).to.have.status(404) - expect(res.body).to.have.property('message').equal(`The '${orgShortName}' organization designated by the shortname path parameter does not exist.`) - done() - }) + chai.request(app) + .post(`/api/registryOrg/${orgShortName}/user`) + .set(constants.headers) + .send(newUser) + .end((err, res) => { + expect(err).to.be.null + expect(res).to.have.status(200) + expect(res.body).to.have.property('message').equal(`${newUser.username} was successfully created.`) + expect(res.body).to.have.property('created') + expect(res.body.created).to.have.property('username', newUser.username) + expect(res.body.created).to.have.property('secret') + done() + }) + }) }) - - // Negative test: User already exists - it('Should not create a user that already exists', (done) => { - const orgShortName = 'mitre' - const existingUser = { - username: 'testuser@example.com', - name: { - first: 'Test', - last: 'User' + context('Negative Tests', () => { + it('Should not create a user in a non-existent organization', (done) => { + const orgShortName = 'nonexistentorg' + const newUser = { + username: 'testuser2@example.com', + name: { + first: 'Test', + last: 'User' + } } - } - - chai.request(app) - .post(`/api/registryOrg/${orgShortName}/user`) - .set(constants.headers) - .send(existingUser) - .end((err, res) => { - expect(err).to.be.null - expect(res).to.have.status(400) - expect(res.body).to.have.property('message').equal(`The user '${existingUser.username}' already exists.`) - done() - }) - }) - - // Negative test: Validation error (missing username) - it('Should not create a user with a missing username', (done) => { - const orgShortName = 'mitre' - const invalidUser = { - name: { - first: 'Test', - last: 'User' + chai.request(app) + .post(`/api/registryOrg/${orgShortName}/user`) + .set(constants.headers) + .send(newUser) + .end((err, res) => { + expect(err).to.be.null + expect(res).to.have.status(404) + expect(res.body).to.have.property('message').equal(`The '${orgShortName}' organization designated by the shortname path parameter does not exist.`) + done() + }) + }) + it('Should not create a user that already exists', (done) => { + const orgShortName = 'mitre' + const existingUser = { + username: 'testuser@example.com', + name: { + first: 'Test', + last: 'User' + } } - } - - chai.request(app) - .post(`/api/registryOrg/${orgShortName}/user`) - .set(constants.headers) - .send(invalidUser) - .end((err, res) => { - expect(err).to.be.null - expect(res).to.have.status(400) - expect(res.body).to.have.property('message').equal('Parameters were invalid') - done() - }) + chai.request(app) + .post(`/api/registryOrg/${orgShortName}/user`) + .set(constants.headers) + .send(existingUser) + .end((err, res) => { + expect(err).to.be.null + expect(res).to.have.status(400) + expect(res.body).to.have.property('message').equal(`The user '${existingUser.username}' already exists.`) + done() + }) + }) + it('Should not create a user with a missing username', (done) => { + const orgShortName = 'mitre' + const invalidUser = { + name: { + first: 'Test', + last: 'User' + } + } + chai.request(app) + .post(`/api/registryOrg/${orgShortName}/user`) + .set(constants.headers) + .send(invalidUser) + .end((err, res) => { + expect(err).to.be.null + expect(res).to.have.status(400) + expect(res.body).to.have.property('message').equal('Parameters were invalid') + done() + }) + }) + it('Should not create a user with an erroneous key not found in the schema', async () => { + const orgShortName = 'mitre' + const existingUser = { + username: 'testuser@example.com', + name: { + first: 'Test', + last: 'User' + }, + test: 'additional key not in schema' + } + chai.request(app) + .post(`/api/registryOrg/${orgShortName}/user`) + .set(constants.headers) + .send(existingUser) + .then((res) => { + expect(res).to.have.status(400) + expect(res.body.message).to.equal('Parameters were invalid') + expect(res.body.errors[0].message).to.equal('must NOT have additional properties') + }) + }) }) }) diff --git a/test/integration-tests/registry-org/registryOrgCRUDTest.js b/test/integration-tests/registry-org/registryOrgCRUDTest.js index db15adecc..a69f520a9 100644 --- a/test/integration-tests/registry-org/registryOrgCRUDTest.js +++ b/test/integration-tests/registry-org/registryOrgCRUDTest.js @@ -60,6 +60,8 @@ describe('Testing /registryOrg endpoints', () => { expect(res.body.created.partner_country).to.equal(testRegistryOrg.partner_country) createdOrg = res.body.created + delete createdOrg.created + delete createdOrg.last_updated }) }) }) @@ -102,6 +104,20 @@ describe('Testing /registryOrg endpoints', () => { expect(res.body.details[0].msg).to.equal('reports_to must not be present') }) }) + it('Fails to create a new registry organization with an erroneous key not found in the schema', async () => { + await chai.request(app) + .post('/api/registryOrg') + .set(secretariatHeaders) + .send({ + ...testRegistryOrg, + test: 'additional key not in schema' + }) + .then((res) => { + expect(res).to.have.status(400) + expect(res.body.message).to.equal('Parameters were invalid') + expect(res.body.errors[0].message).to.equal('must NOT have additional properties') + }) + }) }) }) context('Testing GET /registryOrg endpoints', () => { @@ -403,17 +419,18 @@ describe('Testing /registryOrg endpoints', () => { expect(res.body.details[0].msg).to.equal('reports_to must not be present') }) }) - it('Fails to update a registry organization with an invalidly high quota', async () => { + it('Fails to update a registry organization providing an erroneous key not found in the schema', async () => { await chai.request(app) - .put(`/api/registryOrg/${createdOrg.short_name}`) + .put('/api/registryOrg/registry_org_test') .set(secretariatHeaders) .send({ ...createdOrg, - hard_quota: 1000000 + test: 'additional key not in schema' }) .then((res) => { expect(res).to.have.status(400) expect(res.body.message).to.equal('Parameters were invalid') + expect(res.body.errors[0].message).to.equal('must NOT have additional properties') }) }) }) diff --git a/test/integration-tests/user/createUserTest.js b/test/integration-tests/user/createUserTest.js index 54cb887c3..85a1be780 100644 --- a/test/integration-tests/user/createUserTest.js +++ b/test/integration-tests/user/createUserTest.js @@ -24,16 +24,13 @@ const body = { const registryBody = { username: 'adpUser2', - active: 'true', name: { first: 'SecondTestCnaAdmin', last: 'test', middle: 'N', suffix: 'I' }, - authority: { - active_roles: ['Admin'] - } + status: 'active' } const nonAdminBody = { @@ -51,69 +48,95 @@ const nonAdminBody = { const registryNonAdminBody = { username: 'nonAdminUser2', - active: 'true', name: { first: 'SecondTestCnaAdmin', last: 'test', middle: 'N', suffix: 'I' }, - authority: { - } + status: 'active' } describe('Testing create user endpoint', () => { - it('Should return 200 and new user', (done) => { - chai.request(app) - .post('/api/org/range_4/user') - .set(constants.headers) - .send(body) - .end((err, res) => { - expect(err).to.be.null - expect(res.body).to.have.property('created') - expect(res.body.created.username).to.equal(body.username) - expect(res).to.have.status(200) - done() - }) + context('Positive Tests', () => { + it('Should return 200 and new user', (done) => { + chai.request(app) + .post('/api/org/range_4/user') + .set(constants.headers) + .send(body) + .end((err, res) => { + expect(err).to.be.null + expect(res.body).to.have.property('created') + expect(res.body.created.username).to.equal(body.username) + expect(res).to.have.status(200) + done() + }) + }) + it('Should return 200 and new user with registry enabled', (done) => { + chai.request(app) + .post('/api/registry/org/range_4/user') + .set(constants.headers) + .send(registryBody) + .end((err, res) => { + expect(err).to.be.null + expect(res).to.have.status(200) + done() + }) + }) + it('Should return 200 and create a non admin user', (done) => { + chai.request(app) + .post('/api/org/range_4/user') + .set(constants.headers) + .send(nonAdminBody) + .end((err, res) => { + expect(err).to.be.null + expect(res.body).to.have.property('created') + expect(res.body.created.username).to.equal(nonAdminBody.username) + expect(res).to.have.status(200) + done() + }) + }) + it('Should return 200 and create a non admin user with registry enabled', (done) => { + chai.request(app) + .post('/api/registry/org/range_4/user') + .set(constants.headers) + .send(registryNonAdminBody) + .end((err, res) => { + expect(err).to.be.null + expect(res).to.have.status(200) + done() + }) + }) }) - it('Should return 200 and new user with registry enabled', (done) => { - chai.request(app) - .post('/api/registry/org/range_4/user') - .set(constants.headers) - .send(registryBody) - .end((err, res) => { - expect(err).to.be.null - expect(res.body).to.have.property('created') - expect(res.body.created.username).to.equal(registryBody.username) - expect(res).to.have.status(200) - done() - }) - }) - it('Should return 200 and create a non admin user', (done) => { - chai.request(app) - .post('/api/org/range_4/user') - .set(constants.headers) - .send(nonAdminBody) - .end((err, res) => { - expect(err).to.be.null - expect(res.body).to.have.property('created') - expect(res.body.created.username).to.equal(nonAdminBody.username) - expect(res).to.have.status(200) - done() - }) - }) - it('Should return 200 and create a non admin user with registry enabled', (done) => { - chai.request(app) - .post('/api/registry/org/range_4/user') - .set(constants.headers) - .send(registryNonAdminBody) - .end((err, res) => { - expect(err).to.be.null - expect(res.body).to.have.property('created') - expect(res.body.created.username).to.equal(registryNonAdminBody.username) - expect(res).to.have.status(200) - done() - }) + context('Negative Tests', () => { + it('Should return 400 creating a new user with an erroneous key not found in the schema', async () => { + chai.request(app) + .post('/api/org/range_4/user') + .set(constants.headers) + .send({ + ...body, + test: 'additional key not in schema' + }) + .then((res) => { + expect(res).to.have.status(400) + expect(res.body.message).to.equal('Parameters were invalid') + expect(res.body.errors[0].message).to.equal('must NOT have additional properties') + }) + }) + it('Should return 400 creating a new user with registry enabled and an erroneous key not found in the schema', async () => { + chai.request(app) + .post('/api/registry/org/range_4/user') + .set(constants.headers) + .send({ + ...registryBody, + test: 'additional key not in schema' + }) + .then((res) => { + expect(res).to.have.status(400) + expect(res.body.message).to.equal('Parameters were invalid') + expect(res.body.errors[0].message).to.equal('must NOT have additional properties') + }) + }) }) it('Should default new user status to active when no active/status field is provided', (done) => { const noActiveBody = { ...body, username: 'noActiveUser' } diff --git a/test/integration-tests/user/regularUserUpdateTest.js b/test/integration-tests/user/regularUserUpdateTest.js index 7be104954..32ff85d5a 100644 --- a/test/integration-tests/user/regularUserUpdateTest.js +++ b/test/integration-tests/user/regularUserUpdateTest.js @@ -22,6 +22,7 @@ describe('Regular User Self-Update Tests', () => { expect(res).to.have.status(200) expect(res.body.username).to.equal('jasminesmith@win_5.com') user = res.body + delete user.created_by }) }) it('Should allow regular user to update their own contact info (name)', async () => { diff --git a/test/integration-tests/user/updateUserTest.js b/test/integration-tests/user/updateUserTest.js index da96d74d8..1fe33f07c 100644 --- a/test/integration-tests/user/updateUserTest.js +++ b/test/integration-tests/user/updateUserTest.js @@ -9,7 +9,7 @@ const constants = require('../constants.js') const app = require('../../../src/index.js') describe('Testing Edit user endpoint', () => { - context('User edit tests', () => { + context('Positive Tests', () => { it('Should return 200 when only name changes are done', async () => { await chai.request(app) .put('/api/org/win_5/user/jasminesmith@win_5.com?name.first=NewName') @@ -22,6 +22,7 @@ describe('Testing Edit user endpoint', () => { it('Should return 200 when only name changes are done with registry enabled', async () => { let user await chai.request(app).get('/api/registry/org/win_5/user/jasminesmith@win_5.com').set(constants.nonSecretariatUserHeaders).then((res) => { user = res.body }) + delete user.created_by await chai.request(app) .put('/api/registry/org/win_5/user/jasminesmith@win_5.com') .set(constants.nonSecretariatUserHeaders) @@ -94,6 +95,8 @@ describe('Testing Edit user endpoint', () => { expect(res.body.updated.status).to.equal('active') }) }) + }) + context('Negative Tests', () => { it('Should return an error when admin is required', async () => { await chai.request(app) .put('/api/org/win_5/user/jasminesmith@win_5.com?new_username=NewUsername') @@ -132,6 +135,7 @@ describe('Testing Edit user endpoint', () => { it('Should not allow a first name of more than 100 characters with registry enabled', async () => { let user await chai.request(app).get('/api/registry/org/win_5/user/jasminesmith@win_5.com').set(constants.nonSecretariatUserHeaders).then((res) => { user = res.body }) + delete user.created_by await chai.request(app) .put('/api/registry/org/win_5/user/jasminesmith@win_5.com') .set(constants.nonSecretariatUserHeaders) @@ -160,6 +164,7 @@ describe('Testing Edit user endpoint', () => { it('Should not allow a middle name of more than 100 characters with registry enabled', async () => { let user await chai.request(app).get('/api/registry/org/win_5/user/jasminesmith@win_5.com').set(constants.nonSecretariatUserHeaders).then((res) => { user = res.body }) + delete user.created_by await chai.request(app) .put('/api/registry/org/win_5/user/jasminesmith@win_5.com') .set(constants.nonSecretariatUserHeaders) @@ -188,6 +193,7 @@ describe('Testing Edit user endpoint', () => { it('Should not allow a last name of more than 100 characters with registry enabled', async () => { let user await chai.request(app).get('/api/registry/org/win_5/user/jasminesmith@win_5.com').set(constants.nonSecretariatUserHeaders).then((res) => { user = res.body }) + delete user.created_by await chai.request(app) .put('/api/registry/org/win_5/user/jasminesmith@win_5.com') .set(constants.nonSecretariatUserHeaders) @@ -216,6 +222,7 @@ describe('Testing Edit user endpoint', () => { it('Should not allow a suffix of more than 100 characters with registry enabled', async () => { let user await chai.request(app).get('/api/registry/org/win_5/user/jasminesmith@win_5.com').set(constants.nonSecretariatUserHeaders).then((res) => { user = res.body }) + delete user.created_by await chai.request(app) .put('/api/registry/org/win_5/user/jasminesmith@win_5.com?name.suffix=1:1234567,2:1234567,3:1234567,4:1234567,5:1234567,6:1234567,7:1234567,8:1234567,9:1234567,10:1234567,11:1234567') .set(constants.nonSecretariatUserHeaders) @@ -278,7 +285,6 @@ describe('Testing Edit user endpoint', () => { expect(res.body.error).to.contain('ORG_DNE_PARAM') }) }) - it('Should return 404 when target organization in body does not exist', async () => { const user = constants.headers['CVE-API-USER'] const org = constants.headers['CVE-API-ORG'] @@ -293,5 +299,31 @@ describe('Testing Edit user endpoint', () => { expect(res.body.error).to.contain('ORG_DNE_PARAM') }) }) + it('Should return 400 updating a user with an erroneous key not found in the schema', async () => { + await chai.request(app) + .put('/api/org/win_5/user/jasminesmith@win_5.com?test=testing123') + .set(constants.nonSecretariatUserHeaders) + .then((res, err) => { + expect(res).to.have.status(400) + expect(res.body.message).to.equal('Parameters were invalid') + expect(res.body.details[0].msg).to.equal("'test' is not a valid parameter name.") + }) + }) + it('Should return 400 updating a registry user with an erroneous key not found in the schema', async () => { + const user = constants.headers['CVE-API-USER'] + const org = constants.headers['CVE-API-ORG'] + await chai.request(app) + .put(`/api/registry/org/${org}/user/${user}`) + .set(constants.headers) + .send({ + username: 'user1', + test: 'additional key not in schema' + }) + .then((res) => { + expect(res).to.have.status(400) + expect(res.body.message).to.equal('Parameters were invalid') + expect(res.body.errors[0].message).to.equal('must NOT have additional properties') + }) + }) }) }) From 5134b59cc7bb2fa7accc583934782a9426689428 Mon Sep 17 00:00:00 2001 From: Andrew Foote Date: Wed, 22 Apr 2026 09:20:45 -0400 Subject: [PATCH 32/86] Fixed int tests that broke when merging with dev --- .../registry-org/registryOrgCRUDTest.js | 38 +++++++++---------- test/integration-tests/user/updateUserTest.js | 6 +++ 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/test/integration-tests/registry-org/registryOrgCRUDTest.js b/test/integration-tests/registry-org/registryOrgCRUDTest.js index a69f520a9..9a07abd8c 100644 --- a/test/integration-tests/registry-org/registryOrgCRUDTest.js +++ b/test/integration-tests/registry-org/registryOrgCRUDTest.js @@ -343,27 +343,6 @@ describe('Testing /registryOrg endpoints', () => { .delete(`/api/registryOrg/${subOrg.short_name}`) .set(secretariatHeaders) }) - it('Ignores protected fields such as users and admins during an update', async () => { - const maliciousUsers = ['d41d8cd9-8f00-3204-a980-0998ecf8427e'] - const maliciousAdmins = ['d41d8cd9-8f00-3204-a980-0998ecf8427e'] - - await chai.request(app) - .put(`/api/registryOrg/${createdOrg.short_name}`) - .set(secretariatHeaders) - .send({ - ...createdOrg, - users: maliciousUsers, - admins: maliciousAdmins - }) - .then((res, err) => { - expect(err).to.be.undefined - expect(res).to.have.status(200) - - // Ensure the response body.updated does not contain the malicious data - expect(res.body.updated.users || []).to.not.include(maliciousUsers[0]) - expect(res.body.updated.admins || []).to.not.include(maliciousAdmins[0]) - }) - }) }) context('Negative Tests', () => { it('Fails to update a registry organization that does not exist', async () => { @@ -405,6 +384,23 @@ describe('Testing /registryOrg endpoints', () => { expect(res.body.message).to.equal('Parameters were invalid') }) }) + it('Ignores protected fields such as users and admins during an update', async () => { + const maliciousUsers = ['d41d8cd9-8f00-3204-a980-0998ecf8427e'] + const maliciousAdmins = ['d41d8cd9-8f00-3204-a980-0998ecf8427e'] + + await chai.request(app) + .put(`/api/registryOrg/${createdOrg.short_name}`) + .set(secretariatHeaders) + .send({ + ...createdOrg, + users: maliciousUsers, + admins: maliciousAdmins + }) + .then((res) => { + expect(res).to.have.status(400) + expect(res.body.message).to.equal('Parameters were invalid') + }) + }) it('Fails to update a registry organization with reports_to manually provided', async () => { await chai.request(app) .put(`/api/registryOrg/${createdOrg.short_name}`) diff --git a/test/integration-tests/user/updateUserTest.js b/test/integration-tests/user/updateUserTest.js index 1fe33f07c..3871a4b2a 100644 --- a/test/integration-tests/user/updateUserTest.js +++ b/test/integration-tests/user/updateUserTest.js @@ -42,6 +42,9 @@ describe('Testing Edit user endpoint', () => { let user await chai.request(app).get('/api/registry/org/win_5/user/jasminesmith@win_5.com').set(constants.headers).then((res) => { user = res.body }) + delete user.created + delete user.created_by + delete user.last_updated const maliciousUUID = 'd41d8cd9-8f00-3204-a980-0998ecf8427e' const originalUUID = user.UUID @@ -65,6 +68,9 @@ describe('Testing Edit user endpoint', () => { let user await chai.request(app).get('/api/registry/org/win_5/user/jasminesmith@win_5.com').set(constants.headers).then((res) => { user = res.body }) + delete user.created + delete user.created_by + delete user.last_updated await chai.request(app) .put('/api/registry/org/win_5/user/jasminesmith@win_5.com') .set(constants.headers) From 8cad8f13c2cf33f798edfafe754c5b5f37b77ffb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Apr 2026 15:21:43 +0000 Subject: [PATCH 33/86] Bump path-to-regexp from 0.1.12 to 0.1.13 Bumps [path-to-regexp](https://github.com/pillarjs/path-to-regexp) from 0.1.12 to 0.1.13. - [Release notes](https://github.com/pillarjs/path-to-regexp/releases) - [Changelog](https://github.com/pillarjs/path-to-regexp/blob/v.0.1.13/History.md) - [Commits](https://github.com/pillarjs/path-to-regexp/compare/v0.1.12...v.0.1.13) --- updated-dependencies: - dependency-name: path-to-regexp dependency-version: 0.1.13 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 88c96fde7..227cb2a19 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "cve-services", - "version": "2.7.0", + "version": "2.7.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cve-services", - "version": "2.7.0", + "version": "2.7.1", "license": "(CC0)", "dependencies": { "ajv": "^8.6.2", @@ -6685,9 +6685,9 @@ "license": "MIT" }, "node_modules/path-to-regexp": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", - "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", "license": "MIT" }, "node_modules/path-type": { From 12fab75da1a4937c958a0fa8e3899917a62678eb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Apr 2026 15:21:56 +0000 Subject: [PATCH 34/86] Bump brace-expansion from 1.1.12 to 1.1.13 Bumps [brace-expansion](https://github.com/juliangruber/brace-expansion) from 1.1.12 to 1.1.13. - [Release notes](https://github.com/juliangruber/brace-expansion/releases) - [Commits](https://github.com/juliangruber/brace-expansion/compare/v1.1.12...v1.1.13) --- updated-dependencies: - dependency-name: brace-expansion dependency-version: 1.1.13 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 84 +++++++++++++++++++++++------------------------ 1 file changed, 42 insertions(+), 42 deletions(-) diff --git a/package-lock.json b/package-lock.json index 227cb2a19..39c922f3d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -431,9 +431,9 @@ "license": "Python-2.0" }, "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -512,9 +512,9 @@ } }, "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -1510,9 +1510,9 @@ } }, "node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", "dev": true, "license": "MIT", "dependencies": { @@ -2883,9 +2883,9 @@ } }, "node_modules/eslint-plugin-import/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -2978,9 +2978,9 @@ } }, "node_modules/eslint-plugin-node/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -3126,9 +3126,9 @@ "license": "Python-2.0" }, "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -5846,9 +5846,9 @@ } }, "node_modules/multimatch/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -6116,9 +6116,9 @@ } }, "node_modules/nyc/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -7482,9 +7482,9 @@ } }, "node_modules/replace-in-file/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -7685,9 +7685,9 @@ } }, "node_modules/rimraf/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -8359,9 +8359,9 @@ } }, "node_modules/standard/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -9059,9 +9059,9 @@ } }, "node_modules/swagger-autogen/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -9158,9 +9158,9 @@ } }, "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -9867,9 +9867,9 @@ } }, "node_modules/yamljs/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", From 0642892fc06e8d4147bf6830abe91f7b1e389748 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Tue, 7 Apr 2026 15:15:06 -0400 Subject: [PATCH 35/86] fix high vuln --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 39c922f3d..aa1aceef9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5305,9 +5305,9 @@ } }, "node_modules/lodash": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "license": "MIT" }, "node_modules/lodash.flattendeep": { From eeadd7f40f71998b21120657b82aebd4e0099dbb Mon Sep 17 00:00:00 2001 From: david-rocca Date: Tue, 7 Apr 2026 15:29:45 -0400 Subject: [PATCH 36/86] updating versions --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index aa1aceef9..f590f20c2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "cve-services", - "version": "2.7.1", + "version": "2.7.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cve-services", - "version": "2.7.1", + "version": "2.7.2", "license": "(CC0)", "dependencies": { "ajv": "^8.6.2", From fedb32275f59ffb5904a3c3e743fbdaefe45ddfe Mon Sep 17 00:00:00 2001 From: david-rocca Date: Wed, 29 Apr 2026 12:45:54 -0400 Subject: [PATCH 37/86] Fixing version number conflicts --- api-docs/openapi.json | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/api-docs/openapi.json b/api-docs/openapi.json index 01ab0fad9..eaf2feb02 100644 --- a/api-docs/openapi.json +++ b/api-docs/openapi.json @@ -1,7 +1,7 @@ { "openapi": "3.0.2", "info": { - "version": "2.7.5", + "version": "2.8.0", "title": "CVE Services API", "description": "The CVE Services API supports automation tooling for the CVE Program. Credentials are required for most service endpoints. Representatives of CVE Numbering Authorities (CNAs) should use one of the methods below to obtain credentials:
    • If your organization already has an Organizational Administrator (OA) account for the CVE Services, ask your admin for credentials
    • Contact your Root (Google, INCIBE, JPCERT/CC, or Red Hat) or Top-Level Root (CISA ICS or MITRE) to request credentials

    CVE data is to be in the JSON 5.2 CVE Record format. Details of the JSON 5.2 schema are located here.

    Contact the CVE Services team", "contact": { diff --git a/package-lock.json b/package-lock.json index f590f20c2..6c434b937 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "cve-services", - "version": "2.7.2", + "version": "2.8.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cve-services", - "version": "2.7.2", + "version": "2.8.0", "license": "(CC0)", "dependencies": { "ajv": "^8.6.2", diff --git a/package.json b/package.json index 8e0ec46bb..2b6186b3c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "cve-services", "author": "Automation Working Group", - "version": "2.7.5", + "version": "2.8.0", "license": "(CC0)", "devDependencies": { "@faker-js/faker": "^7.6.0", From e6a06976b6f31cc17b45676aa65f3f5f05e7d8e3 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Wed, 29 Apr 2026 13:39:51 -0400 Subject: [PATCH 38/86] more conflicts --- schemas/glossary/glossary.json | 24 ++ .../list-glossary-items-response.json | 16 + .../glossary.controller.js | 128 +++++++ src/controller/glossary.controller/index.js | 341 ++++++++++++++++++ src/middleware/schemas/ADPOrg.json | 18 + src/middleware/schemas/BaseOrg.json | 158 ++++++++ src/middleware/schemas/BulkDownloadOrg.json | 18 + src/middleware/schemas/CNAOrg.json | 4 + src/middleware/schemas/SecretariatOrg.json | 4 + src/model/glossary.js | 14 + src/repositories/glossaryRepository.js | 26 ++ src/repositories/repositoryFactory.js | 6 + src/routes.config.js | 2 + src/scripts/migrate.js | 11 + src/scripts/populate.js | 8 +- .../glossary/glossaryCRUDTest.js | 156 ++++++++ 16 files changed, 932 insertions(+), 2 deletions(-) create mode 100644 schemas/glossary/glossary.json create mode 100644 schemas/glossary/list-glossary-items-response.json create mode 100644 src/controller/glossary.controller/glossary.controller.js create mode 100644 src/controller/glossary.controller/index.js create mode 100644 src/middleware/schemas/ADPOrg.json create mode 100644 src/middleware/schemas/BaseOrg.json create mode 100644 src/middleware/schemas/BulkDownloadOrg.json create mode 100644 src/model/glossary.js create mode 100644 src/repositories/glossaryRepository.js create mode 100644 test/integration-tests/glossary/glossaryCRUDTest.js diff --git a/schemas/glossary/glossary.json b/schemas/glossary/glossary.json new file mode 100644 index 000000000..0f6bbe633 --- /dev/null +++ b/schemas/glossary/glossary.json @@ -0,0 +1,24 @@ +{ + "$id": "https://cve.mitre.org/api-docs/schema/glossary/glossary.json", + "type": "object", + "properties": { + "short_name": { + "type": "string", + "minLength": 1 + }, + "label": { + "type": "string", + "minLength": 1 + }, + "def": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "short_name", + "label", + "def" + ], + "additionalProperties": false +} diff --git a/schemas/glossary/list-glossary-items-response.json b/schemas/glossary/list-glossary-items-response.json new file mode 100644 index 000000000..fee135e11 --- /dev/null +++ b/schemas/glossary/list-glossary-items-response.json @@ -0,0 +1,16 @@ +{ + "$id": "https://cve.mitre.org/api-docs/schema/glossary/list-glossary-items-response.json", + "type": "object", + "properties": { + "glossary": { + "type": "array", + "items": { + "type": "object" + } + } + }, + "required": [ + "glossary" + ], + "additionalProperties": false +} diff --git a/src/controller/glossary.controller/glossary.controller.js b/src/controller/glossary.controller/glossary.controller.js new file mode 100644 index 000000000..18f0f2a8f --- /dev/null +++ b/src/controller/glossary.controller/glossary.controller.js @@ -0,0 +1,128 @@ +const errors = require('../../utils/error') +const error = new errors.IDRError() + +/** + * Retrieves all glossary items. + * + * @param {Object} req - The Express request object. + * @param {Object} res - The Express response object. + * @param {Function} next - The Express next middleware function. + * @returns {Promise} Returns a JSON response containing an array of all glossary items. + */ +async function getAllGlossaryItems (req, res, next) { + try { + const glossaryRepo = req.ctx.repositories.getGlossaryRepository() + const result = await glossaryRepo.getAll() + return res.status(200).json({ glossary: result }) + } catch (err) { + next(err) + } +} + +/** + * Retrieves a single glossary item by its short name. + * + * @param {Object} req - The Express request object. + * @param {Object} res - The Express response object. + * @param {Function} next - The Express next middleware function. + * @returns {Promise} Returns a JSON response containing the requested glossary item, or a 404 if not found. + */ +async function getGlossaryItem (req, res, next) { + try { + const glossaryRepo = req.ctx.repositories.getGlossaryRepository() + const shortName = req.params.short_name + + const result = await glossaryRepo.findOneByShortName(shortName) + if (!result) { + return res.status(404).json(error.notFound()) + } + return res.status(200).json(result) + } catch (err) { + next(err) + } +} + +/** + * Creates a new glossary item. + * + * @param {Object} req - The Express request object. + * @param {Object} res - The Express response object. + * @param {Function} next - The Express next middleware function. + * @returns {Promise} Returns a JSON response containing the newly created glossary item, or a 400 if it already exists. + */ +async function createGlossaryItem (req, res, next) { + try { + const glossaryRepo = req.ctx.repositories.getGlossaryRepository() + const glossaryData = req.body + + const existing = await glossaryRepo.findOneByShortName(glossaryData.short_name) + if (existing) { + return res.status(400).json(error.badInput(['Glossary item with this short_name already exists'])) + } + + const result = await glossaryRepo.collection.create(glossaryData) + return res.status(200).json(result) + } catch (err) { + next(err) + } +} + +/** + * Updates an existing glossary item by its short name. + * + * @param {Object} req - The Express request object. + * @param {Object} res - The Express response object. + * @param {Function} next - The Express next middleware function. + * @returns {Promise} Returns a JSON response containing the updated glossary item, or a 404 if not found. + */ +async function updateGlossaryItem (req, res, next) { + try { + const glossaryRepo = req.ctx.repositories.getGlossaryRepository() + const shortName = req.params.short_name + const glossaryData = req.body + + if (glossaryData.short_name && glossaryData.short_name !== shortName) { + return res.status(400).json(error.badInput(['Cannot change short_name through this endpoint.'])) + } + + const result = await glossaryRepo.updateByShortName(shortName, glossaryData) + if (!result) { + return res.status(404).json(error.notFound()) + } + + return res.status(200).json(result) + } catch (err) { + next(err) + } +} + +/** + * Deletes an existing glossary item by its short name. + * + * @param {Object} req - The Express request object. + * @param {Object} res - The Express response object. + * @param {Function} next - The Express next middleware function. + * @returns {Promise} Returns a JSON message confirming deletion, or a 404 if not found. + */ +async function deleteGlossaryItem (req, res, next) { + try { + const glossaryRepo = req.ctx.repositories.getGlossaryRepository() + const shortName = req.params.short_name + + const result = await glossaryRepo.deleteByShortName(shortName) + if (!result) { + return res.status(404).json(error.notFound()) + } + return res.status(200).json({ message: 'Glossary item deleted' }) + } catch (err) { + next(err) + } +} + +module.exports = { + getAllGlossaryItems, + getGlossaryItem, + createGlossaryItem, + updateGlossaryItem, + deleteGlossaryItem +} diff --git a/src/controller/glossary.controller/index.js b/src/controller/glossary.controller/index.js new file mode 100644 index 000000000..8d83cc8ca --- /dev/null +++ b/src/controller/glossary.controller/index.js @@ -0,0 +1,341 @@ +const router = require('express').Router() +const controller = require('./glossary.controller') +const mw = require('../../middleware/middleware') + +// Get all glossary items - SEC only +router.get('/glossary', + /* + #swagger.tags = ['Glossary'] + #swagger.operationId = 'glossaryAll' + #swagger.summary = "Retrieves all glossary items (accessible to Secretariat only)" + #swagger.description = " +

    Access Control

    +

    User must belong to an organization with the Secretariat role

    +

    Expected Behavior

    +

    Secretariat: Retrieves all glossary items

    " + #swagger.parameters['$ref'] = [ + '#/components/parameters/apiEntityHeader', + '#/components/parameters/apiUserHeader', + '#/components/parameters/apiSecretHeader' + ] + #swagger.responses[200] = { + description: 'Returns a list of all glossary items', + content: { + "application/json": { + schema: { + $ref: '../schemas/glossary/list-glossary-items-response.json' + } + } + } + } + #swagger.responses[401] = { + description: 'Not Authenticated', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + #swagger.responses[403] = { + description: 'Forbidden', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + #swagger.responses[500] = { + description: 'Internal Server Error', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + */ + mw.validateUser, + mw.onlySecretariat, + controller.getAllGlossaryItems +) + +// Get glossary item by short_name - SEC only +router.get('/glossary/short_name/:short_name', + /* + #swagger.tags = ['Glossary'] + #swagger.operationId = 'glossarySingle' + #swagger.summary = "Retrieves a single glossary item by its short name (accessible to Secretariat only)" + #swagger.description = " +

    Access Control

    +

    User must belong to an organization with the Secretariat role

    +

    Expected Behavior

    +

    Secretariat: Retrieves the specified glossary item

    " + #swagger.parameters['short_name'] = { description: 'The short name of the glossary item' } + #swagger.parameters['$ref'] = [ + '#/components/parameters/apiEntityHeader', + '#/components/parameters/apiUserHeader', + '#/components/parameters/apiSecretHeader' + ] + #swagger.responses[200] = { + description: 'Returns the specified glossary item', + content: { + "application/json": { + schema: { + $ref: '../schemas/glossary/glossary.json' + } + } + } + } + #swagger.responses[401] = { + description: 'Not Authenticated', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + #swagger.responses[403] = { + description: 'Forbidden', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + #swagger.responses[404] = { + description: 'Not Found', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + #swagger.responses[500] = { + description: 'Internal Server Error', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + */ + mw.validateUser, + mw.onlySecretariat, + controller.getGlossaryItem +) + +// Create a glossary item - SEC only +router.post('/glossary', + /* + #swagger.tags = ['Glossary'] + #swagger.operationId = 'glossaryCreate' + #swagger.summary = "Creates a new glossary item (accessible to Secretariat only)" + #swagger.description = " +

    Access Control

    +

    User must belong to an organization with the Secretariat role

    +

    Expected Behavior

    +

    Secretariat: Creates a new glossary item

    " + #swagger.parameters['$ref'] = [ + '#/components/parameters/apiEntityHeader', + '#/components/parameters/apiUserHeader', + '#/components/parameters/apiSecretHeader' + ] + #swagger.requestBody = { + required: true, + content: { + 'application/json': { + schema: { + $ref: '../schemas/glossary/glossary.json' + } + } + } + } + #swagger.responses[200] = { + description: 'Returns the created glossary item', + content: { + "application/json": { + schema: { + $ref: '../schemas/glossary/glossary.json' + } + } + } + } + #swagger.responses[400] = { + description: 'Bad Request', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/bad-request.json' } + } + } + } + #swagger.responses[401] = { + description: 'Not Authenticated', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + #swagger.responses[403] = { + description: 'Forbidden', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + #swagger.responses[500] = { + description: 'Internal Server Error', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + */ + mw.validateUser, + mw.onlySecretariat, + controller.createGlossaryItem +) + +// Update a glossary item - SEC only +router.put('/glossary/short_name/:short_name', + /* + #swagger.tags = ['Glossary'] + #swagger.operationId = 'glossaryUpdate' + #swagger.summary = "Updates an existing glossary item (accessible to Secretariat only)" + #swagger.description = " +

    Access Control

    +

    User must belong to an organization with the Secretariat role

    +

    Expected Behavior

    +

    Secretariat: Updates the specified glossary item

    " + #swagger.parameters['short_name'] = { description: 'The short name of the glossary item' } + #swagger.parameters['$ref'] = [ + '#/components/parameters/apiEntityHeader', + '#/components/parameters/apiUserHeader', + '#/components/parameters/apiSecretHeader' + ] + #swagger.requestBody = { + required: true, + content: { + 'application/json': { + schema: { + $ref: '../schemas/glossary/glossary.json' + } + } + } + } + #swagger.responses[200] = { + description: 'Returns the updated glossary item', + content: { + "application/json": { + schema: { + $ref: '../schemas/glossary/glossary.json' + } + } + } + } + #swagger.responses[400] = { + description: 'Bad Request', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/bad-request.json' } + } + } + } + #swagger.responses[401] = { + description: 'Not Authenticated', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + #swagger.responses[403] = { + description: 'Forbidden', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + #swagger.responses[404] = { + description: 'Not Found', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + #swagger.responses[500] = { + description: 'Internal Server Error', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + */ + mw.validateUser, + mw.onlySecretariat, + controller.updateGlossaryItem +) + +// Delete a glossary item - SEC only +router.delete('/glossary/short_name/:short_name', + /* + #swagger.tags = ['Glossary'] + #swagger.operationId = 'glossaryDelete' + #swagger.summary = "Deletes an existing glossary item (accessible to Secretariat only)" + #swagger.description = " +

    Access Control

    +

    User must belong to an organization with the Secretariat role

    +

    Expected Behavior

    +

    Secretariat: Deletes the specified glossary item

    " + #swagger.parameters['short_name'] = { description: 'The short name of the glossary item' } + #swagger.parameters['$ref'] = [ + '#/components/parameters/apiEntityHeader', + '#/components/parameters/apiUserHeader', + '#/components/parameters/apiSecretHeader' + ] + #swagger.responses[200] = { + description: 'Confirms deletion of the glossary item' + } + #swagger.responses[401] = { + description: 'Not Authenticated', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + #swagger.responses[403] = { + description: 'Forbidden', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + #swagger.responses[404] = { + description: 'Not Found', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + #swagger.responses[500] = { + description: 'Internal Server Error', + content: { + "application/json": { + schema: { $ref: '../schemas/errors/generic.json' } + } + } + } + */ + mw.validateUser, + mw.onlySecretariat, + controller.deleteGlossaryItem +) + +module.exports = router diff --git a/src/middleware/schemas/ADPOrg.json b/src/middleware/schemas/ADPOrg.json new file mode 100644 index 000000000..b5bea44cb --- /dev/null +++ b/src/middleware/schemas/ADPOrg.json @@ -0,0 +1,18 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "ADPOrg", + "type": "object", + "title": "CVE ADP Organization", + "description": "Schema for a CVE ADP Organization", + "allOf": [ + { "$ref": "/BaseOrg" }, + { + "type": "object", + "properties": { + "authority": { + "const": ["ADP"] + } + } + } + ] +} diff --git a/src/middleware/schemas/BaseOrg.json b/src/middleware/schemas/BaseOrg.json new file mode 100644 index 000000000..a87e55fe4 --- /dev/null +++ b/src/middleware/schemas/BaseOrg.json @@ -0,0 +1,158 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "/BaseOrg", + "type": "object", + "title": "CVE Base Organization", + "description": "Base schema for a CVE Organization", + "definitions": { + "uuidType": { + "description": "A version 4 (random) universally unique identifier (UUID) as defined by [RFC 4122](https://tools.ietf.org/html/rfc4122#section-4.1.3).", + "type": "string", + "format": "uuid", + "pattern": "^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$" + }, + "uriType": { + "description": "A universal resource identifier (URI), according to [RFC 3986](https://tools.ietf.org/html/rfc3986).", + "type": "string", + "format": "uri", + "pattern": "^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\\?([^#]*))?(#(.*))?", + "minLength": 1, + "maxLength": 2048 + }, + "shortName": { + "description": "A 2-32 character name that can be used to complement an organization's UUID.", + "type": "string", + "minLength": 2, + "maxLength": 32 + }, + "longName": { + "description": "A 1-256 character name that can be used to complement an organization's short_name.", + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "authority": { + "description": "The authority (role) of this organization within the CVE program", + "type": "string", + "enum": ["CNA", "SECRETARIAT", "BULK_DOWNLOAD", "ADP"] + }, + "discriminator": { + "description": "Discriminator key used by Mongoose for type inheritance", + "type": "string" + }, + "timestamp": { + "description": "Date/time format based on RFC3339 and ISO ISO8601, with an optional timezone in the format 'yyyy-MM-ddTHH:mm:ss[+-]ZH:ZM'. If timezone offset is not given, GMT (+00:00) is assumed.", + "pattern": "^(((2000|2400|2800|(19|2[0-9](0[48]|[2468][048]|[13579][26])))-02-29)|(((19|2[0-9])[0-9]{2})-02-(0[1-9]|1[0-9]|2[0-8]))|(((19|2[0-9])[0-9]{2})-(0[13578]|10|12)-(0[1-9]|[12][0-9]|3[01]))|(((19|2[0-9])[0-9]{2})-(0[469]|11)-(0[1-9]|[12][0-9]|30)))T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})?$", + "type": "string" + } + }, + "properties": { + "UUID": { + "$ref": "#/definitions/uuidType" + }, + "__t": { + "$ref": "#/definitions/discriminator" + }, + "short_name": { + "$ref": "#/definitions/shortName" + }, + "long_name": { + "$ref": "#/definitions/longName" + }, + "aliases": { + "type": "array", + "uniqueItems": true, + "items": { + "type": "string" + } + }, + "authority": { + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "#/definitions/authority" + } + }, + "root_or_tlr": { + "type": "boolean" + }, + "users": { + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "#/definitions/uuidType" + } + }, + "admins": { + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "#/definitions/uuidType" + } + }, + "contact_info": { + "type": "object", + "properties": { + "additional_contact_users": { + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "#/definitions/uuidType" + } + }, + "poc": { + "type": "string" + }, + "poc_email": { + "type": "string", + "format": "email" + }, + "poc_phone": { + "type": "string" + }, + "org_email": { + "type": "string", + "format": "email" + }, + "website": { + "$ref": "#/definitions/uriType", + "type": "string", + "pattern": "^(ftp|http)s?://\\S+$" + } + }, + "additionalProperties": false + }, + "partner_role": { + "type": "string" + }, + "partner_type": { + "type": "string" + }, + "partner_country": { + "type": "string" + }, + "vulnerability_advisory_locations": { + "type": "array", + "uniqueItems": true, + "items": { + "type": "string" + } + }, + "advisory_location_require_credentials": { + "type": "boolean" + }, + "industry": { + "type": "string" + }, + "tl_root_start_date": { + "$ref": "#/definitions/timestamp" + }, + "is_cna_discussion_list": { + "type": "boolean" + } + }, + "required": [ + "short_name", + "long_name" + ] +} \ No newline at end of file diff --git a/src/middleware/schemas/BulkDownloadOrg.json b/src/middleware/schemas/BulkDownloadOrg.json new file mode 100644 index 000000000..768ae1123 --- /dev/null +++ b/src/middleware/schemas/BulkDownloadOrg.json @@ -0,0 +1,18 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "BaseOrg", + "type": "object", + "title": "CVE Bulk Download Organization", + "description": "Schema for a CVE Bulk Download Organization", + "allOf": [ + { "$ref": "/BaseOrg" }, + { + "type": "object", + "properties": { + "authority": { + "const": ["BULK_DOWNLOAD"] + } + } + } + ] +} diff --git a/src/middleware/schemas/CNAOrg.json b/src/middleware/schemas/CNAOrg.json index 5dcb3f3db..21797fe93 100644 --- a/src/middleware/schemas/CNAOrg.json +++ b/src/middleware/schemas/CNAOrg.json @@ -7,6 +7,10 @@ "allOf": [ { "$ref": "/BaseOrg" }, { +<<<<<<< HEAD +======= + "type": "object", +>>>>>>> b62d7ad4 (Conflicts) "properties": { "authority": { "const": ["CNA"] diff --git a/src/middleware/schemas/SecretariatOrg.json b/src/middleware/schemas/SecretariatOrg.json index 125ba92b1..63c452a0b 100644 --- a/src/middleware/schemas/SecretariatOrg.json +++ b/src/middleware/schemas/SecretariatOrg.json @@ -7,6 +7,10 @@ "allOf": [ { "$ref": "/BaseOrg" }, { +<<<<<<< HEAD +======= + "type": "object", +>>>>>>> b62d7ad4 (Conflicts) "properties": { "authority": { "const": ["SECRETARIAT"] diff --git a/src/model/glossary.js b/src/model/glossary.js new file mode 100644 index 000000000..fd9e9a2b5 --- /dev/null +++ b/src/model/glossary.js @@ -0,0 +1,14 @@ +const mongoose = require('mongoose') + +const schema = { + short_name: { type: String, required: true }, + label: { type: String, required: true }, + def: { type: String, required: true } +} + +const GlossarySchema = new mongoose.Schema(schema, { collection: 'Glossary', timestamps: { createdAt: 'createdAt', updatedAt: 'updatedAt' } }) + +GlossarySchema.index({ short_name: 1 }, { unique: true }) + +const Glossary = mongoose.model('Glossary', GlossarySchema) +module.exports = Glossary diff --git a/src/repositories/glossaryRepository.js b/src/repositories/glossaryRepository.js new file mode 100644 index 000000000..934cce610 --- /dev/null +++ b/src/repositories/glossaryRepository.js @@ -0,0 +1,26 @@ +const BaseRepository = require('./baseRepository') +const Glossary = require('../model/glossary') + +class GlossaryRepository extends BaseRepository { + constructor () { + super(Glossary) + } + + async getAll () { + return this.find({}, { multiple: true }) + } + + async findOneByShortName (shortName) { + return this.findOne({ short_name: shortName }) + } + + async updateByShortName (shortName, newGlossaryData) { + return this.findOneAndUpdate({ short_name: shortName }, newGlossaryData) + } + + async deleteByShortName (shortName) { + return this.collection.findOneAndDelete({ short_name: shortName }) + } +} + +module.exports = GlossaryRepository diff --git a/src/repositories/repositoryFactory.js b/src/repositories/repositoryFactory.js index 4750fffea..7f97e1177 100644 --- a/src/repositories/repositoryFactory.js +++ b/src/repositories/repositoryFactory.js @@ -7,6 +7,7 @@ const BaseOrgRepository = require('./baseOrgRepository') const BaseUserRepository = require('./baseUserRepository') const ConversationRepository = require('./conversationRepository') const ReviewObjectRepository = require('./reviewObjectRepository') +const GlossaryRepository = require('./glossaryRepository') class RepositoryFactory { getOrgRepository () { @@ -54,6 +55,11 @@ class RepositoryFactory { return repo } + getGlossaryRepository () { + const repo = new GlossaryRepository() + return repo + } + getAuditRepository () { const AuditRepository = require('./auditRepository') const repo = new AuditRepository() diff --git a/src/routes.config.js b/src/routes.config.js index 1cf2fe159..9cf95cdc3 100644 --- a/src/routes.config.js +++ b/src/routes.config.js @@ -12,6 +12,7 @@ const RegistryOrgController = require('./controller/registry-org.controller') const AuditController = require('./controller/audit.controller') const ConversationController = require('./controller/conversation.controller') const ReviewObjectController = require('./controller/review-object.controller') +const GlossaryController = require('./controller/glossary.controller') var options = { swaggerOptions: { @@ -40,6 +41,7 @@ module.exports = async function configureRoutes (app) { app.use('/api/', RegistryOrgController) app.use('/api/', ConversationController) app.use('/api/', ReviewObjectController) + app.use('/api/', GlossaryController) app.get('/api-docs/openapi.json', (req, res) => res.json(openApiSpecification)) app.use('/api-docs', swaggerUi.serveFiles(null, options), swaggerUi.setup(null, setupOptions)) app.use('/schemas/', SchemasController) diff --git a/src/scripts/migrate.js b/src/scripts/migrate.js index 7f4bd5879..a0097c1d6 100644 --- a/src/scripts/migrate.js +++ b/src/scripts/migrate.js @@ -65,6 +65,7 @@ async function run () { // Each helper handlers querying changes from srcDB and updating trgDB await orgHelper(db) await userHelper(db) + await glossaryHelper(db) } catch (err) { // Ensures that the client will close when you finish/error await dbClient.close() @@ -265,3 +266,13 @@ async function userHelper (db) { await trgUserCol.updateOne(trgQuery, updateDoc, options) } } + +async function glossaryHelper (db) { + console.log('Ensuring Glossary collection exists...') + // Create collection if it doesn't exist + await db.createCollection('Glossary').catch((err) => { + if (err.codeName !== 'NamespaceExists') { + console.warn('Could not create Glossary collection', err) + } + }) +} diff --git a/src/scripts/populate.js b/src/scripts/populate.js index 28fe3d057..c70713938 100644 --- a/src/scripts/populate.js +++ b/src/scripts/populate.js @@ -21,6 +21,7 @@ const BaseUser = require('../model/baseuser') const ReviewObject = require('../model/reviewobject') const Conversation = require('../model/conversation') const Audit = require('../model/audit') +const Glossary = require('../model/glossary') const error = new errors.IDRError() @@ -34,14 +35,16 @@ const populateTheseCollections = { BaseUser: BaseUser, ReviewObject: ReviewObject, Conversation: Conversation, - Audit: Audit + Audit: Audit, + Glossary: Glossary } const indexesToCreate = { Cve: [{ 'cve.cveMetadata.cveId': 1 }, { 'cve.cveMetadata.dateUpdated': 1 }], 'Cve-Id': [{ cve_id: 1 }, { owning_cna: 1, state: 1 }, { reserved: 1 }], User: [{ UUID: 1 }], - Org: [{ UUID: 1 }, { 'authority.active_roles': 1 }] + Org: [{ UUID: 1 }, { 'authority.active_roles': 1 }], + Glossary: [{ short_name: 1 }] } // Body Parser Middleware @@ -143,6 +146,7 @@ db.once('open', async () => { await Audit.createCollection() await ReviewObject.createCollection() await Conversation.createCollection() + await Glossary.createCollection() } catch (err) { logger.error('Error creating indexes:', err) } finally { diff --git a/test/integration-tests/glossary/glossaryCRUDTest.js b/test/integration-tests/glossary/glossaryCRUDTest.js new file mode 100644 index 000000000..dfe65fc87 --- /dev/null +++ b/test/integration-tests/glossary/glossaryCRUDTest.js @@ -0,0 +1,156 @@ +/* eslint-disable no-unused-expressions */ +const chai = require('chai') +const expect = chai.expect +chai.use(require('chai-http')) + +const constants = require('../constants.js') +const app = require('../../../src/index.js') + +const secretariatHeaders = { ...constants.headers, 'content-type': 'application/json' } + +const testGlossaryItem = { + short_name: 'test_glossary_item', + label: 'Test Glossary Item', + def: 'The definition of Test Glossary Item' +} + +describe('Testing /glossary endpoints', () => { + context('Testing POST /glossary endpoint', () => { + context('Positive Tests', () => { + it('Creates a new glossary item', async () => { + await chai.request(app) + .post('/api/glossary') + .set(secretariatHeaders) + .send(testGlossaryItem) + .then((res, err) => { + expect(err).to.be.undefined + expect(res).to.have.status(200) + + expect(res.body).to.haveOwnProperty('short_name') + expect(res.body.short_name).to.equal(testGlossaryItem.short_name) + + expect(res.body).to.haveOwnProperty('label') + expect(res.body.label).to.equal(testGlossaryItem.label) + + expect(res.body).to.haveOwnProperty('def') + expect(res.body.def).to.equal(testGlossaryItem.def) + }) + }) + }) + context('Negative Tests', () => { + it('Fails to create a new glossary item with an existing short name', async () => { + await chai.request(app) + .post('/api/glossary') + .set(secretariatHeaders) + .send(testGlossaryItem) + .then((res) => { + expect(res).to.have.status(400) + expect(res.body.message).to.equal('Parameters were invalid') + expect(res.body.details[0]).to.equal('Glossary item with this short_name already exists') + }) + }) + }) + }) + context('Testing GET /glossary endpoints', () => { + context('Positive Tests', () => { + it('Gets a list of all glossary items', async () => { + await chai.request(app) + .get('/api/glossary') + .set(secretariatHeaders) + .then((res) => { + expect(res).to.have.status(200) + expect(res.body.glossary).to.be.an('array').that.is.not.empty + }) + }) + it('Gets a glossary item by short name', async () => { + await chai.request(app) + .get(`/api/glossary/short_name/${testGlossaryItem.short_name}`) + .set(secretariatHeaders) + .then((res) => { + expect(res).to.have.status(200) + expect(res.body).to.have.property('label', testGlossaryItem.label) + expect(res.body).to.have.property('short_name', testGlossaryItem.short_name) + expect(res.body).to.have.property('def', testGlossaryItem.def) + }) + }) + }) + context('Negative Tests', () => { + it('Fails to get a glossary item that does not exist', async () => { + await chai.request(app) + .get('/api/glossary/short_name/nonexistent_item') + .set(secretariatHeaders) + .then((res) => { + expect(res).to.have.status(404) + expect(res.body.message).to.equal('404: resource not found') + }) + }) + }) + }) + context('Testing PUT /glossary endpoint', () => { + context('Positive Tests', () => { + it('Updates a glossary item', async () => { + await chai.request(app) + .put(`/api/glossary/short_name/${testGlossaryItem.short_name}`) + .set(secretariatHeaders) + .send({ + ...testGlossaryItem, + label: 'Updated Glossary Item', + def: 'Updated definition' + }) + .then((res, err) => { + expect(err).to.be.undefined + expect(res).to.have.status(200) + + expect(res.body).to.haveOwnProperty('short_name') + expect(res.body.short_name).to.equal(testGlossaryItem.short_name) + + expect(res.body).to.haveOwnProperty('label') + expect(res.body.label).to.equal('Updated Glossary Item') + + expect(res.body).to.haveOwnProperty('def') + expect(res.body.def).to.equal('Updated definition') + }) + }) + }) + context('Negative Tests', () => { + it('Fails to update a glossary item changing its short_name', async () => { + await chai.request(app) + .put(`/api/glossary/short_name/${testGlossaryItem.short_name}`) + .set(secretariatHeaders) + .send({ + ...testGlossaryItem, + short_name: 'new_short_name' + }) + .then((res) => { + expect(res).to.have.status(400) + expect(res.body.message).to.equal('Parameters were invalid') + expect(res.body.details[0]).to.equal('Cannot change short_name through this endpoint.') + }) + }) + }) + }) + context('Testing DELETE /glossary endpoint', () => { + context('Positive Tests', () => { + it('Deletes a glossary item', async () => { + await chai.request(app) + .delete(`/api/glossary/short_name/${testGlossaryItem.short_name}`) + .set(secretariatHeaders) + .then((res) => { + expect(res).to.have.status(200) + expect(res.body.message).to.equal('Glossary item deleted') + }) + }) + }) + context('Negative Tests', () => { + it('Fails to delete a glossary item that does not exist', async () => { + await chai.request(app) + .delete('/api/glossary/short_name/nonexistent_item') + .set(secretariatHeaders) + .then((res) => { + expect(res).to.have.status(404) + expect(res.body.message).to.equal('404: resource not found') + }) + }) + }) + }) +}) From 391afeacda3e53d7f12dcd491f70ce6362d211ec Mon Sep 17 00:00:00 2001 From: david-rocca Date: Tue, 7 Apr 2026 14:46:17 -0400 Subject: [PATCH 39/86] Added Glossary capability --- datadump/pre-population/glossary.json | 7 ++ .../create-glossary-item-response.json | 17 +++++ schemas/glossary/glossary.json | 4 +- .../glossary.controller.js | 33 +++++---- src/controller/glossary.controller/index.js | 18 ++--- src/model/glossary.js | 4 +- src/repositories/glossaryRepository.js | 14 ++-- src/scripts/populate.js | 8 ++- .../glossary/glossaryCRUDTest.js | 67 +++++++++++++------ 9 files changed, 119 insertions(+), 53 deletions(-) create mode 100644 datadump/pre-population/glossary.json create mode 100644 schemas/glossary/create-glossary-item-response.json diff --git a/datadump/pre-population/glossary.json b/datadump/pre-population/glossary.json new file mode 100644 index 000000000..ac9ccc4e7 --- /dev/null +++ b/datadump/pre-population/glossary.json @@ -0,0 +1,7 @@ +[ + { + "services_short_name": "long_name", + "label": "Long Name", + "def": "The full, official name of an organization participating in the CVE program." + } +] diff --git a/schemas/glossary/create-glossary-item-response.json b/schemas/glossary/create-glossary-item-response.json new file mode 100644 index 000000000..d4c4b2c38 --- /dev/null +++ b/schemas/glossary/create-glossary-item-response.json @@ -0,0 +1,17 @@ +{ + "$id": "https://cve.mitre.org/api-docs/schema/glossary/create-glossary-item-response.json", + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "glossary_item_added": { + "$ref": "glossary.json" + } + }, + "required": [ + "message", + "glossary_item_added" + ], + "additionalProperties": false +} diff --git a/schemas/glossary/glossary.json b/schemas/glossary/glossary.json index 0f6bbe633..d5e59f8b0 100644 --- a/schemas/glossary/glossary.json +++ b/schemas/glossary/glossary.json @@ -2,7 +2,7 @@ "$id": "https://cve.mitre.org/api-docs/schema/glossary/glossary.json", "type": "object", "properties": { - "short_name": { + "services_short_name": { "type": "string", "minLength": 1 }, @@ -16,7 +16,7 @@ } }, "required": [ - "short_name", + "services_short_name", "label", "def" ], diff --git a/src/controller/glossary.controller/glossary.controller.js b/src/controller/glossary.controller/glossary.controller.js index 18f0f2a8f..29410395c 100644 --- a/src/controller/glossary.controller/glossary.controller.js +++ b/src/controller/glossary.controller/glossary.controller.js @@ -30,9 +30,9 @@ async function getAllGlossaryItems (req, res, next) { async function getGlossaryItem (req, res, next) { try { const glossaryRepo = req.ctx.repositories.getGlossaryRepository() - const shortName = req.params.short_name + const servicesShortName = req.params.services_short_name - const result = await glossaryRepo.findOneByShortName(shortName) + const result = await glossaryRepo.findOneByServicesShortName(servicesShortName) if (!result) { return res.status(404).json(error.notFound()) } @@ -55,13 +55,22 @@ async function createGlossaryItem (req, res, next) { const glossaryRepo = req.ctx.repositories.getGlossaryRepository() const glossaryData = req.body - const existing = await glossaryRepo.findOneByShortName(glossaryData.short_name) + const existing = await glossaryRepo.findOneByServicesShortName(glossaryData.services_short_name) if (existing) { - return res.status(400).json(error.badInput(['Glossary item with this short_name already exists'])) + return res.status(400).json(error.badInput(['Glossary item with this services_short_name already exists'])) } - const result = await glossaryRepo.collection.create(glossaryData) - return res.status(200).json(result) + const createdDoc = await glossaryRepo.collection.create(glossaryData) + const result = createdDoc.toObject() + delete result._id + delete result.__v + delete result.createdAt + delete result.updatedAt + + return res.status(200).json({ + message: 'glossary item successfully added', + glossary_item_added: result + }) } catch (err) { next(err) } @@ -78,14 +87,14 @@ async function createGlossaryItem (req, res, next) { async function updateGlossaryItem (req, res, next) { try { const glossaryRepo = req.ctx.repositories.getGlossaryRepository() - const shortName = req.params.short_name + const servicesShortName = req.params.services_short_name const glossaryData = req.body - if (glossaryData.short_name && glossaryData.short_name !== shortName) { - return res.status(400).json(error.badInput(['Cannot change short_name through this endpoint.'])) + if (glossaryData.services_short_name && glossaryData.services_short_name !== servicesShortName) { + return res.status(400).json(error.badInput(['Cannot change services_short_name through this endpoint.'])) } - const result = await glossaryRepo.updateByShortName(shortName, glossaryData) + const result = await glossaryRepo.updateByServicesShortName(servicesShortName, glossaryData) if (!result) { return res.status(404).json(error.notFound()) } @@ -107,9 +116,9 @@ async function updateGlossaryItem (req, res, next) { async function deleteGlossaryItem (req, res, next) { try { const glossaryRepo = req.ctx.repositories.getGlossaryRepository() - const shortName = req.params.short_name + const servicesShortName = req.params.services_short_name - const result = await glossaryRepo.deleteByShortName(shortName) + const result = await glossaryRepo.deleteByServicesShortName(servicesShortName) if (!result) { return res.status(404).json(error.notFound()) } diff --git a/src/controller/glossary.controller/index.js b/src/controller/glossary.controller/index.js index 8d83cc8ca..d510651de 100644 --- a/src/controller/glossary.controller/index.js +++ b/src/controller/glossary.controller/index.js @@ -58,8 +58,8 @@ router.get('/glossary', controller.getAllGlossaryItems ) -// Get glossary item by short_name - SEC only -router.get('/glossary/short_name/:short_name', +// Get glossary item by services_short_name - SEC only +router.get('/glossary/:services_short_name', /* #swagger.tags = ['Glossary'] #swagger.operationId = 'glossarySingle' @@ -69,7 +69,7 @@ router.get('/glossary/short_name/:short_name',

    User must belong to an organization with the Secretariat role

    Expected Behavior

    Secretariat: Retrieves the specified glossary item

    " - #swagger.parameters['short_name'] = { description: 'The short name of the glossary item' } + #swagger.parameters['services_short_name'] = { description: 'The short name of the glossary item' } #swagger.parameters['$ref'] = [ '#/components/parameters/apiEntityHeader', '#/components/parameters/apiUserHeader', @@ -150,11 +150,11 @@ router.post('/glossary', } } #swagger.responses[200] = { - description: 'Returns the created glossary item', + description: 'Returns the created glossary item wrapped in a success message', content: { "application/json": { schema: { - $ref: '../schemas/glossary/glossary.json' + $ref: '../schemas/glossary/create-glossary-item-response.json' } } } @@ -198,7 +198,7 @@ router.post('/glossary', ) // Update a glossary item - SEC only -router.put('/glossary/short_name/:short_name', +router.put('/glossary/:services_short_name', /* #swagger.tags = ['Glossary'] #swagger.operationId = 'glossaryUpdate' @@ -208,7 +208,7 @@ router.put('/glossary/short_name/:short_name',

    User must belong to an organization with the Secretariat role

    Expected Behavior

    Secretariat: Updates the specified glossary item

    " - #swagger.parameters['short_name'] = { description: 'The short name of the glossary item' } + #swagger.parameters['services_short_name'] = { description: 'The short name of the glossary item' } #swagger.parameters['$ref'] = [ '#/components/parameters/apiEntityHeader', '#/components/parameters/apiUserHeader', @@ -281,7 +281,7 @@ router.put('/glossary/short_name/:short_name', ) // Delete a glossary item - SEC only -router.delete('/glossary/short_name/:short_name', +router.delete('/glossary/:services_short_name', /* #swagger.tags = ['Glossary'] #swagger.operationId = 'glossaryDelete' @@ -291,7 +291,7 @@ router.delete('/glossary/short_name/:short_name',

    User must belong to an organization with the Secretariat role

    Expected Behavior

    Secretariat: Deletes the specified glossary item

    " - #swagger.parameters['short_name'] = { description: 'The short name of the glossary item' } + #swagger.parameters['services_short_name'] = { description: 'The short name of the glossary item' } #swagger.parameters['$ref'] = [ '#/components/parameters/apiEntityHeader', '#/components/parameters/apiUserHeader', diff --git a/src/model/glossary.js b/src/model/glossary.js index fd9e9a2b5..d06f09a61 100644 --- a/src/model/glossary.js +++ b/src/model/glossary.js @@ -1,14 +1,14 @@ const mongoose = require('mongoose') const schema = { - short_name: { type: String, required: true }, + services_short_name: { type: String, required: true }, label: { type: String, required: true }, def: { type: String, required: true } } const GlossarySchema = new mongoose.Schema(schema, { collection: 'Glossary', timestamps: { createdAt: 'createdAt', updatedAt: 'updatedAt' } }) -GlossarySchema.index({ short_name: 1 }, { unique: true }) +GlossarySchema.index({ services_short_name: 1 }, { unique: true }) const Glossary = mongoose.model('Glossary', GlossarySchema) module.exports = Glossary diff --git a/src/repositories/glossaryRepository.js b/src/repositories/glossaryRepository.js index 934cce610..3ff74abe4 100644 --- a/src/repositories/glossaryRepository.js +++ b/src/repositories/glossaryRepository.js @@ -7,19 +7,19 @@ class GlossaryRepository extends BaseRepository { } async getAll () { - return this.find({}, { multiple: true }) + return this.collection.find({}, { _id: 0, __v: 0, createdAt: 0, updatedAt: 0 }).exec() } - async findOneByShortName (shortName) { - return this.findOne({ short_name: shortName }) + async findOneByServicesShortName (servicesShortName) { + return this.collection.findOne({ services_short_name: servicesShortName }, { _id: 0, __v: 0, createdAt: 0, updatedAt: 0 }).exec() } - async updateByShortName (shortName, newGlossaryData) { - return this.findOneAndUpdate({ short_name: shortName }, newGlossaryData) + async updateByServicesShortName (servicesShortName, newGlossaryData) { + return this.collection.findOneAndUpdate({ services_short_name: servicesShortName }, newGlossaryData, { projection: { _id: 0, __v: 0, createdAt: 0, updatedAt: 0 }, new: true }).exec() } - async deleteByShortName (shortName) { - return this.collection.findOneAndDelete({ short_name: shortName }) + async deleteByServicesShortName (servicesShortName) { + return this.collection.findOneAndDelete({ services_short_name: servicesShortName }, { projection: { _id: 0, __v: 0, createdAt: 0, updatedAt: 0 } }).exec() } } diff --git a/src/scripts/populate.js b/src/scripts/populate.js index c70713938..b92659901 100644 --- a/src/scripts/populate.js +++ b/src/scripts/populate.js @@ -44,7 +44,7 @@ const indexesToCreate = { 'Cve-Id': [{ cve_id: 1 }, { owning_cna: 1, state: 1 }, { reserved: 1 }], User: [{ UUID: 1 }], Org: [{ UUID: 1 }, { 'authority.active_roles': 1 }], - Glossary: [{ short_name: 1 }] + Glossary: [{ services_short_name: 1 }] } // Body Parser Middleware @@ -125,6 +125,12 @@ db.once('open', async () => { CveId, dataUtils.newCveIdTransform )) + // Glossary + populatePromises.push(dataUtils.populateCollection( + './datadump/pre-population/glossary.json', + Glossary + )) + // don't close database connection until all remaining populate // promises are resolved Promise.all(populatePromises).then(async function () { diff --git a/test/integration-tests/glossary/glossaryCRUDTest.js b/test/integration-tests/glossary/glossaryCRUDTest.js index dfe65fc87..921fc3c10 100644 --- a/test/integration-tests/glossary/glossaryCRUDTest.js +++ b/test/integration-tests/glossary/glossaryCRUDTest.js @@ -9,7 +9,7 @@ const app = require('../../../src/index.js') const secretariatHeaders = { ...constants.headers, 'content-type': 'application/json' } const testGlossaryItem = { - short_name: 'test_glossary_item', + services_short_name: 'test_glossary_item', label: 'Test Glossary Item', def: 'The definition of Test Glossary Item' } @@ -26,14 +26,25 @@ describe('Testing /glossary endpoints', () => { expect(err).to.be.undefined expect(res).to.have.status(200) - expect(res.body).to.haveOwnProperty('short_name') - expect(res.body.short_name).to.equal(testGlossaryItem.short_name) + expect(res.body).to.haveOwnProperty('message') + expect(res.body.message).to.equal('glossary item successfully added') - expect(res.body).to.haveOwnProperty('label') - expect(res.body.label).to.equal(testGlossaryItem.label) + expect(res.body).to.haveOwnProperty('glossary_item_added') + const item = res.body.glossary_item_added - expect(res.body).to.haveOwnProperty('def') - expect(res.body.def).to.equal(testGlossaryItem.def) + expect(item).to.haveOwnProperty('services_short_name') + expect(item.services_short_name).to.equal(testGlossaryItem.services_short_name) + + expect(item).to.haveOwnProperty('label') + expect(item.label).to.equal(testGlossaryItem.label) + + expect(item).to.haveOwnProperty('def') + expect(item.def).to.equal(testGlossaryItem.def) + + expect(item).to.not.have.property('_id') + expect(item).to.not.have.property('__v') + expect(item).to.not.have.property('createdAt') + expect(item).to.not.have.property('updatedAt') }) }) }) @@ -46,7 +57,7 @@ describe('Testing /glossary endpoints', () => { .then((res) => { expect(res).to.have.status(400) expect(res.body.message).to.equal('Parameters were invalid') - expect(res.body.details[0]).to.equal('Glossary item with this short_name already exists') + expect(res.body.details[0]).to.equal('Glossary item with this services_short_name already exists') }) }) }) @@ -60,24 +71,35 @@ describe('Testing /glossary endpoints', () => { .then((res) => { expect(res).to.have.status(200) expect(res.body.glossary).to.be.an('array').that.is.not.empty + res.body.glossary.forEach(item => { + expect(item).to.not.have.property('_id') + expect(item).to.not.have.property('__v') + expect(item).to.not.have.property('createdAt') + expect(item).to.not.have.property('updatedAt') + }) }) }) it('Gets a glossary item by short name', async () => { await chai.request(app) - .get(`/api/glossary/short_name/${testGlossaryItem.short_name}`) + .get(`/api/glossary/${testGlossaryItem.services_short_name}`) .set(secretariatHeaders) .then((res) => { expect(res).to.have.status(200) expect(res.body).to.have.property('label', testGlossaryItem.label) - expect(res.body).to.have.property('short_name', testGlossaryItem.short_name) + expect(res.body).to.have.property('services_short_name', testGlossaryItem.services_short_name) expect(res.body).to.have.property('def', testGlossaryItem.def) + + expect(res.body).to.not.have.property('_id') + expect(res.body).to.not.have.property('__v') + expect(res.body).to.not.have.property('createdAt') + expect(res.body).to.not.have.property('updatedAt') }) }) }) context('Negative Tests', () => { it('Fails to get a glossary item that does not exist', async () => { await chai.request(app) - .get('/api/glossary/short_name/nonexistent_item') + .get('/api/glossary/nonexistent_item') .set(secretariatHeaders) .then((res) => { expect(res).to.have.status(404) @@ -90,7 +112,7 @@ describe('Testing /glossary endpoints', () => { context('Positive Tests', () => { it('Updates a glossary item', async () => { await chai.request(app) - .put(`/api/glossary/short_name/${testGlossaryItem.short_name}`) + .put(`/api/glossary/${testGlossaryItem.services_short_name}`) .set(secretariatHeaders) .send({ ...testGlossaryItem, @@ -101,30 +123,35 @@ describe('Testing /glossary endpoints', () => { expect(err).to.be.undefined expect(res).to.have.status(200) - expect(res.body).to.haveOwnProperty('short_name') - expect(res.body.short_name).to.equal(testGlossaryItem.short_name) + expect(res.body).to.haveOwnProperty('services_short_name') + expect(res.body.services_short_name).to.equal(testGlossaryItem.services_short_name) expect(res.body).to.haveOwnProperty('label') expect(res.body.label).to.equal('Updated Glossary Item') expect(res.body).to.haveOwnProperty('def') expect(res.body.def).to.equal('Updated definition') + + expect(res.body).to.not.have.property('_id') + expect(res.body).to.not.have.property('__v') + expect(res.body).to.not.have.property('createdAt') + expect(res.body).to.not.have.property('updatedAt') }) }) }) context('Negative Tests', () => { - it('Fails to update a glossary item changing its short_name', async () => { + it('Fails to update a glossary item changing its services_short_name', async () => { await chai.request(app) - .put(`/api/glossary/short_name/${testGlossaryItem.short_name}`) + .put(`/api/glossary/${testGlossaryItem.services_short_name}`) .set(secretariatHeaders) .send({ ...testGlossaryItem, - short_name: 'new_short_name' + services_short_name: 'new_services_short_name' }) .then((res) => { expect(res).to.have.status(400) expect(res.body.message).to.equal('Parameters were invalid') - expect(res.body.details[0]).to.equal('Cannot change short_name through this endpoint.') + expect(res.body.details[0]).to.equal('Cannot change services_short_name through this endpoint.') }) }) }) @@ -133,7 +160,7 @@ describe('Testing /glossary endpoints', () => { context('Positive Tests', () => { it('Deletes a glossary item', async () => { await chai.request(app) - .delete(`/api/glossary/short_name/${testGlossaryItem.short_name}`) + .delete(`/api/glossary/${testGlossaryItem.services_short_name}`) .set(secretariatHeaders) .then((res) => { expect(res).to.have.status(200) @@ -144,7 +171,7 @@ describe('Testing /glossary endpoints', () => { context('Negative Tests', () => { it('Fails to delete a glossary item that does not exist', async () => { await chai.request(app) - .delete('/api/glossary/short_name/nonexistent_item') + .delete('/api/glossary/nonexistent_item') .set(secretariatHeaders) .then((res) => { expect(res).to.have.status(404) From a3bcd32ec1d8b4e7065be3d1c7d653a5df1adcdb Mon Sep 17 00:00:00 2001 From: david-rocca Date: Tue, 7 Apr 2026 14:56:00 -0400 Subject: [PATCH 40/86] Make the returns a little bit better --- .../create-glossary-item-response.json | 4 +-- .../update-glossary-item-response.json | 17 +++++++++++ .../glossary.controller.js | 7 +++-- src/controller/glossary.controller/index.js | 4 +-- .../glossary/glossaryCRUDTest.js | 30 +++++++++++-------- 5 files changed, 44 insertions(+), 18 deletions(-) create mode 100644 schemas/glossary/update-glossary-item-response.json diff --git a/schemas/glossary/create-glossary-item-response.json b/schemas/glossary/create-glossary-item-response.json index d4c4b2c38..c4d5a2394 100644 --- a/schemas/glossary/create-glossary-item-response.json +++ b/schemas/glossary/create-glossary-item-response.json @@ -5,13 +5,13 @@ "message": { "type": "string" }, - "glossary_item_added": { + "created": { "$ref": "glossary.json" } }, "required": [ "message", - "glossary_item_added" + "created" ], "additionalProperties": false } diff --git a/schemas/glossary/update-glossary-item-response.json b/schemas/glossary/update-glossary-item-response.json new file mode 100644 index 000000000..64bc27db6 --- /dev/null +++ b/schemas/glossary/update-glossary-item-response.json @@ -0,0 +1,17 @@ +{ + "$id": "https://cve.mitre.org/api-docs/schema/glossary/update-glossary-item-response.json", + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "updated": { + "$ref": "glossary.json" + } + }, + "required": [ + "message", + "updated" + ], + "additionalProperties": false +} diff --git a/src/controller/glossary.controller/glossary.controller.js b/src/controller/glossary.controller/glossary.controller.js index 29410395c..b55d65850 100644 --- a/src/controller/glossary.controller/glossary.controller.js +++ b/src/controller/glossary.controller/glossary.controller.js @@ -69,7 +69,7 @@ async function createGlossaryItem (req, res, next) { return res.status(200).json({ message: 'glossary item successfully added', - glossary_item_added: result + created: result }) } catch (err) { next(err) @@ -99,7 +99,10 @@ async function updateGlossaryItem (req, res, next) { return res.status(404).json(error.notFound()) } - return res.status(200).json(result) + return res.status(200).json({ + message: 'glossary item successfully updated', + updated: result + }) } catch (err) { next(err) } diff --git a/src/controller/glossary.controller/index.js b/src/controller/glossary.controller/index.js index d510651de..8c374131b 100644 --- a/src/controller/glossary.controller/index.js +++ b/src/controller/glossary.controller/index.js @@ -225,11 +225,11 @@ router.put('/glossary/:services_short_name', } } #swagger.responses[200] = { - description: 'Returns the updated glossary item', + description: 'Returns the updated glossary item wrapped in a success message', content: { "application/json": { schema: { - $ref: '../schemas/glossary/glossary.json' + $ref: '../schemas/glossary/update-glossary-item-response.json' } } } diff --git a/test/integration-tests/glossary/glossaryCRUDTest.js b/test/integration-tests/glossary/glossaryCRUDTest.js index 921fc3c10..844f4b4c2 100644 --- a/test/integration-tests/glossary/glossaryCRUDTest.js +++ b/test/integration-tests/glossary/glossaryCRUDTest.js @@ -29,8 +29,8 @@ describe('Testing /glossary endpoints', () => { expect(res.body).to.haveOwnProperty('message') expect(res.body.message).to.equal('glossary item successfully added') - expect(res.body).to.haveOwnProperty('glossary_item_added') - const item = res.body.glossary_item_added + expect(res.body).to.haveOwnProperty('created') + const item = res.body.created expect(item).to.haveOwnProperty('services_short_name') expect(item.services_short_name).to.equal(testGlossaryItem.services_short_name) @@ -123,19 +123,25 @@ describe('Testing /glossary endpoints', () => { expect(err).to.be.undefined expect(res).to.have.status(200) - expect(res.body).to.haveOwnProperty('services_short_name') - expect(res.body.services_short_name).to.equal(testGlossaryItem.services_short_name) + expect(res.body).to.haveOwnProperty('message') + expect(res.body.message).to.equal('glossary item successfully updated') - expect(res.body).to.haveOwnProperty('label') - expect(res.body.label).to.equal('Updated Glossary Item') + expect(res.body).to.haveOwnProperty('updated') + const item = res.body.updated - expect(res.body).to.haveOwnProperty('def') - expect(res.body.def).to.equal('Updated definition') + expect(item).to.haveOwnProperty('services_short_name') + expect(item.services_short_name).to.equal(testGlossaryItem.services_short_name) - expect(res.body).to.not.have.property('_id') - expect(res.body).to.not.have.property('__v') - expect(res.body).to.not.have.property('createdAt') - expect(res.body).to.not.have.property('updatedAt') + expect(item).to.haveOwnProperty('label') + expect(item.label).to.equal('Updated Glossary Item') + + expect(item).to.haveOwnProperty('def') + expect(item.def).to.equal('Updated definition') + + expect(item).to.not.have.property('_id') + expect(item).to.not.have.property('__v') + expect(item).to.not.have.property('createdAt') + expect(item).to.not.have.property('updatedAt') }) }) }) From c711ce7e25b164854f929fdff554f6ea5d1676d9 Mon Sep 17 00:00:00 2001 From: david-rocca Date: Wed, 29 Apr 2026 12:46:53 -0400 Subject: [PATCH 41/86] Unlock for development --- src/controller/org.controller/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controller/org.controller/index.js b/src/controller/org.controller/index.js index f7805c496..1c5e6f110 100644 --- a/src/controller/org.controller/index.js +++ b/src/controller/org.controller/index.js @@ -640,7 +640,7 @@ router.put('/registry/org/:shortname', */ mw.useRegistry(), mw.validateUser, - mw.onlySecretariat, + // mw.onlySecretariat, parseError, parsePutParams, registryOrgController.UPDATE_ORG From 75525511138677edec41c36f74ffa941a3865797 Mon Sep 17 00:00:00 2001 From: Andrew Foote Date: Thu, 23 Apr 2026 12:44:43 -0400 Subject: [PATCH 42/86] Updated error handling on getOrg() to properly separate 404 responses from 500s, when something is actually broken --- .../org.controller/org.controller.js | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/controller/org.controller/org.controller.js b/src/controller/org.controller/org.controller.js index 297887707..b44bd89a0 100644 --- a/src/controller/org.controller/org.controller.js +++ b/src/controller/org.controller/org.controller.js @@ -61,22 +61,30 @@ async function getOrg (req, res, next) { try { const requesterOrg = await repo.findOneByShortName(requesterOrgShortName, {}, returnLegacyFormat) + + if (!requesterOrg) { + return res.status(404).json(error.orgDne(requesterOrgShortName, 'requesterOrgShortName', 'header')) + } + const requesterOrgIdentifier = identifierIsUUID ? requesterOrg.UUID : requesterOrgShortName const isSecretariat = await repo.isSecretariat(requesterOrg, {}, returnLegacyFormat) if (requesterOrgIdentifier !== identifier && !isSecretariat) { - logger.info({ uuid: req.ctx.uuid, message: identifier + ' organization can only be viewed by the users of the same organization or the Secretariat.' }) + logger.info({ uuid: req.ctx.uuid, message: identifier + ' organization can only be viewed by same-org users or Secretariat.' }) return res.status(403).json(error.notSameOrgOrSecretariat()) } returnValue = await repo.getOrg(identifier, identifierIsUUID, {}, returnLegacyFormat) - } catch (error) { - // Handle the specific error thrown by BaseOrgRepository.createOrg - if (error.message && error.message.includes('Unknown Org type requested')) { - return res.status(400).json({ message: error.message }) + } catch (err) { + if (err.message && err.message.includes('Unknown Org type requested')) { + return res.status(400).json({ message: err.message }) } + + logger.error({ uuid: req.ctx.uuid, message: 'Internal Server Error', error: err.stack }) + return res.status(500).json(error.internal()) } - if (!returnValue) { // an empty result can only happen if the requestor is the Secretariat + + if (!returnValue) { logger.info({ uuid: req.ctx.uuid, message: identifier + ' organization does not exist.' }) return res.status(404).json(error.orgDne(identifier, 'identifier', 'path')) } From 695233b7b405b7c696da61b12242f0b6f920e72a Mon Sep 17 00:00:00 2001 From: Andrew Foote Date: Thu, 23 Apr 2026 12:55:18 -0400 Subject: [PATCH 43/86] Adding context to ensure error handling is clear --- src/controller/org.controller/org.controller.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/controller/org.controller/org.controller.js b/src/controller/org.controller/org.controller.js index b44bd89a0..b562bae9c 100644 --- a/src/controller/org.controller/org.controller.js +++ b/src/controller/org.controller/org.controller.js @@ -62,6 +62,7 @@ async function getOrg (req, res, next) { try { const requesterOrg = await repo.findOneByShortName(requesterOrgShortName, {}, returnLegacyFormat) + // Ensure requester org exists if (!requesterOrg) { return res.status(404).json(error.orgDne(requesterOrgShortName, 'requesterOrgShortName', 'header')) } @@ -69,6 +70,7 @@ async function getOrg (req, res, next) { const requesterOrgIdentifier = identifierIsUUID ? requesterOrg.UUID : requesterOrgShortName const isSecretariat = await repo.isSecretariat(requesterOrg, {}, returnLegacyFormat) + // Ensure that if the requester is not Secretariat, they can't view orgs other than their own if (requesterOrgIdentifier !== identifier && !isSecretariat) { logger.info({ uuid: req.ctx.uuid, message: identifier + ' organization can only be viewed by same-org users or Secretariat.' }) return res.status(403).json(error.notSameOrgOrSecretariat()) @@ -76,14 +78,17 @@ async function getOrg (req, res, next) { returnValue = await repo.getOrg(identifier, identifierIsUUID, {}, returnLegacyFormat) } catch (err) { + // Handle the specific error thrown by BaseOrgRepository.getOrg if (err.message && err.message.includes('Unknown Org type requested')) { return res.status(400).json({ message: err.message }) } + // Handle database / network errors logger.error({ uuid: req.ctx.uuid, message: 'Internal Server Error', error: err.stack }) - return res.status(500).json(error.internal()) + return res.status(500).json(error.internal()) } + // Handle the error where the org can't be found if (!returnValue) { logger.info({ uuid: req.ctx.uuid, message: identifier + ' organization does not exist.' }) return res.status(404).json(error.orgDne(identifier, 'identifier', 'path')) From 3558b9434519eee20afc581f10aaadbf4d9221e5 Mon Sep 17 00:00:00 2001 From: Andrew Foote Date: Thu, 23 Apr 2026 14:21:19 -0400 Subject: [PATCH 44/86] Added back in parseError in registryUser routing, added tests to ensure parseError is working as expected --- .../registry-user.controller/index.js | 12 ++-- .../registry-user.middleware.js | 18 +++++- .../registry-user/registryUserCRUDTest.js | 64 +++++++++++++++++++ 3 files changed, 87 insertions(+), 7 deletions(-) create mode 100644 test/integration-tests/registry-user/registryUserCRUDTest.js diff --git a/src/controller/registry-user.controller/index.js b/src/controller/registry-user.controller/index.js index 872c6fe11..4bb60022f 100644 --- a/src/controller/registry-user.controller/index.js +++ b/src/controller/registry-user.controller/index.js @@ -3,7 +3,7 @@ const router = express.Router() const mw = require('../../middleware/middleware') const { param, query } = require('express-validator') const controller = require('./registry-user.controller') -const { parseGetParams, parsePostParams, parseDeleteParams } = require('./registry-user.middleware') +const { parseGetParams, parsePostParams, parseDeleteParams, parseError } = require('./registry-user.middleware') const getConstants = require('../../constants').getConstants const CONSTANTS = getConstants() @@ -69,7 +69,7 @@ router.get('/registryUser', mw.onlySecretariat, query(['page']).optional().isInt({ min: CONSTANTS.PAGINATOR_PAGE }), query(['page']).custom((val) => { return mw.containsNoInvalidCharacters(val) }), - // parseError, + parseError, parseGetParams, controller.ALL_USERS ) @@ -140,7 +140,7 @@ router.get('/registryUser/:identifier', mw.validateUser, mw.onlySecretariat, param(['identifier']).isString().trim(), - // parseError, + parseError, parseGetParams, controller.SINGLE_USER ) @@ -212,6 +212,8 @@ router.post('/registryUser/:shortname', */ mw.validateUser, mw.onlySecretariat, + param(['shortname']).isString().trim(), + parseError, parsePostParams, controller.CREATE_USER ) @@ -299,7 +301,7 @@ router.put('/registryUser/:identifier', mw.onlySecretariat, param(['identifier']).isString().trim(), // TODO: do more validation here - // parseError, + parseError, parsePostParams, controller.UPDATE_USER ) @@ -387,7 +389,7 @@ router.delete( mw.validateUser, mw.onlySecretariat, param(['identifier']).isString().trim(), - // parseError, + parseError, parseDeleteParams, controller.DELETE_USER ) diff --git a/src/controller/registry-user.controller/registry-user.middleware.js b/src/controller/registry-user.controller/registry-user.middleware.js index 6b30b69e0..e39b721c0 100644 --- a/src/controller/registry-user.controller/registry-user.middleware.js +++ b/src/controller/registry-user.controller/registry-user.middleware.js @@ -1,8 +1,11 @@ const utils = require('../../utils/utils') +const { validationResult } = require('express-validator') +const errors = require('../registry-org.controller/error') +const error = new errors.RegistryOrgControllerError() function parsePostParams (req, res, next) { utils.reqCtxMapping(req, 'body', []) - utils.reqCtxMapping(req, 'params', ['identifier']) + utils.reqCtxMapping(req, 'params', ['identifier', 'shortname']) utils.reqCtxMapping(req, 'query', [ 'new_username', 'name.first', 'name.last', 'name.middle', 'name.suffix', @@ -23,8 +26,19 @@ function parseDeleteParams (req, res, next) { next() } +function parseError (req, res, next) { + const err = validationResult(req).formatWith(({ location, msg, param, value, nestedErrors }) => { + return { msg: msg, param: param, location: location } + }) + if (!err.isEmpty()) { + return res.status(400).json(error.badInput(err.array())) + } + next() +} + module.exports = { parsePostParams, parseGetParams, - parseDeleteParams + parseDeleteParams, + parseError } diff --git a/test/integration-tests/registry-user/registryUserCRUDTest.js b/test/integration-tests/registry-user/registryUserCRUDTest.js new file mode 100644 index 000000000..84a76d36d --- /dev/null +++ b/test/integration-tests/registry-user/registryUserCRUDTest.js @@ -0,0 +1,64 @@ +const chai = require('chai') +const expect = chai.expect +chai.use(require('chai-http')) + +const constants = require('../constants.js') +const app = require('../../../src/index.js') + +const secretariatHeaders = { ...constants.headers, 'content-type': 'application/json' } + +describe('Testing /registryUser endpoints', () => { + context('Positive Tests', () => { + // TODO + }) + context('Negative Tests', () => { + it('Fails when page query parameter is not an integer', async () => { + await chai.request(app) + .get('/api/registryUser') + .set(secretariatHeaders) // Must be secretariat to reach validation + .query({ page: 'not-a-number' }) // Invalid data + .then((res) => { + expect(res).to.have.status(400) + expect(res.body.message).to.equal('Parameters were invalid') + }) + }) + + it('Fails when page query parameter is below the minimum', async () => { + await chai.request(app) + .get('/api/registryUser') + .set(secretariatHeaders) + .query({ page: 0 }) // Assuming min is 1 + .then((res) => { + expect(res).to.have.status(400) + }) + }) + + it('Fails when identifier contains invalid characters', async () => { + await chai.request(app) + .get('/api/registryUser/uuid