diff --git a/src/app/app-routing-stix.module.ts b/src/app/app-routing-stix.module.ts index bb363bb48..370e7a8bd 100644 --- a/src/app/app-routing-stix.module.ts +++ b/src/app/app-routing-stix.module.ts @@ -78,6 +78,12 @@ const stixRouteData = [ headerSection: 'defenses', deprecated: true, }, + // more + { + attackType: 'identity', + editable: true, + headerSection: 'more', + }, ]; const stixRoutes: Routes = []; diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 54b5bb72b..d90fd7223 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -45,6 +45,7 @@ import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatRadioModule } from '@angular/material/radio'; import { MatSelectModule } from '@angular/material/select'; import { MatSidenavModule } from '@angular/material/sidenav'; +import { MatSlideToggleModule } from '@angular/material/slide-toggle'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatSortModule } from '@angular/material/sort'; import { MatStepperModule } from '@angular/material/stepper'; @@ -185,6 +186,7 @@ import { CampaignViewComponent } from './views/stix/campaign-view/campaign-view. import { DataComponentViewComponent } from './views/stix/data-component-view/data-component-view.component'; import { DataSourceViewComponent } from './views/stix/data-source-view/data-source-view.component'; import { GroupViewComponent } from './views/stix/group-view/group-view.component'; +import { IdentityViewComponent } from './views/stix/identity-view/identity-view.component'; import { MarkingDefinitionViewComponent } from './views/stix/marking-definition-view/marking-definition-view.component'; import { MatrixFlatComponent } from './views/stix/matrix/matrix-flat/matrix-flat.component'; import { MatrixSideComponent } from './views/stix/matrix/matrix-side/matrix-side.component'; @@ -328,6 +330,7 @@ export function initConfig(appConfigService: AppConfigService) { IdentityPropertyComponent, DataSourceViewComponent, DataComponentViewComponent, + IdentityViewComponent, MarkingDefinitionViewComponent, CampaignViewComponent, CitationPropertyComponent, @@ -394,6 +397,7 @@ export function initConfig(appConfigService: AppConfigService) { MatSelectModule, MatExpansionModule, MatCheckboxModule, + MatSlideToggleModule, MatRadioModule, MatProgressSpinnerModule, MatMenuModule, @@ -441,6 +445,7 @@ export function initConfig(appConfigService: AppConfigService) { MatSelectModule, MatExpansionModule, MatCheckboxModule, + MatSlideToggleModule, MatRadioModule, MatProgressSpinnerModule, MatMenuModule, diff --git a/src/app/classes/stix/identity.ts b/src/app/classes/stix/identity.ts index efcff67f0..5a462ce61 100644 --- a/src/app/classes/stix/identity.ts +++ b/src/app/classes/stix/identity.ts @@ -1,4 +1,4 @@ -import { Observable, of } from 'rxjs'; +import { Observable } from 'rxjs'; import { RestApiConnectorService } from 'src/app/services/connectors/rest-api/rest-api-connector.service'; import { logger } from '../../utils/logger'; import { ValidationData } from '../serializable'; @@ -8,7 +8,8 @@ import { WorkflowState } from 'src/app/utils/types'; export class Identity extends StixObject { public name: string; // identity name public identity_class: string; // type of entity this identity describes - public roles?: string[]; // list of roles this identity performs + public roles: string[] = []; // list of roles this identity performs + public sectors: string[] = []; // list of sectors this identity belongs to public contact?: string; // contact information for this identity public readonly supportsAttackID = false; // Identity does not support ATT&CK IDs @@ -24,6 +25,7 @@ export class Identity extends StixObject { if (sdo) { this.deserialize(sdo); } + this.workflow = undefined; } /** @@ -37,6 +39,7 @@ export class Identity extends StixObject { rep.stix.name = this.name; rep.stix.identity_class = this.identity_class; if (this.roles) rep.stix.roles = this.roles; + if (this.sectors?.length) rep.stix.sectors = this.sectors; if (this.contact) rep.stix.contact_information = this.contact; // Strip properties that are empty strs + lists @@ -82,6 +85,15 @@ export class Identity extends StixObject { if ('roles' in sdo) { if (this.isStringArray(sdo.roles)) this.roles = sdo.roles; else logger.error('TypeError: roles field is not a string array.'); + } else { + this.roles = []; + } + + if ('sectors' in sdo) { + if (this.isStringArray(sdo.sectors)) this.sectors = sdo.sectors; + else logger.error('TypeError: sectors field is not a string array.'); + } else { + this.sectors = []; } if ('contact_information' in sdo) { @@ -106,9 +118,9 @@ export class Identity extends StixObject { */ public validate( restAPIService: RestApiConnectorService, - tempWorkflowState?: WorkflowState + _tempWorkflowState?: WorkflowState ): Observable { - return this.base_validate(restAPIService, tempWorkflowState); + return this.base_validate(restAPIService); } /** @@ -130,9 +142,14 @@ export class Identity extends StixObject { return postObservable; } - public delete(_restAPIService: RestApiConnectorService): Observable { - // deletion is not supported on Identity objects - return of({}); + public delete(restAPIService: RestApiConnectorService): Observable { + const deleteObservable = restAPIService.deleteIdentity(this.stixID); + const subscription = deleteObservable.subscribe({ + complete: () => { + subscription.unsubscribe(); + }, + }); + return deleteObservable; } /** diff --git a/src/app/classes/stix/stix-object.ts b/src/app/classes/stix/stix-object.ts index bd3a1def5..fc0bbc87d 100644 --- a/src/app/classes/stix/stix-object.ts +++ b/src/app/classes/stix/stix-object.ts @@ -17,6 +17,7 @@ import { v4 as uuid } from 'uuid'; import { logger } from '../../utils/logger'; import { ExternalReferences } from '../external-references'; import { Serializable, ValidationData } from '../serializable'; +import { UserAccount } from '../authn/user-account'; import { VersionNumber } from '../version-number'; export type workflowStates = @@ -35,6 +36,7 @@ export abstract class StixObject extends Serializable { public created_by?: any; public modified_by_ref: string; //embedded relationship public modified_by?: any; + public created_by_user_account?: UserAccount; public firstInitialized: boolean; // boolean to track if it is a newly created object public object_marking_refs: string[] = []; //list of embedded relationships to marking_defs @@ -357,6 +359,11 @@ export abstract class StixObject extends Serializable { "ObjectError: 'stix' field does not exist in modified_by_identity object" ); } + if ('created_by_user_account' in raw && raw.created_by_user_account) { + this.created_by_user_account = new UserAccount( + raw.created_by_user_account + ); + } if ('workspace' in raw) { // parse workspace fields diff --git a/src/app/components/confirmation-dialog/confirmation-dialog.component.html b/src/app/components/confirmation-dialog/confirmation-dialog.component.html index 0f3c3f43c..43dc1517a 100644 --- a/src/app/components/confirmation-dialog/confirmation-dialog.component.html +++ b/src/app/components/confirmation-dialog/confirmation-dialog.component.html @@ -5,6 +5,13 @@ no, {{ config.no_suffix }} + @@ -149,6 +150,40 @@

