Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -698,12 +698,20 @@ Saving a relationship creates a new relationship revision and returns its connec

### Revoking and Deprecating Objects

All objects within the knowledge base can be _revoked_ or _deprecated_. These functionalities allow you to remove irrelevant or outdated objects without deleting them outright since deletions cannot be propagated to data consumers subscribed to your collections. Revoked and deprecated objects are hidden from UIs unless the user explicitly chooses to display them.
Objects can be retired without deleting their history. Deprecation marks an object as no longer used; supported object types can instead be revoked in favor of an existing replacement. Retired objects are hidden from default lists unless explicitly included.

- _Revoked_ objects are objects that are replaced by others within the knowledge base. Revoke an object by clicking the gear icon in the toolbar while on an object page and then clicking "revoke." You will then be prompted to select the revoking (replacing) object. Relationships cannot be revoked, only deprecated.
- _Deprecated_ objects are objects that you want to remove without indicating a replacement. Deprecate an object by clicking the gear icon in the toolbar while on an object page and then clicking "deprecate." We also recommend prepending a paragraph to the object description explaining the reason for the deprecation, although this is optional.

When an object is revoked or deprecated, all relationships attached to the object in question will themselves be deprecated.
Before deprecation, Workbench checks the latest authoritative inbound and outbound domain references. If embedded references remain, a dialog lists their source, target, field, and direction. Remove or replace those references on their source objects first. No relationships or dependent objects are automatically changed by a blocked attempt.

If only blocking formal relationships remain, confirmation retires those SROs first, then checks again and deprecates the object. Existing active `subtechnique-of` and `revoked-by` relationships are **preserved unchanged in both directions**: they neither block SDO deprecation nor receive new revisions. Ordinary relationship retirement creates a new revision with `x_mitre_deprecated: true`; it does not delete history. A failure stops the final object save, although earlier relationship retirements may already have completed. Data-source deprecation no longer automatically deprecates its components.

The type exemptions apply only when deprecating an SDO, not when explicitly retiring a relationship itself. The relationship controls and data-quality duplicate-relationship bulk action can still retire `subtechnique-of` or `revoked-by` SROs, including when an endpoint is inactive. The frontend consistently excludes these types from cascade confirmation, retirement, and final blocker checks even against older API responses; an older server may still reject the final SDO save, which is reported without retiring the preserved links. These exemptions do not change revocation.

When revoking with relationship preservation enabled, supported revocations also transfer embedded references through new source/replacement revisions. Analytic log-source names and channels are preserved. Conflicting single-reference fields and inactive referring objects block the operation rather than silently losing references. Without preservation, existing embedded references are not detached.

Authoring new embedded references or active SROs to revoked/deprecated objects is rejected, except for `revoked-by` SROs. Preserving an existing `subtechnique-of` link does not permit authoring a new one with inactive endpoints. Unchanged legacy embedded references and source-faithful collection imports have separate retention semantics. Historical revisions, retired SROs, attribution, markings, and release inventory do not block deprecation checks.

