From 959a2c6accb2d952325ccd3500544d6d23ec8f8b Mon Sep 17 00:00:00 2001 From: Andrew Foote Date: Mon, 20 Apr 2026 16:13:16 -0400 Subject: [PATCH 01/18] 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 02/18] 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 03/18] 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 04/18] 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 05/18] 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 06/18] 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 07/18] 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 08/18] 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 09/18] 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 10/18] 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 11/18] 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 12/18] 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 13/18] 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 14/18] 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 15/18] 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