From f0c457e9bddfcd13c7037bd68f4f1556248ffe84 Mon Sep 17 00:00:00 2001 From: Chris Breeden <47154440+breedenc@users.noreply.github.com> Date: Mon, 18 Dec 2023 15:47:17 -0800 Subject: [PATCH 1/3] Bugfixes: email migration and user deletion (#81) * add debug logs to authorizer lambda * break out of alias loops after alias is found * hard delete user services when deleting user * add audit logging for user account events that occur automatically upon login * change AuditLogger events from positional to keyword args * improve mocking in unit tests to accommodate updates to tested functions --- .../api/authorizer/authorizer/handlers.py | 55 ++++- .../api/authorizer/tests/test_get_user.py | 81 ++++++- backend/lambdas/api/users/users/delete.py | 3 + .../artemislib/artemislib/audit/logger.py | 209 +++++++++++++----- 4 files changed, 277 insertions(+), 71 deletions(-) diff --git a/backend/lambdas/api/authorizer/authorizer/handlers.py b/backend/lambdas/api/authorizer/authorizer/handlers.py index 8cd41aad..f3b869e2 100644 --- a/backend/lambdas/api/authorizer/authorizer/handlers.py +++ b/backend/lambdas/api/authorizer/authorizer/handlers.py @@ -112,7 +112,9 @@ def process_user_auth(event): _verify_claims(claims) # Get the user - user = _get_update_or_create_user(email=claims["email"].lower()) + user = _get_update_or_create_user( + email=claims["email"].lower(), source_ip=event["requestContext"]["identity"]["sourceIp"] + ) if not user: raise Exception("Unauthorized") @@ -172,7 +174,7 @@ def _verify_claims(claims: dict): @transaction.atomic -def _get_update_or_create_user(email: str) -> User: +def _get_update_or_create_user(email: str, source_ip: str) -> User: """ Attempt to get a user based on email. If user does not exist, try to match based on EMAIL_DOMAIN_ALIASES, and update email if successful. @@ -183,15 +185,21 @@ def _get_update_or_create_user(email: str) -> User: # If user is soft-deleted, return None so that auth fails # this should never occur in practice because user email is modified with a suffix of "_DELETED_{timestamp}" at deletion, but it is ok to retain this check if user and user.deleted: + LOG.debug(f"User found by email {email}, but is deleted") return None # if user is not found by email, check for aliases if not user and EMAIL_DOMAIN_ALIASES: + LOG.debug(f"User not found by email {email}, checking for aliases") email_local_part = email.split("@")[0] email_domain = email.split("@")[1] # Check if any aliases exist for domain, and attempt to find a match for alias in EMAIL_DOMAIN_ALIASES: + # if user was found in a previous iteration, break the loop + if user and not user.deleted: + break + if alias["new_domain"] == email_domain: for old_domain in alias["old_domains"]: if alias.get("email_transformation"): @@ -204,23 +212,51 @@ def _get_update_or_create_user(email: str) -> User: else: old_email = f"{email_local_part}@{old_domain}" + LOG.debug(f"Attempting to get user with possible alias {old_email}") user = _get_user(old_email) # If a user is found with an old email (and was not soft-deleted), update the email and return user if user and not user.deleted: + LOG.debug(f"User account discovered with alias {old_email}") + LOG.debug(f"Attempting to update user email from {old_email} to {email}") user.email = email user.save() + audit_log = AuditLogger(principal=old_email, source_ip=source_ip) + audit_log.user_modified( + user=user.email, scope=user.scope, features=user.features, admin=user.admin + ) + LOG.debug(f"User account email updated to {user.email}") + + # since user was successfully found and updated, break out of inner loop + break + # if user is found directly or via alias (and was not soft-deleted), ensure that user's self group is named correctly, then return user if user and not user.deleted: + LOG.debug("Checking that email and self group name match") if user.self_group.name != user.email: + LOG.debug(f"Self group {user.self_group.name} does not match email {user.email}") + LOG.debug(f"Attempting to update self group name to {user.email}") user.self_group.name = user.email user.self_group.save() + audit_log = AuditLogger(principal=user.email, source_ip=source_ip) + audit_log.group_modified( + group_id=str(user.self_group.group_id), + name=user.self_group.name, + scope=user.self_group.scope, + features=user.self_group.features, + admin=user.self_group.admin, + allowlist=user.self_group.allowlist, + ) + LOG.debug(f"Self group name updated to {user.self_group.name}") + + LOG.debug(f"Returning user with email {user.email} and self group {user.self_group.name}") return user # Create the user since no match has been found at this point - return _create_user(email) + LOG.debug(f"No matching user discovered. Creating a new user with email {email}") + return _create_user(email, source_ip) def _update_login_timestamp(user: User) -> None: @@ -231,7 +267,7 @@ def _update_login_timestamp(user: User) -> None: user.save() -def _create_user(email: str) -> User: +def _create_user(email: str, source_ip: str) -> User: """ Create a new user """ @@ -240,6 +276,17 @@ def _create_user(email: str) -> User: # User was created so create their self group as well Group.create_self_group(user) + audit_log = AuditLogger(principal=email, source_ip=source_ip) + audit_log.user_created(user=user.email, scope=user.scope, features=user.features, admin=user.admin) + audit_log.group_created( + group_id=str(user.self_group.group_id), + name=user.self_group.name, + scope=user.self_group.scope, + features=user.self_group.features, + admin=user.self_group.admin, + allowlist=user.self_group.allowlist, + ) + return user diff --git a/backend/lambdas/api/authorizer/tests/test_get_user.py b/backend/lambdas/api/authorizer/tests/test_get_user.py index 8daea677..ec67d2d7 100644 --- a/backend/lambdas/api/authorizer/tests/test_get_user.py +++ b/backend/lambdas/api/authorizer/tests/test_get_user.py @@ -20,6 +20,11 @@ "old_domains": ["company.com"], "email_transformation": {"new_email_regex": "_", "old_email_expr": "."}, }, + { + "new_domain": "company5.com", + "old_domains": ["company6.com", "company6andsuffix.com"], + "email_transformation": {"new_email_regex": "[.]", "old_email_expr": "_"}, + }, {"new_domain": "newcompany.com", "old_domains": ["company.com"]}, ] @@ -60,12 +65,33 @@ "last_login": "2023-01-01 00:00:00.000000+00:00", "self_group": {"name": "first_last@company3.com"}, }, + { + "id": 6, + "email": "first_last@company6.com", + "deleted": False, + "last_login": "2023-01-01 00:00:00.000000+00:00", + "self_group": {"name": "first_last@company6.com"}, + }, + { + "id": 7, + "email": "first2_last2@company6andsuffix.com", + "deleted": False, + "last_login": "2023-01-01 00:00:00.000000+00:00", + "self_group": {"name": "first2_last2@company6andsuffix.com"}, + }, ] +EVENT_IP = "0.0.0.0" + class MockGroup(object): def __init__(self, **kwargs): self.name = kwargs.get("name") or "" + self.group_id = kwargs.get("group_id") or "" + self.scope = kwargs.get("scope") or [] + self.features = kwargs.get("features") or {} + self.admin = kwargs.get("admin") or False + self.allowlist = kwargs.get("allowlist") or False def save(self): pass @@ -88,6 +114,9 @@ def __init__(self, **kwargs): self_group = kwargs.get("self_group") or None if self_group: self.self_group = MockGroup(name=self_group.get("name")) + self.scope = kwargs.get("scope") or [] + self.features = kwargs.get("features") or {} + self.admin = kwargs.get("admin") or False def save(self): for user in MockUser.users: @@ -118,6 +147,10 @@ def get(email: str, **kwargs): _get_update_or_create_user = authorizer.handlers._get_update_or_create_user.__wrapped__ +@patch("authorizer.handlers.AuditLogger.group_created", lambda *x, **y: None) +@patch("authorizer.handlers.AuditLogger.group_modified", lambda *x, **y: None) +@patch("authorizer.handlers.AuditLogger.user_created", lambda *x, **y: None) +@patch("authorizer.handlers.AuditLogger.user_modified", lambda *x, **y: None) @patch("authorizer.handlers.EMAIL_DOMAIN_ALIASES", EMAIL_DOMAIN_ALIASES) @patch("authorizer.handlers.User", MockUser) @patch("authorizer.handlers.Group", MockGroup) @@ -128,7 +161,7 @@ def test_get_existing_user(self): """ MockUser.users = copy.deepcopy(USERS) email = "first.last@company.com" - user = _get_update_or_create_user(email=email) + user = _get_update_or_create_user(email=email, source_ip=EVENT_IP) self.assertTrue( user.__dict__.get("id") == 1 and user.__dict__.get("email") == email @@ -141,7 +174,7 @@ def test_get_deleted_user(self): """ MockUser.users = copy.deepcopy(USERS) email = "first.last.1@company.com" - user = _get_update_or_create_user(email=email) + user = _get_update_or_create_user(email=email, source_ip=EVENT_IP) self.assertTrue(user == None) def test_get_nonexistent_user(self): @@ -151,7 +184,7 @@ def test_get_nonexistent_user(self): MockUser.users = copy.deepcopy(USERS) email = "first.last@doesnotexist.com" expected_userid = MockUser.users[-1].get("id") + 1 - user = _get_update_or_create_user(email=email) + user = _get_update_or_create_user(email=email, source_ip=EVENT_IP) self.assertTrue( user.__dict__.get("id") == expected_userid and user.__dict__.get("email") == email @@ -165,7 +198,7 @@ def test_get_user_with_new_email(self): """ MockUser.users = copy.deepcopy(USERS) email = "first.last@newcompany.com" - user = _get_update_or_create_user(email=email) + user = _get_update_or_create_user(email=email, source_ip=EVENT_IP) self.assertTrue( user.__dict__.get("id") == 1 and user.__dict__.get("email") == email @@ -179,7 +212,7 @@ def test_get_user_with_new_email_with_transformation(self): """ MockUser.users = copy.deepcopy(USERS) email = "first.last@company1.com" - user = _get_update_or_create_user(email=email) + user = _get_update_or_create_user(email=email, source_ip=EVENT_IP) self.assertTrue( user.__dict__.get("id") == 2 and user.__dict__.get("email") == email @@ -193,7 +226,7 @@ def test_get_user_with_new_email_with_transformation2(self): """ MockUser.users = copy.deepcopy(USERS) email = "first.last@company2.com" - user = _get_update_or_create_user(email=email) + user = _get_update_or_create_user(email=email, source_ip=EVENT_IP) self.assertTrue( user.__dict__.get("id") == 4 and user.__dict__.get("email") == email @@ -207,7 +240,7 @@ def test_get_user_with_new_email_with_transformation3(self): """ MockUser.users = copy.deepcopy(USERS) email = "first_last@company3.com" - user = _get_update_or_create_user(email=email) + user = _get_update_or_create_user(email=email, source_ip=EVENT_IP) self.assertTrue( user.__dict__.get("id") == 1 and user.__dict__.get("email") == email @@ -221,7 +254,7 @@ def test_get_user_with_email_and_self_group_mismatch(self): """ MockUser.users = copy.deepcopy(USERS) email = "first.last@company4.com" - user = _get_update_or_create_user(email=email) + user = _get_update_or_create_user(email=email, source_ip=EVENT_IP) self.assertTrue( user.__dict__.get("id") == 5 and user.__dict__.get("email") == email @@ -236,9 +269,39 @@ def test_get_user_with_new_email_and_deleted_old_user(self): MockUser.users = copy.deepcopy(USERS) email = "first.last.1@newcompany.com" expected_userid = MockUser.users[-1].get("id") + 1 - user = _get_update_or_create_user(email=email) + user = _get_update_or_create_user(email=email, source_ip=EVENT_IP) self.assertTrue( user.__dict__.get("id") == expected_userid and user.__dict__.get("email") == email and user.__dict__.get("self_group").name == email ) + + def test_get_user_with_new_email_and_multiple_old_domains_first_domain(self): + """ + User logs in with email "first.last@company5.com" and has an existing acount with email "first_last@company6.com" + Existing account is found, and email and self group name are updated to the new email "first.last@company5.com" + This is distinct from other tests because there are multiple old domains mapped to new domain company5.com + """ + MockUser.users = copy.deepcopy(USERS) + email = "first.last@company5.com" + user = _get_update_or_create_user(email=email, source_ip=EVENT_IP) + self.assertTrue( + user.__dict__.get("id") == 6 + and user.__dict__.get("email") == email + and user.__dict__.get("self_group").name == email + ) + + def test_get_user_with_new_email_and_multiple_old_domains_second_domain(self): + """ + User logs in with email "first2.last2@company5.com" and has an existing acount with email "first2_last2@company6andsuffix.com" + Existing account is found, and email and self group name are updated to the new email "first.last@company5.com" + This is distinct from other tests because there are multiple old domains mapped to new domain company5.com + """ + MockUser.users = copy.deepcopy(USERS) + email = "first2.last2@company5.com" + user = _get_update_or_create_user(email=email, source_ip=EVENT_IP) + self.assertTrue( + user.__dict__.get("id") == 7 + and user.__dict__.get("email") == email + and user.__dict__.get("self_group").name == email + ) diff --git a/backend/lambdas/api/users/users/delete.py b/backend/lambdas/api/users/users/delete.py index 082cec5e..287ff3eb 100644 --- a/backend/lambdas/api/users/users/delete.py +++ b/backend/lambdas/api/users/users/delete.py @@ -54,6 +54,9 @@ def delete(event, email=None, admin: bool = False): # Hard delete all of the user's self group API keys user.self_group.apikey_set.all().delete() + # Hard delete user's services + user.userservice_set.all().delete() + # This is outside the transaction so that if the transaction rolled back we don't log the audit event if user.deleted: audit_log = AuditLogger(principal=email, source_ip=event["requestContext"]["identity"]["sourceIp"]) diff --git a/backend/libs/artemislib/artemislib/audit/logger.py b/backend/libs/artemislib/artemislib/audit/logger.py index 0a425ed2..c208d240 100644 --- a/backend/libs/artemislib/artemislib/audit/logger.py +++ b/backend/libs/artemislib/artemislib/audit/logger.py @@ -43,21 +43,45 @@ def _queue_event(self, event): # User audit events def user_login(self) -> None: - self._queue_event(UserAuditEvent(self.principal, self.source_ip, Action.LOGIN)) + self._queue_event(UserAuditEvent(principal=self.principal, source_ip=self.source_ip, action=Action.LOGIN)) def user_created(self, user: str, scope: list[str], features: dict, admin: bool) -> None: - self._queue_event(UserAuditEvent(self.principal, self.source_ip, Action.CREATED, user, scope, features, admin)) + self._queue_event( + UserAuditEvent( + principal=self.principal, + source_ip=self.source_ip, + action=Action.CREATED, + user=user, + scope=scope, + features=features, + admin=admin, + ) + ) def user_modified(self, user: str, scope: list[str] = None, features: dict = None, admin: bool = None) -> None: - self._queue_event(UserAuditEvent(self.principal, self.source_ip, Action.MODIFIED, user, scope, features, admin)) + self._queue_event( + UserAuditEvent( + principal=self.principal, + source_ip=self.source_ip, + action=Action.MODIFIED, + user=user, + scope=scope, + features=features, + admin=admin, + ) + ) def user_deleted(self, user: str) -> None: - self._queue_event(UserAuditEvent(self.principal, self.source_ip, Action.DELETED, user)) + self._queue_event( + UserAuditEvent(principal=self.principal, source_ip=self.source_ip, action=Action.DELETED, user=user) + ) # API key audit events def key_login(self, key_id: str) -> None: - self._queue_event(APIKeyAuditEvent(self.principal, self.source_ip, Action.LOGIN, key_id)) + self._queue_event( + APIKeyAuditEvent(principal=self.principal, source_ip=self.source_ip, action=Action.LOGIN, key_id=key_id) + ) def key_created( self, @@ -71,21 +95,23 @@ def key_created( ) -> None: self._queue_event( APIKeyAuditEvent( - self.principal, - self.source_ip, - Action.CREATED, - key_id, - scope, - features, - admin, - expires, - group_id, - group_name, + principal=self.principal, + source_ip=self.source_ip, + action=Action.CREATED, + key_id=key_id, + scope=scope, + features=features, + admin=admin, + expires=expires, + group_id=group_id, + group_name=group_name, ) ) def key_deleted(self, key_id: str) -> None: - self._queue_event(APIKeyAuditEvent(self.principal, self.source_ip, Action.DELETED, key_id)) + self._queue_event( + APIKeyAuditEvent(principal=self.principal, source_ip=self.source_ip, action=Action.DELETED, key_id=key_id) + ) # AllowList audit events @@ -94,17 +120,17 @@ def al_created( ) -> None: self._queue_event( AllowListAuditEvent( - self.principal, - self.source_ip, - Action.CREATED, - al_id, - service, - repo, - type, - expires, - value, - reason, - severity, + principal=self.principal, + source_ip=self.source_ip, + action=Action.CREATED, + al_id=al_id, + service=service, + repo=repo, + type=type, + expires=expires, + value=value, + reason=reason, + severity=severity, ) ) @@ -113,17 +139,17 @@ def al_modified( ) -> None: self._queue_event( AllowListAuditEvent( - self.principal, - self.source_ip, - Action.MODIFIED, - al_id, - service, - repo, - type, - expires, - value, - reason, - severity, + principal=self.principal, + source_ip=self.source_ip, + action=Action.MODIFIED, + al_id=al_id, + service=service, + repo=repo, + type=type, + expires=expires, + value=value, + reason=reason, + severity=severity, ) ) @@ -132,17 +158,17 @@ def al_deleted( ) -> None: self._queue_event( AllowListAuditEvent( - self.principal, - self.source_ip, - Action.DELETED, - al_id, - service, - repo, - type, - expires, - value, - reason, - severity, + principal=self.principal, + source_ip=self.source_ip, + action=Action.DELETED, + al_id=al_id, + service=service, + repo=repo, + type=type, + expires=expires, + value=value, + reason=reason, + severity=severity, ) ) @@ -150,17 +176,41 @@ def al_deleted( def sal_created(self, al_id: str, type: str, value: dict, reason: str) -> None: self._queue_event( - AllowListAuditEvent(self.principal, self.source_ip, Action.CREATED, al_id, type, value, reason) + AllowListAuditEvent( + principal=self.principal, + source_ip=self.source_ip, + action=Action.CREATED, + al_id=al_id, + type=type, + value=value, + reason=reason, + ) ) def sal_modified(self, al_id: str, type: str, value: dict, reason: str) -> None: self._queue_event( - AllowListAuditEvent(self.principal, self.source_ip, Action.MODIFIED, al_id, type, value, reason) + AllowListAuditEvent( + principal=self.principal, + source_ip=self.source_ip, + action=Action.MODIFIED, + al_id=al_id, + type=type, + value=value, + reason=reason, + ) ) def sal_deleted(self, al_id: str, type: str, value: dict, reason: str) -> None: self._queue_event( - AllowListAuditEvent(self.principal, self.source_ip, Action.DELETED, al_id, type, value, reason) + AllowListAuditEvent( + principal=self.principal, + source_ip=self.source_ip, + action=Action.DELETED, + al_id=al_id, + type=type, + value=value, + reason=reason, + ) ) # Group audit events @@ -170,7 +220,15 @@ def group_created( ) -> None: self._queue_event( GroupAuditEvent( - self.principal, self.source_ip, Action.CREATED, group_id, name, scope, features, admin, allowlist + principal=self.principal, + source_ip=self.source_ip, + action=Action.CREATED, + group_id=group_id, + name=name, + scope=scope, + features=features, + admin=admin, + allowlist=allowlist, ) ) @@ -179,26 +237,61 @@ def group_modified( ) -> None: self._queue_event( GroupAuditEvent( - self.principal, self.source_ip, Action.MODIFIED, group_id, name, scope, features, admin, allowlist + principal=self.principal, + source_ip=self.source_ip, + action=Action.MODIFIED, + group_id=group_id, + name=name, + scope=scope, + features=features, + admin=admin, + allowlist=allowlist, ) ) def group_deleted(self, group_id: str, name: str) -> None: - self._queue_event(GroupAuditEvent(self.principal, self.source_ip, Action.DELETED, group_id, name)) + self._queue_event( + GroupAuditEvent( + principal=self.principal, source_ip=self.source_ip, action=Action.DELETED, group_id=group_id, name=name + ) + ) # Group membership audit events def group_member_added(self, user_id: str, group_id: str, name: str, admin: bool) -> None: self._queue_event( - GroupMemberAuditEvent(self.principal, self.source_ip, Action.CREATED, user_id, group_id, name, admin) + GroupMemberAuditEvent( + principal=self.principal, + source_ip=self.source_ip, + action=Action.CREATED, + user_id=user_id, + group_id=group_id, + name=name, + admin=admin, + ) ) def group_member_modified(self, user_id: str, group_id: str, name: str, admin: bool) -> None: self._queue_event( - GroupMemberAuditEvent(self.principal, self.source_ip, Action.MODIFIED, user_id, group_id, name, admin) + GroupMemberAuditEvent( + principal=self.principal, + source_ip=self.source_ip, + action=Action.MODIFIED, + user_id=user_id, + group_id=group_id, + name=name, + admin=admin, + ) ) def group_member_removed(self, user_id: str, group_id: str, name: str) -> None: self._queue_event( - GroupMemberAuditEvent(self.principal, self.source_ip, Action.DELETED, user_id, group_id, name) + GroupMemberAuditEvent( + principal=self.principal, + source_ip=self.source_ip, + action=Action.DELETED, + user_id=user_id, + group_id=group_id, + name=name, + ) ) From d1bdc12eafbced49af519cbcc09ea1f7beb74ba3 Mon Sep 17 00:00:00 2001 From: David Merrill <13295452+davakos@users.noreply.github.com> Date: Tue, 19 Dec 2023 16:35:42 -0500 Subject: [PATCH 2/3] UI: SBOM allow id or license_id field (#82) UI: SBOM allow id or license_id fields & Update dependencies --- .github/PULL_REQUEST_TEMPLATE.md | 1 + .github/workflows/test_backend.yml | 2 +- .github/workflows/test_orchestrator.yml | 2 +- .github/workflows/test_ui.yml | 6 +++--- ui/.nvmrc | 2 +- ui/Dockerfile.dev | 8 +++---- ui/package-lock.json | 16 +++++++------- ui/package.json | 2 +- ui/src/api/server.ts | 10 ++++++--- ui/src/features/scans/scansSchemas.ts | 10 +++++---- ui/src/locale/en/messages.po | 28 ++++++++++++------------- 11 files changed, 46 insertions(+), 41 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 2bfd078e..d321ffb7 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -17,6 +17,7 @@ - [ ] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) +- [ ] Documentation change ## Checklist diff --git a/.github/workflows/test_backend.yml b/.github/workflows/test_backend.yml index edc326fd..b75bb652 100644 --- a/.github/workflows/test_backend.yml +++ b/.github/workflows/test_backend.yml @@ -9,7 +9,7 @@ jobs: test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # actions/checkout@v3.5.2 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # actions/checkout@v4.1.1 - uses: actions/setup-python@41b7212b1668f5de9d65e9c82aa777e6bbedb3a8 # actions/setup-python@v2.1.4 with: python-version: "3.9" diff --git a/.github/workflows/test_orchestrator.yml b/.github/workflows/test_orchestrator.yml index 3c8b039a..ae8875be 100644 --- a/.github/workflows/test_orchestrator.yml +++ b/.github/workflows/test_orchestrator.yml @@ -9,7 +9,7 @@ jobs: test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # actions/checkout@v3.5.2 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # actions/checkout@v4.1.1 - name: setup python environment uses: actions/setup-python@41b7212b1668f5de9d65e9c82aa777e6bbedb3a8 # actions/setup-python@v2.1.4 with: diff --git a/.github/workflows/test_ui.yml b/.github/workflows/test_ui.yml index 7869d28f..e5d8f9c1 100644 --- a/.github/workflows/test_ui.yml +++ b/.github/workflows/test_ui.yml @@ -11,14 +11,14 @@ jobs: env: UI_PATH: ./ui HADOLINT_URL: https://github.com/hadolint/hadolint/releases/download/v2.10.0/hadolint-Linux-x86_64 - NPM_VERSION: 9 + NPM_VERSION: 10 steps: - name: Checkout the code - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # actions/checkout@v3.5.2 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # actions/checkout@v4.1.1 - name: Setup node from node version file - uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # actions/setup-node@v3.6.0 + uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # actions/setup-node@v4.0.1 with: node-version-file: "${{ env.UI_PATH }}/.nvmrc" diff --git a/ui/.nvmrc b/ui/.nvmrc index 6d80269a..d5a15960 100644 --- a/ui/.nvmrc +++ b/ui/.nvmrc @@ -1 +1 @@ -18.16.0 +20.10.0 diff --git a/ui/Dockerfile.dev b/ui/Dockerfile.dev index 63e31e91..f55fd8ef 100644 --- a/ui/Dockerfile.dev +++ b/ui/Dockerfile.dev @@ -1,7 +1,7 @@ # Note: This Dockerfile creates an image intended ONLY for local development and testing # it may contain unnessary source code and artifacts that are NOT intended for deployment -FROM node:18-bullseye-slim +FROM node:20-bookworm-slim WORKDIR /app @@ -16,9 +16,7 @@ COPY package*.json hadolint.sha512 ./ # it doesn't include a file checksum, so use one we've generated to validate the package # hadolint ignore=DL3008 RUN apt-get update && \ - grep security /etc/apt/sources.list > /etc/apt/security.sources.list && \ apt-get upgrade -y && \ - apt-get upgrade -y -o Dir::Etc::Sourcelist=/etc/apt/security.sources.list && \ apt-get install -y --no-install-recommends jq wget ca-certificates make && \ wget --quiet \ https://github.com/hadolint/hadolint/releases/download/v2.10.0/hadolint-Linux-x86_64 \ @@ -29,9 +27,9 @@ RUN apt-get update && \ rm -rf /var/lib/apt/lists/* # hadolint ignore=DL3059 -RUN npm install -g npm@^9.6.7 && \ +RUN npm install -g npm@^10.2.5 && \ npm ci && \ - npm install -g serve@^14.2.0 && \ + npm install -g serve@^14.2.1 && \ npm ls -a COPY . . diff --git a/ui/package-lock.json b/ui/package-lock.json index 97ff8539..b84062b4 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -1,12 +1,12 @@ { "name": "artemis-ui", - "version": "1.14.0", + "version": "1.15.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "artemis-ui", - "version": "1.14.0", + "version": "1.15.0", "license": "MIT", "dependencies": { "@date-io/luxon": "^2.16.1", @@ -7247,9 +7247,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001481", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001481.tgz", - "integrity": "sha512-KCqHwRnaa1InZBtqXzP98LPg0ajCVujMKjqKDhZEthIpAsJl/YEIa3YvXjGXPVqzZVguccuu7ga9KOE1J9rKPQ==", + "version": "1.0.30001570", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001570.tgz", + "integrity": "sha512-+3e0ASu4sw1SWaoCtvPeyXp+5PsjigkSt8OXZbF9StH5pQWbxEjLAZE3n8Aup5udop1uRiKA7a4utUk/uoSpUw==", "dev": true, "funding": [ { @@ -25858,9 +25858,9 @@ } }, "caniuse-lite": { - "version": "1.0.30001481", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001481.tgz", - "integrity": "sha512-KCqHwRnaa1InZBtqXzP98LPg0ajCVujMKjqKDhZEthIpAsJl/YEIa3YvXjGXPVqzZVguccuu7ga9KOE1J9rKPQ==", + "version": "1.0.30001570", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001570.tgz", + "integrity": "sha512-+3e0ASu4sw1SWaoCtvPeyXp+5PsjigkSt8OXZbF9StH5pQWbxEjLAZE3n8Aup5udop1uRiKA7a4utUk/uoSpUw==", "dev": true }, "case-sensitive-paths-webpack-plugin": { diff --git a/ui/package.json b/ui/package.json index d8eb2ea6..c6bf09fb 100644 --- a/ui/package.json +++ b/ui/package.json @@ -1,7 +1,7 @@ { "name": "artemis-ui", "description": "Web UI for Artemis", - "version": "1.14.0", + "version": "1.15.0", "author": "WMCSO AppSec Team ", "contributors": [], "license": "MIT", diff --git a/ui/src/api/server.ts b/ui/src/api/server.ts index 885070cc..02811d4c 100644 --- a/ui/src/api/server.ts +++ b/ui/src/api/server.ts @@ -2945,7 +2945,7 @@ export function makeServer() { { name: "component1", version: "0.1.0", - licenses: [{ id: "apache", name: "Apache" }], + licenses: [{ license_id: "apache", name: "Apache" }], source: "node/module/package.json", deps: [ { @@ -2971,7 +2971,9 @@ export function makeServer() { { name: "component11", version: "1.2.3", - licenses: [{ id: "unlicensed", name: "Unlicensed" }], + licenses: [ + { license_id: "unlicensed", name: "Unlicensed" }, + ], source: "node/module/package.json", deps: [], }, @@ -2992,7 +2994,9 @@ export function makeServer() { { name: "component14", version: "0.0.1", - licenses: [{ id: "bsd", name: "BSD 3-Clause" }], + licenses: [ + { license_id: "bsd", name: "BSD 3-Clause" }, + ], source: "node/module/package.json", deps: [], }, diff --git a/ui/src/features/scans/scansSchemas.ts b/ui/src/features/scans/scansSchemas.ts index f7e8f43b..7ecb8ee9 100644 --- a/ui/src/features/scans/scansSchemas.ts +++ b/ui/src/features/scans/scansSchemas.ts @@ -3,9 +3,9 @@ import { t } from "@lingui/macro"; import * as Yup from "yup"; import { PagedResponse, Response, responseSchema } from "api/apiSchemas"; -import { SPLIT_MULTILINE_CN_REGEX } from "utils/formatters"; -import { User } from "features/users/usersSchemas"; import { AppMeta, appMetaSchema } from "custom/scanMetaSchemas"; +import { User } from "features/users/usersSchemas"; +import { SPLIT_MULTILINE_CN_REGEX } from "utils/formatters"; // interfaces export interface ScanFormLocationState { @@ -161,7 +161,8 @@ export interface ResultsConfiguration { } interface SbomLicense { - id: string; + id?: string; + license_id?: string; name: string; } @@ -351,7 +352,8 @@ const scanInventorySchema: Yup.ObjectSchema = Yup.object() const sbomLicenseSchema: Yup.ObjectSchema = Yup.object() .shape({ - id: Yup.string().defined(), + id: Yup.string(), + license_id: Yup.string(), name: Yup.string().defined(), }) .defined(); diff --git a/ui/src/locale/en/messages.po b/ui/src/locale/en/messages.po index 4cc5a785..5890046f 100644 --- a/ui/src/locale/en/messages.po +++ b/ui/src/locale/en/messages.po @@ -686,7 +686,7 @@ msgstr "Update" msgid "Before" msgstr "Before" -#: src/features/scans/scansSchemas.ts:493 +#: src/features/scans/scansSchemas.ts:495 msgid "May only contain the characters: A-Z, a-z, 0-9, ., -, _, /" msgstr "May only contain the characters: A-Z, a-z, 0-9, ., -, _, /" @@ -1191,8 +1191,8 @@ msgstr "API Key:" msgid "Software Bill of Materials" msgstr "Software Bill of Materials" -#: src/features/scans/scansSchemas.ts:542 -#: src/features/scans/scansSchemas.ts:560 +#: src/features/scans/scansSchemas.ts:544 +#: src/features/scans/scansSchemas.ts:562 msgid "Invalid path, longer than {MAX_PATH_LENGTH} characters" msgstr "Invalid path, longer than {MAX_PATH_LENGTH} characters" @@ -1250,7 +1250,7 @@ msgstr "Modify Hidden Finding" msgid "Features" msgstr "Features" -#: src/features/scans/scansSchemas.ts:509 +#: src/features/scans/scansSchemas.ts:511 msgid "Contains one of more of the following invalid characters: space, \\, ~, ^, :, ?, *, [" msgstr "Contains one of more of the following invalid characters: space, \\, ~, ^, :, ?, *, [" @@ -1417,8 +1417,8 @@ msgstr "Include paths: {0} ; Exclude paths: {1}" msgid "Standard" msgstr "Standard" -#: src/features/scans/scansSchemas.ts:486 -#: src/features/scans/scansSchemas.ts:490 +#: src/features/scans/scansSchemas.ts:488 +#: src/features/scans/scansSchemas.ts:492 #: src/pages/ResultsPage.tsx:937 #: src/pages/ResultsPage.tsx:950 #: src/pages/SearchPage.tsx:3441 @@ -1932,8 +1932,8 @@ msgstr "OWASP Check (Java)" msgid "Meta Data Field1 Missing" msgstr "Meta Data Field1 Missing" -#: src/features/scans/scansSchemas.ts:541 -#: src/features/scans/scansSchemas.ts:559 +#: src/features/scans/scansSchemas.ts:543 +#: src/features/scans/scansSchemas.ts:561 msgid "Invalid path, must be relative to repository base directory and contain valid characters" msgstr "Invalid path, must be relative to repository base directory and contain valid characters" @@ -2185,7 +2185,7 @@ msgstr "Components or Licenses" msgid "This column is filtered" msgstr "This column is filtered" -#: src/features/scans/scansSchemas.ts:499 +#: src/features/scans/scansSchemas.ts:501 msgid "Missing \"Organization/\" Prefix" msgstr "Missing \"Organization/\" Prefix" @@ -2474,8 +2474,8 @@ msgstr "Scan started" msgid "Meta data field2 must be less than {META_FIELD2_LENGTH} characters" msgstr "Meta data field2 must be less than {META_FIELD2_LENGTH} characters" -#: src/features/scans/scansSchemas.ts:519 -#: src/features/scans/scansSchemas.ts:520 +#: src/features/scans/scansSchemas.ts:521 +#: src/features/scans/scansSchemas.ts:522 msgid "Positive integer" msgstr "Positive integer" @@ -2761,7 +2761,7 @@ msgstr "Start a new scan using the same options used in this scan?<0/>Initiating msgid "Veracode SCA" msgstr "Veracode SCA" -#: src/features/scans/scansSchemas.ts:487 +#: src/features/scans/scansSchemas.ts:489 #: src/pages/ResultsPage.tsx:6344 msgid "Invalid value" msgstr "Invalid value" @@ -2832,8 +2832,8 @@ msgstr "End" msgid "Bundler Audit (Ruby)" msgstr "Bundler Audit (Ruby)" -#: src/features/scans/scansSchemas.ts:570 -#: src/features/scans/scansSchemas.ts:608 +#: src/features/scans/scansSchemas.ts:572 +#: src/features/scans/scansSchemas.ts:610 msgid "At least one scan feature or plugin must be enabled" msgstr "At least one scan feature or plugin must be enabled" From 8df0faa23cdd117455234176f7abbba045188e59 Mon Sep 17 00:00:00 2001 From: Chris Breeden <47154440+breedenc@users.noreply.github.com> Date: Fri, 5 Jan 2024 12:31:36 -0800 Subject: [PATCH 3/3] update setup-python action version (#83) * update setup-python action version --- .github/workflows/test_backend.yml | 2 +- .github/workflows/test_orchestrator.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test_backend.yml b/.github/workflows/test_backend.yml index b75bb652..57119f96 100644 --- a/.github/workflows/test_backend.yml +++ b/.github/workflows/test_backend.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # actions/checkout@v4.1.1 - - uses: actions/setup-python@41b7212b1668f5de9d65e9c82aa777e6bbedb3a8 # actions/setup-python@v2.1.4 + - uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # actions/setup-python@v5.0.0 with: python-version: "3.9" - name: Run backend tests diff --git a/.github/workflows/test_orchestrator.yml b/.github/workflows/test_orchestrator.yml index ae8875be..818a0e87 100644 --- a/.github/workflows/test_orchestrator.yml +++ b/.github/workflows/test_orchestrator.yml @@ -11,7 +11,7 @@ jobs: steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # actions/checkout@v4.1.1 - name: setup python environment - uses: actions/setup-python@41b7212b1668f5de9d65e9c82aa777e6bbedb3a8 # actions/setup-python@v2.1.4 + uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # actions/setup-python@v5.0.0 with: python-version: "3.9" architecture: "x64"