### Deleting Objects

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
overflow-y: scroll;
max-height: 50vh;
max-width: 33em;
overflow-wrap: anywhere;
}
.buttons {
margin-top: 24px;
Expand Down
24 changes: 21 additions & 3 deletions src/app/components/object-status/object-status.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ import { MatTooltipModule } from '@angular/material/tooltip';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { ActivatedRoute, Router } from '@angular/router';
import { MtxPopoverModule } from '@ng-matero/extensions/popover';
import { of } from 'rxjs';
import { of, Subject } from 'rxjs';
import { vi } from 'vitest';
import { Technique } from 'src/app/classes/stix/technique';
import { DeprecationService } from 'src/app/services/helpers/deprecation.service';

import { ObjectStatusComponent } from './object-status.component';
import { RestApiConnectorService } from 'src/app/services/connectors/rest-api/rest-api-connector.service';
Expand Down Expand Up @@ -75,7 +78,22 @@ describe('ObjectStatusComponent', () => {
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
it('uses authoritative deprecation without changing local status early', () => {
const pending = new Subject<boolean>();
const lifecycle = TestBed.inject(DeprecationService);
vi.spyOn(lifecycle, 'deprecate').mockReturnValue(pending);
component.object = new Technique();
component.objects = [component.object];
component.loaded = true;
component.editorService.editing = false;

component.toggleDeprecated();
expect(component.object.deprecated).toBe(false);
expect(component.deprecateDisabled).toBe(true);

pending.next(false);
pending.complete();
expect(component.deprecated).toBe(false);
expect(component.lifecyclePending).toBe(false);
});
});
170 changes: 61 additions & 109 deletions src/app/components/object-status/object-status.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ import { StixObject } from 'src/app/classes/stix/stix-object';
import { RestApiConnectorService } from 'src/app/services/connectors/rest-api/rest-api-connector.service';
import { EditorService } from 'src/app/services/editor/editor.service';
import { AddDialogComponent } from '../add-dialog/add-dialog.component';
import { ConfirmationDialogComponent } from '../confirmation-dialog/confirmation-dialog.component';
import { forkJoin } from 'rxjs';
import { of } from 'rxjs';
import { finalize, map, switchMap } from 'rxjs/operators';
import { DeprecationService } from 'src/app/services/helpers/deprecation.service';
import { WorkflowStatusMap } from 'src/app/utils/types';

@Component({
Expand All @@ -25,10 +26,13 @@ export class ObjectStatusComponent implements OnInit {
public relationships = [];
public revoked = false;
public deprecated = false;
public lifecyclePending = false;

public get disabled(): boolean {
return (
this.editorService.editing || this.editorService.type == 'collection'
this.lifecyclePending ||
this.editorService.editing ||
this.editorService.type == 'collection'
);
}

Expand All @@ -53,7 +57,8 @@ export class ObjectStatusComponent implements OnInit {
constructor(
public editorService: EditorService,
private restAPIService: RestApiConnectorService,
private dialog: MatDialog
private dialog: MatDialog,
private deprecationService: DeprecationService
) {}

ngOnInit(): void {
Expand Down Expand Up @@ -115,21 +120,6 @@ export class ObjectStatusComponent implements OnInit {
},
});

if (this.editorService.type == 'data-source') {
// retrieve related data components & their relationships
data$ = this.restAPIService.getAllRelatedToDataSource(
this.editorService.stixId
);
const dataSubscription = data$.subscribe({
next: results => {
this.relationships = this.relationships.concat(results);
},
complete: () => {
dataSubscription.unsubscribe();
},
});
}

// retrieve relationships with the object
data$ = this.restAPIService.getRelatedTo({
sourceOrTargetRef: this.editorService.stixId,
Expand All @@ -147,15 +137,6 @@ export class ObjectStatusComponent implements OnInit {
}
}

private save() {
const saveSubscription = this.object.save(this.restAPIService).subscribe({
complete: () => {
this.editorService.onReload.emit();
saveSubscription.unsubscribe();
},
});
}

public revoke() {
if (!this.loaded || !this.object || !this.objects) return;
if (this.revokeDisabled) return;
Expand All @@ -176,7 +157,11 @@ export class ObjectStatusComponent implements OnInit {
this.select = new SelectionModel<string>();
const revokeDialogData = {
selectableObjects: this.objects.filter(object => {
return object.stixID !== this.editorService.stixId;
return (
object.stixID !== this.editorService.stixId &&
!object.revoked &&
!object.deprecated
);
}),
type: this.editorService.type,
select: this.select,
Expand Down Expand Up @@ -207,29 +192,63 @@ export class ObjectStatusComponent implements OnInit {
});
} else {
// unrevoke object, deprecate the 'revoked-by' relationship
// this is the only case in which a 'revoked-by' relationship is deprecated
const revokedRelationship = this.relationships.find(
r =>
r.relationship_type == 'revoked-by' &&
r.source_ref == this.object.stixID
);
if (revokedRelationship) {
revokedRelationship.deprecated = true;
revokedRelationship.save(this.restAPIService);
}
this.revoked = false;
this.object.revoked = false;
this.save();
this.lifecyclePending = true;
const retire = revokedRelationship
? this.deprecationService.deprecate(revokedRelationship)
: of(true);
retire
.pipe(
switchMap(retired => {
if (!retired) return of(false);
this.object.revoked = false;
return this.object.save(this.restAPIService).pipe(map(() => true));
}),
finalize(() => (this.lifecyclePending = false))
)
.subscribe({
next: saved => {
this.revoked = !saved;
if (saved) this.editorService.onReload.emit();
},
error: error => {
this.revoked = true;
this.object.revoked = true;
this.deprecationService.showError(error);
},
});
}
}

private setDeprecated(deprecated: boolean) {
this.deprecated = deprecated;
this.lifecyclePending = true;
if (deprecated) {
this.deprecateObjects(false);
this.deprecationService
.deprecate(this.object)
.pipe(finalize(() => (this.lifecyclePending = false)))
.subscribe(saved => {
this.deprecated = this.object.deprecated;
if (saved) this.editorService.onReload.emit();
});
} else {
this.object.deprecated = false;
this.save();
this.object
.save(this.restAPIService)
.pipe(finalize(() => (this.lifecyclePending = false)))
.subscribe({
complete: () => {
this.deprecated = false;
this.editorService.onReload.emit();
},
error: error => {
this.object.deprecated = true;
this.deprecationService.showError(error);
},
});
}
}

Expand Down Expand Up @@ -266,77 +285,10 @@ export class ObjectStatusComponent implements OnInit {
this.editorService.onReload.emit();
revokeSubscription.unsubscribe();
},
error: () => {
error: error => {
this.deprecationService.showError(error);
this.revoked = false;
},
});
}

/**
* Deprecates or revokes the object and deprecates all relationships with this object,
* with the exception of 'subtechnique-of' relationships
*/
private deprecateObjects(revoked: boolean, revoked_by_id?: string) {
const saves = [];

// inform users of relationship changes
const confirmationPrompt = this.dialog.open(ConfirmationDialogComponent, {
maxWidth: '35em',
data: {
message:
'All relationships with this object will be deprecated. Do you want to continue?',
},
autoFocus: false, // prevents auto focus on toolbar buttons
});

const confirmationSub = confirmationPrompt.afterClosed().subscribe({
next: result => {
if (!result) {
// user cancelled
if (revoked) this.revoked = false;
else this.deprecated = false;
return;
}

// deprecate or revoke object
if (revoked) this.object.revoked = true;
else this.object.deprecated = true;
saves.push(this.object.save(this.restAPIService));

// update relationships with the object
for (const relationship of this.relationships) {
// do not deprecate 'subtechnique-of' or 'revoked-by' relationships
if (
!relationship.deprecated &&
!['subtechnique-of', 'revoked-by'].includes(
relationship.relationship_type
)
) {
relationship.deprecated = true;
saves.push(relationship.save(this.restAPIService));
}
}

if (revoked_by_id) {
// create a new 'revoked-by' relationship
const revokedRelationship = new Relationship();
revokedRelationship.relationship_type = 'revoked-by';
revokedRelationship.source_ref = this.object.stixID;
revokedRelationship.target_ref = revoked_by_id;
saves.push(revokedRelationship.save(this.restAPIService));
}

// complete save calls
const saveSubscription = forkJoin(saves).subscribe({
complete: () => {
this.editorService.onReload.emit();
saveSubscription.unsubscribe();
},
});
},
complete: () => {
confirmationSub.unsubscribe();
},
});
}
}

This file was deleted.

28 changes: 28 additions & 0 deletions src/app/services/connectors/rest-api/rest-api-connector.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,27 @@ export interface Paginated<T> {
};
}

export interface DeprecationBlockers {
sros: {
stix_id: string;
modified: string;
relationship_type: string;
direction: 'inbound' | 'outbound';
}[];
embedded: {
source_ref: string;
target_ref: string;
path: string;
direction: 'inbound' | 'outbound';
}[];
}

export interface DeprecationCheck {
stix_id: string;
can_deprecate: boolean;
blockers: DeprecationBlockers;
}

export interface Namespace {
prefix: string;
range_start: string;
Expand Down Expand Up @@ -143,6 +164,13 @@ export class RestApiConnectorService extends ApiConnector {
super(snackbar);
}

/** Check authoritative latest references; never substitute cached workspace data. */
public getDeprecationCheck(stixId: string): Observable<DeprecationCheck> {
return this.http.get<DeprecationCheck>(
`${this.apiUrl}/attack-objects/${encodeURIComponent(stixId)}/deprecation-check`
);
}

/**
* Get the name of a given STIX object
*/
Expand Down
Loading
Loading