Namespace Settings

+ +
+
+
+

MITRE Identity Writes

+
+
+
+
+
+

+ Enable this only when this Workbench instance is allowed to create or + update the protected MITRE Corporation identity object. +

+
+ + {{ mitreIdentityWrites.enabled ? 'enabled' : 'disabled' }} + + +
+
+
+
@@ -157,4 +192,8 @@

Namespace Settings

+ + + diff --git a/src/app/views/dashboard-page/org-settings-page/org-settings-page.component.scss b/src/app/views/dashboard-page/org-settings-page/org-settings-page.component.scss index 97c2ae3bd..7bb5761fc 100644 --- a/src/app/views/dashboard-page/org-settings-page/org-settings-page.component.scss +++ b/src/app/views/dashboard-page/org-settings-page/org-settings-page.component.scss @@ -9,3 +9,57 @@ hr { margin-bottom: 30px; } + +.mitre-identity-write-setting { + align-items: center; + display: flex; + gap: 24px; + justify-content: space-between; + + p { + margin: 0; + max-width: 760px; + } +} + +.mitre-identity-write-controls { + align-items: center; + display: flex; + flex-shrink: 0; + gap: 16px; +} + +.mitre-identity-write-controls .mat-mdc-slide-toggle.toggle-enabled { + --mdc-switch-selected-focus-handle-color: #ffffff; + --mdc-switch-selected-focus-state-layer-color: #2e7d32; + --mdc-switch-selected-focus-track-color: #2e7d32; + --mdc-switch-selected-handle-color: #ffffff; + --mdc-switch-selected-hover-handle-color: #ffffff; + --mdc-switch-selected-hover-state-layer-color: #2e7d32; + --mdc-switch-selected-hover-track-color: #2e7d32; + --mdc-switch-selected-pressed-handle-color: #ffffff; + --mdc-switch-selected-pressed-state-layer-color: #2e7d32; + --mdc-switch-selected-pressed-track-color: #2e7d32; + --mdc-switch-selected-track-color: #2e7d32; +} + +.mitre-identity-write-controls .mat-mdc-slide-toggle.toggle-disabled { + --mdc-switch-unselected-focus-handle-color: #ffffff; + --mdc-switch-unselected-focus-state-layer-color: #c62828; + --mdc-switch-unselected-focus-track-color: #c62828; + --mdc-switch-unselected-handle-color: #ffffff; + --mdc-switch-unselected-hover-handle-color: #ffffff; + --mdc-switch-unselected-hover-state-layer-color: #c62828; + --mdc-switch-unselected-hover-track-color: #c62828; + --mdc-switch-unselected-pressed-handle-color: #ffffff; + --mdc-switch-unselected-pressed-state-layer-color: #c62828; + --mdc-switch-unselected-pressed-track-color: #c62828; + --mdc-switch-unselected-track-color: #c62828; +} + +@media (max-width: 760px) { + .mitre-identity-write-setting { + align-items: flex-start; + flex-direction: column; + } +} diff --git a/src/app/views/dashboard-page/org-settings-page/org-settings-page.component.spec.ts b/src/app/views/dashboard-page/org-settings-page/org-settings-page.component.spec.ts index 7d98cbbed..e253e2b76 100644 --- a/src/app/views/dashboard-page/org-settings-page/org-settings-page.component.spec.ts +++ b/src/app/views/dashboard-page/org-settings-page/org-settings-page.component.spec.ts @@ -7,6 +7,7 @@ import { RestApiConnectorService } from 'src/app/services/connectors/rest-api/re import { createMockRestApiConnector, createAsyncObservable, + createPaginatedResponse, } from 'src/app/testing/mocks/rest-api-connector.mock'; describe('OrgSettingsPageComponent', () => { @@ -16,6 +17,7 @@ describe('OrgSettingsPageComponent', () => { beforeEach(async () => { const mockRestApiConnector = createMockRestApiConnector({ getOrganizationIdentity: () => createAsyncObservable({}), + getAllIdentities: () => createAsyncObservable(createPaginatedResponse()), getOrganizationNamespace: () => createAsyncObservable({ prefix: '', range_start: undefined }), }); diff --git a/src/app/views/dashboard-page/org-settings-page/org-settings-page.component.ts b/src/app/views/dashboard-page/org-settings-page/org-settings-page.component.ts index b491d081e..4f91e8550 100644 --- a/src/app/views/dashboard-page/org-settings-page/org-settings-page.component.ts +++ b/src/app/views/dashboard-page/org-settings-page/org-settings-page.component.ts @@ -1,10 +1,14 @@ import { Component, OnInit } from '@angular/core'; +import { forkJoin } from 'rxjs'; import { Identity } from 'src/app/classes/stix/identity'; import { + MitreIdentityWrites, Namespace, RestApiConnectorService, } from 'src/app/services/connectors/rest-api/rest-api-connector.service'; +const MITRE_IDENTITY_STIX_ID = 'identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5'; + @Component({ selector: 'app-org-settings-page', templateUrl: './org-settings-page.component.html', @@ -13,7 +17,11 @@ import { }) export class OrgSettingsPageComponent implements OnInit { public organizationIdentity: Identity; + public organizationIdentities: Identity[]; + public selectedOrganizationIdentityId: string; public organizationNamespace: Namespace; + public mitreIdentityWrites: MitreIdentityWrites; + private savedMitreIdentityWritesEnabled: boolean; public idRegex = `^([A-Za-z])*$`; public rangeRegex = `^([0-9]){1,4}$`; @@ -29,11 +37,58 @@ export class OrgSettingsPageComponent implements OnInit { ); } + public get selectedOrganizationIdentity(): Identity { + return this.organizationIdentities?.find( + identity => identity.stixID === this.selectedOrganizationIdentityId + ); + } + + public get isIdentityUnchanged(): boolean { + return ( + !this.selectedOrganizationIdentityId || + this.selectedOrganizationIdentityId === this.organizationIdentity?.stixID + ); + } + + public get isMitreIdentityWritesUnchanged(): boolean { + return ( + !this.mitreIdentityWrites || + this.mitreIdentityWrites.enabled === this.savedMitreIdentityWritesEnabled + ); + } + + public get hasMitreIdentity(): boolean { + return ( + this.organizationIdentities?.some( + identity => identity.stixID === MITRE_IDENTITY_STIX_ID + ) ?? false + ); + } + constructor(private restAPIConnector: RestApiConnectorService) {} ngOnInit(): void { - const idSub = this.restAPIConnector.getOrganizationIdentity().subscribe({ - next: identity => (this.organizationIdentity = identity), + const idSub = forkJoin({ + identity: this.restAPIConnector.getOrganizationIdentity(), + identities: this.restAPIConnector.getAllIdentities(), + }).subscribe({ + next: ({ identity, identities }) => { + this.organizationIdentity = identity; + this.organizationIdentities = identities.data as Identity[]; + if ( + !this.organizationIdentities.some( + organizationIdentity => + organizationIdentity.stixID === identity.stixID + ) + ) { + this.organizationIdentities.push(identity); + } + this.organizationIdentities.sort((a, b) => + (a.name || a.stixID).localeCompare(b.name || b.stixID) + ); + this.selectedOrganizationIdentityId = identity.stixID; + if (this.hasMitreIdentity) this.loadMitreIdentityWrites(); + }, complete: () => idSub.unsubscribe(), }); @@ -52,6 +107,18 @@ export class OrgSettingsPageComponent implements OnInit { }); } + private loadMitreIdentityWrites(): void { + const mitreIdentityWritesSub = this.restAPIConnector + .getMitreIdentityWrites() + .subscribe({ + next: mitreIdentityWrites => { + this.mitreIdentityWrites = mitreIdentityWrites; + this.savedMitreIdentityWritesEnabled = mitreIdentityWrites.enabled; + }, + complete: () => mitreIdentityWritesSub.unsubscribe(), + }); + } + onBlur(): void { if (!this.isNOU(this.organizationNamespace.range_start)) { this.organizationNamespace.range_start = @@ -61,9 +128,10 @@ export class OrgSettingsPageComponent implements OnInit { saveIdentity(): void { const subscription = this.restAPIConnector - .postIdentity(this.organizationIdentity) + .setOrganizationIdentityRef(this.selectedOrganizationIdentityId) .subscribe({ - next: identity => (this.organizationIdentity = identity), + next: () => + (this.organizationIdentity = this.selectedOrganizationIdentity), complete: () => subscription.unsubscribe(), }); } @@ -76,4 +144,15 @@ export class OrgSettingsPageComponent implements OnInit { complete: () => subscription.unsubscribe(), }); } + + saveMitreIdentityWrites(): void { + const subscription = this.restAPIConnector + .setMitreIdentityWrites(this.mitreIdentityWrites.enabled) + .subscribe({ + next: () => + (this.savedMitreIdentityWritesEnabled = + this.mitreIdentityWrites.enabled), + complete: () => subscription.unsubscribe(), + }); + } } diff --git a/src/app/views/landing-page/landing-page.component.ts b/src/app/views/landing-page/landing-page.component.ts index c588dc8da..aa33c0b5e 100644 --- a/src/app/views/landing-page/landing-page.component.ts +++ b/src/app/views/landing-page/landing-page.component.ts @@ -16,6 +16,9 @@ import { stixRoutes } from '../../app-routing-stix.module'; standalone: false, }) export class LandingPageComponent implements OnInit, OnDestroy { + private readonly placeholderIdentityName = 'Placeholder Organization Identity'; + private readonly placeholderIdentityReminderKey = + 'attack-workbench.placeholder-organization-identity-reminder-dismissed'; private loginSubscription: Subscription; public pendingUsers; public routes: any[] = []; @@ -71,15 +74,16 @@ export class LandingPageComponent implements OnInit, OnDestroy { // bug the admin about editing their organization identity private openOrgIdentityDialog(): void { + if (localStorage.getItem(this.placeholderIdentityReminderKey) === 'true') { + return; + } + if (this.authenticationService.isAuthorized([Role.ADMIN])) { const subscription = this.restApiConnector .getOrganizationIdentity() .subscribe({ next: identity => { - if ( - identity && - identity.name == 'Placeholder Organization Identity' - ) { + if (identity && identity.name == this.placeholderIdentityName) { const prompt = this.dialog.open(ConfirmationDialogComponent, { maxWidth: '35em', data: { @@ -87,13 +91,23 @@ export class LandingPageComponent implements OnInit, OnDestroy { '### Your organization identity has not yet been set.\n\nYour organization identity is used for attribution of edits you make to objects in the knowledge base and is attached to published collections. Currently, a placeholder is being used.\n\nUpdate your organization identity now?', yes_suffix: 'edit my identity now', no_suffix: 'edit my identity later', + alternate_label: 'No, and stop reminding me', + alternate_value: 'dismiss', }, autoFocus: false, // prevents auto focus on buttons }); const prompt_subscription = prompt.afterClosed().subscribe({ next: prompt_result => { - if (prompt_result) - this.router.navigate(['/dashboard/org-settings']); + if (prompt_result === true) { + this.router.navigate(['/identity', identity.stixID], { + queryParams: { editing: true }, + }); + } else if (prompt_result === 'dismiss') { + localStorage.setItem( + this.placeholderIdentityReminderKey, + 'true' + ); + } }, complete: () => { prompt_subscription.unsubscribe(); diff --git a/src/app/views/stix/identity-view/identity-view.component.html b/src/app/views/stix/identity-view/identity-view.component.html new file mode 100644 index 000000000..fdf31b425 --- /dev/null +++ b/src/app/views/stix/identity-view/identity-view.component.html @@ -0,0 +1,98 @@ +
+
+
+ + +
+
+
+
+ +
+
+
+
+ +
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+

References

+
+
+
+
+ +
+
+
+
diff --git a/src/app/views/stix/identity-view/identity-view.component.ts b/src/app/views/stix/identity-view/identity-view.component.ts new file mode 100644 index 000000000..f27c732d1 --- /dev/null +++ b/src/app/views/stix/identity-view/identity-view.component.ts @@ -0,0 +1,32 @@ +import { Component, OnInit } from '@angular/core'; +import { Identity } from 'src/app/classes/stix'; +import { AuthenticationService } from 'src/app/services/connectors/authentication/authentication.service'; +import { RestApiConnectorService } from 'src/app/services/connectors/rest-api/rest-api-connector.service'; +import { StixViewPage } from '../stix-view-page'; + +@Component({ + selector: 'app-identity-view', + templateUrl: './identity-view.component.html', + standalone: false, +}) +export class IdentityViewComponent extends StixViewPage implements OnInit { + public get identity(): Identity { + return this.configCurrentObject as Identity; + } + public get previous(): Identity { + return this.configPreviousObject as Identity; + } + + constructor( + authenticationService: AuthenticationService, + private restApiConnector: RestApiConnectorService + ) { + super(authenticationService); + } + + ngOnInit(): void { + if (this.identity.firstInitialized) { + this.identity.setDefaultMarkingDefinitions(this.restApiConnector); + } + } +} diff --git a/src/app/views/stix/stix-page/stix-page.component.html b/src/app/views/stix/stix-page/stix-page.component.html index 6f5abf590..c1f3ac025 100644 --- a/src/app/views/stix/stix-page/stix-page.component.html +++ b/src/app/views/stix/stix-page/stix-page.component.html @@ -50,6 +50,9 @@

+ diff --git a/src/app/views/stix/stix-page/stix-page.component.ts b/src/app/views/stix/stix-page/stix-page.component.ts index 261a7deff..2767971a2 100644 --- a/src/app/views/stix/stix-page/stix-page.component.ts +++ b/src/app/views/stix/stix-page/stix-page.component.ts @@ -112,6 +112,7 @@ export class StixPageComponent implements OnInit, OnDestroy { ? this.oldAnalytics : undefined, versionAlreadyIncremented: versionChanged, + showWorkflow: this.editorService.hasWorkflow, }, autoFocus: false, // prevent auto focus on form field }); @@ -339,6 +340,8 @@ export class StixPageComponent implements OnInit, OnDestroy { ); else if (this.objectType == 'asset') objects$ = this.restApiService.getAsset(objectStixID); + else if (this.objectType == 'identity') + objects$ = this.restApiService.getIdentity(objectStixID); else if (this.objectType == 'marking-definition') objects$ = this.restApiService.getMarkingDefinition(objectStixID); const subscription = objects$.subscribe({