Skip to content
Open
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
297 changes: 155 additions & 142 deletions docs/usage.md

Large diffs are not rendered by default.

45 changes: 36 additions & 9 deletions src/app/classes/release-tracks/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@ export interface CreateReleaseTrackPayload {
name: string;
description?: string;
snapshot_description?: string;
external_references?: any[];
object_marking_refs?: string[];
type?: ReleaseTrackType;
config?: ReleaseTrackConfig;
composition?: Composition;
Expand All @@ -31,8 +29,8 @@ export interface StixBundlePayload {
export interface UpdateMetadataPayload {
name?: string;
description?: string;
external_references?: any[];
object_marking_refs?: string[];
/** URL-safe slug accepted wherever the track ID is; null clears it. */
alias?: string | null;
}

export interface UpdateContentsPayload {
Expand Down Expand Up @@ -62,10 +60,9 @@ export interface PromoteQuarantinePayload {

export interface ReleaseTrackSnapshotOptions {
format?: ExportFormatType;
/** Workbench responses only; bundles reject it (they replay the sealed manifest). */
include?: 'members' | 'staged' | 'candidates' | 'quarantine' | 'all';
state?: string | string[];
stixVersion?: '2.0' | '2.1';
includeToc?: boolean;
}

export interface SnapshotHistoryOptions {
Expand All @@ -74,7 +71,7 @@ export interface SnapshotHistoryOptions {
offset?: number;
}

export interface SnapshotGraphStatistics {
export interface SnapshotContentStatistics {
primary_count: number;
secondary_count: number;
relationship_count: number;
Expand All @@ -83,6 +80,33 @@ export interface SnapshotGraphStatistics {
total_count: number;
}

export interface SnapshotPublication {
collection_id: string;
created: string;
created_by_ref: string;
object_marking_refs: string[];
attack_spec_version: string;
}

export interface PreviewRelationshipChange {
object_ref: string;
object_modified: string;
relationship_type?: string;
source_ref?: string;
target_ref?: string;
stale_endpoints?: ('source' | 'target')[];
}

export interface PreviewRelationshipChanges {
selected_count: number;
added_count: number;
removed_count: number;
unchanged_count: number;
added: PreviewRelationshipChange[];
removed: PreviewRelationshipChange[];
stale_endpoints: PreviewRelationshipChange[];
}

export type ReleasePreviewOptions = ReleasePayload & {
format?: ReleasePreviewFormatType;
};
Expand Down Expand Up @@ -115,6 +139,7 @@ export interface StandardReleasePreviewSummary extends ReleasePreviewSummaryBase
changes: {
promoted_count: number;
};
relationships?: PreviewRelationshipChanges;
}

export interface VirtualReleasePreviewSummary extends ReleasePreviewSummaryBase {
Expand Down Expand Up @@ -152,9 +177,11 @@ export interface ReleaseTrackSnapshotHistoryItem {
id?: string;
modified?: string | Date;
version?: string | null;
graph_manifest_id?: string;
content_manifest_id?: string;
publication?: SnapshotPublication;
bundle_id?: string;
bundle_hashes?: SnapshotBundleHashes;
graph_statistics?: SnapshotGraphStatistics;
content_statistics?: SnapshotContentStatistics;
snapshot_description?: string;
type?: ReleaseTrackType;
name?: string;
Expand Down
39 changes: 35 additions & 4 deletions src/app/classes/release-tracks/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,42 @@ import type {
MemberSyncStrategyType,
} from './enums';

export type InheritedIdentitySetting =
{ inherit: true } | { inherit: false; value: string };

export type InheritedMarkingRefsSetting =
{ inherit: true } | { inherit: false; value: string[] };

// Publication metadata for the emitted x-mitre-collection object. Each rule
// inherits the global system configuration unless overridden at the track
// scope. collection_id and created become immutable once the track has a
// tagged release.
export interface PublicationConfig {
collection_id?: string | null;
created?: string | null;
created_by_ref?: InheritedIdentitySetting;
object_marking_refs?: InheritedMarkingRefsSetting;
}

export type PublicationSource = 'track' | 'global' | 'derived' | 'content';

export interface PublicationResolved {
collection_id: string;
created: string;
created_by_ref: string;
object_marking_refs: string[];
attack_spec_version: string;
sources: {
collection_id: PublicationSource;
created: PublicationSource;
created_by_ref: PublicationSource;
object_marking_refs: PublicationSource;
};
}

export interface ReleaseTrackConfig {
candidacy_threshold?: WorkflowStatusType;
auto_promote?: boolean;
include_secondary_objects?: {
enabled?: boolean;
status_threshold?: WorkflowStatusType;
};
promotion_conflicts?: {
candidates_to_staged?: ConflictPolicyType;
staged_to_members?: ConflictPolicyType;
Expand All @@ -28,4 +57,6 @@ export interface ReleaseTrackConfig {
status_policy?: MemberSyncPolicyType;
};
};
publication?: PublicationConfig;
publication_resolved?: PublicationResolved;
}
2 changes: 2 additions & 0 deletions src/app/classes/release-tracks/release-track.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ export interface ReleaseTrack {
track_id: string;
type: ReleaseTrackType;
name: string;
/** Optional URL-safe slug accepted wherever the track ID is. */
alias?: string | null;
description?: string;
created_at: Date;
updated_at: Date;
Expand Down
21 changes: 17 additions & 4 deletions src/app/classes/release-tracks/snapshot.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { SnapshotBundleHashes, SnapshotPublication } from './api';
import { Composition, CompositionResolution } from './composition';
import { ReleaseTrackConfig } from './config';
import { ReleaseTrackType } from './enums';
Expand All @@ -17,11 +18,16 @@ export class ReleaseTrackSnapshot {
public modified: Date = new Date();
public version?: string | null;
public name = '';
/** Registry alias for the track, attached to workbench responses. */
public alias?: string | null;
public description?: string;
public snapshot_description?: string;
public created: Date = new Date();
public created_by_ref?: string;
public object_marking_refs?: string[];
public content_manifest_id?: string;
public publication?: SnapshotPublication;
public bundle_id?: string;
public bundle_hashes?: SnapshotBundleHashes;

public config: ReleaseTrackConfig = {} as ReleaseTrackConfig;
public version_history: VersionHistoryEntry[] = [];
Expand Down Expand Up @@ -91,13 +97,17 @@ export class ReleaseTrackSnapshot {
if ('modified' in raw) this.modified = new Date(raw.modified);
if ('version' in raw) this.version = raw.version;
if ('name' in raw) this.name = raw.name;
if ('alias' in raw) this.alias = raw.alias;
if ('description' in raw) this.description = raw.description;
if ('snapshot_description' in raw)
this.snapshot_description = raw.snapshot_description;
if ('created' in raw) this.created = new Date(raw.created);
if ('created_by_ref' in raw) this.created_by_ref = raw.created_by_ref;
if ('object_marking_refs' in raw && Array.isArray(raw.object_marking_refs))
this.object_marking_refs = raw.object_marking_refs.slice();
if ('content_manifest_id' in raw)
this.content_manifest_id = raw.content_manifest_id;
if ('publication' in raw) this.publication = raw.publication;
if ('bundle_id' in raw) this.bundle_id = raw.bundle_id;
if ('bundle_hashes' in raw) this.bundle_hashes = raw.bundle_hashes;

if ('config' in raw) this.config = raw.config;
if ('summary' in raw) this.summary = raw.summary;
Expand Down Expand Up @@ -196,7 +206,10 @@ export class ReleaseTrackSnapshot {
snapshot_description: this.snapshot_description,
created: this.created ? this.created.toISOString() : undefined,
created_by_ref: this.created_by_ref,
object_marking_refs: this.object_marking_refs,
content_manifest_id: this.content_manifest_id,
publication: this.publication,
bundle_id: this.bundle_id,
bundle_hashes: this.bundle_hashes,
config: this.config,
summary: this.summary,
version_history: this.version_history?.map(v => ({
Expand Down
6 changes: 5 additions & 1 deletion src/app/classes/release-tracks/tiers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,15 @@ export interface TierEntryModifiedByUser {
export interface TierEntryDisplayFields {
attack_id?: string;
name?: string;
/** STIX object type of the selected revision */
type?: string;
/** ATT&CK version of the selected revision */
x_mitre_version?: string;
description?: string;
modified_by_user?: TierEntryModifiedByUser;
}

export interface MemberEntry {
export interface MemberEntry extends TierEntryDisplayFields {
object_ref: string;
object_modified: Date;
}
Expand Down
29 changes: 15 additions & 14 deletions src/app/classes/stix/relationship.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ function rawObject(type: string, id: string, state: string) {
}

describe('Relationship revision workflow', () => {
it('creates WIP revisions of related SDOs after saving a relationship revision', async () => {
it('resets related SDOs to WIP in place after saving a relationship revision', async () => {
const source = rawObject(
'intrusion-set',
'intrusion-set--00000000-0000-4000-8000-000000000001',
Expand Down Expand Up @@ -55,16 +55,16 @@ describe('Relationship revision workflow', () => {
calls.push('relationship:post');
return createAsyncObservable(value);
});
const postGroup = vi.fn((value: Group) => {
calls.push('source:post');
const postGroup = vi.fn();
const postMitigation = vi.fn();
const putGroup = vi.fn((value: Group) => {
calls.push('source:put');
return createAsyncObservable(value);
});
const postMitigation = vi.fn((value: Mitigation) => {
calls.push('target:post');
const putMitigation = vi.fn((value: Mitigation) => {
calls.push('target:put');
return createAsyncObservable(value);
});
const putGroup = vi.fn();
const putMitigation = vi.fn();
const restApiService = {
postRelationship,
postGroup,
Expand All @@ -75,15 +75,16 @@ describe('Relationship revision workflow', () => {

await firstValueFrom(relationship.save(restApiService));

expect(calls).toEqual(['relationship:post', 'source:post', 'target:post']);
expect(calls).toEqual(['relationship:post', 'source:put', 'target:put']);
expect(postRelationship).toHaveBeenCalledWith(relationship);
expect(postGroup).toHaveBeenCalledOnce();
expect(postMitigation).toHaveBeenCalledOnce();
expect(postGroup.mock.calls[0][0].workflow?.state).toBe('work-in-progress');
expect(postMitigation.mock.calls[0][0].workflow?.state).toBe(
expect(putGroup).toHaveBeenCalledOnce();
expect(putMitigation).toHaveBeenCalledOnce();
expect(putGroup.mock.calls[0][0].workflow?.state).toBe('work-in-progress');
expect(putMitigation.mock.calls[0][0].workflow?.state).toBe(
'work-in-progress'
);
expect(putGroup).not.toHaveBeenCalled();
expect(putMitigation).not.toHaveBeenCalled();
// Workflow state is workspace metadata: no new SDO revisions are created.
expect(postGroup).not.toHaveBeenCalled();
expect(postMitigation).not.toHaveBeenCalled();
});
});
7 changes: 4 additions & 3 deletions src/app/classes/stix/relationship.ts
Original file line number Diff line number Diff line change
Expand Up @@ -487,7 +487,7 @@
*/
public validate(
restAPIService: RestApiConnectorService,
tempWorkflowState?: WorkflowStatusType

Check warning on line 490 in src/app/classes/stix/relationship.ts

View workflow job for this annotation

GitHub Actions / static-checks

'tempWorkflowState' is defined but never used. Allowed unused args must match /^_/u
): Observable<ValidationData> {
return this.base_validate(restAPIService).pipe(
map(result => {
Expand Down Expand Up @@ -689,8 +689,9 @@
}

/**
* Creates a WIP revision of a related object. Existing revisions may be
* pinned by deterministic snapshot graphs and must remain immutable.
* Resets a related object's workflow state to WIP in place. Workflow state
* is workspace metadata, not STIX content, so a PUT never changes a sealed
* revision and never creates a new object revision.
* @param restAPIService the rest api service
* @param object the relationship source object
*/
Expand All @@ -704,6 +705,6 @@
object.workflow = { state: WorkflowStatus.WorkInProgress };
}
object.workflow.state = WorkflowStatus.WorkInProgress;
return object.save(restAPIService);
return object.update(restAPIService);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,39 @@ <h2 id="release-preview-dialog-title">
</article>
</section>

<section
*ngIf="relationshipChanges"
class="release-summary release-summary--relationships"
aria-label="Relationships sealed by this release">
<article class="release-stat release-stat--included">
<strong>{{ relationshipSelectedCount }}</strong>
<span>Relationships sealed</span>
</article>
<article class="release-stat release-stat--new">
<strong>{{ relationshipAddedCount }}</strong>
<span>Relationships added</span>
</article>
<article class="release-stat release-stat--excluded">
<strong>{{ relationshipRemovedCount }}</strong>
<span>Relationships dropped</span>
</article>
<article
class="release-stat"
[class.release-stat--updated]="staleEndpointRelationships.length">
<strong>{{ staleEndpointRelationships.length }}</strong>
<span>Authored against other revisions</span>
</article>
</section>
<p
*ngIf="staleEndpointRelationships.length"
class="release-relationship-warning">
<mat-icon aria-hidden="true">info_outline</mat-icon>
{{ staleEndpointRelationships.length }} relationship(s) will ship
against a newer revision of an endpoint than the one they were authored
on. Review them in the released bundle if their descriptions reference
revision-specific content.
</p>

<mat-form-field
appearance="outline"
class="release-snapshot-description-field">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -439,3 +439,20 @@
}
}
}

.release-summary--relationships {
margin-top: 0.75rem;
}

.release-relationship-warning {
display: flex;
align-items: flex-start;
gap: 0.5rem;
margin: 0.75rem 0 0;
font-size: 0.9rem;
opacity: 0.85;

mat-icon {
flex-shrink: 0;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,29 @@ export class ReleasePreviewDialogComponent {
return this.data.previewSummary?.changes?.removed_count ?? 0;
}

/**
* Relationship inventory the release commit would seal (standard tracks).
*/
public get relationshipChanges(): any | null {
return this.data.previewSummary?.relationships ?? null;
}

public get relationshipSelectedCount(): number {
return this.relationshipChanges?.selected_count ?? 0;
}

public get relationshipAddedCount(): number {
return this.relationshipChanges?.added_count ?? 0;
}

public get relationshipRemovedCount(): number {
return this.relationshipChanges?.removed_count ?? 0;
}

public get staleEndpointRelationships(): any[] {
return this.relationshipChanges?.stale_endpoints ?? [];
}

public get quarantinedObjectCount(): number {
return this.data.previewSummary?.changes?.quarantined_count ?? 0;
}
Expand Down
Loading
Loading