Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
bbfeaad
feat: diable submit for review button if object is not in any track
adpare Aug 14, 2026
57d90ad
feat(release-tracks): add bulk and individual candidate review dialogs
avwchapman Aug 14, 2026
27ce3c8
test(release-tracks): expand candidate review coverage
avwchapman Aug 14, 2026
e0c3148
style: apply Prettier formatting
avwchapman Aug 14, 2026
11492fb
fix: grey out button when not part of a release track
adpare Aug 21, 2026
a1d5ea7
Merge pull request #932 from mitre-attack/disable-submit-for-review
avwchapman Aug 24, 2026
7610475
feat(release-tracks): add confirmed bulk approval for reviews
avwchapman Aug 24, 2026
ca29998
Merge branch 'beta' into 789-bulk-review-modals
avwchapman Aug 24, 2026
f82e4b9
fix(relationships): reset related objects in place
seansica Sep 3, 2026
013c38a
feat(release-tracks): surface sealed content and publication settings
seansica Sep 3, 2026
ff2f8db
feat(data-quality): show domain consistency report
seansica Sep 3, 2026
4bc6f47
feat(release-tracks): adopt the draft-then-tag flow with aliases
seansica Sep 3, 2026
7e3f162
feat(release-tracks): show page activity while long operations run
seansica Sep 3, 2026
79db885
Merge pull request #933 from mitre-attack/789-bulk-review-modals
seansica Sep 9, 2026
0f71c6d
Merge branch 'beta' into feat/sealed-content-manifests
seansica Sep 9, 2026
f2bc0bb
style: apply formatting
seansica Sep 9, 2026
e529ef2
Merge pull request #953 from mitre-attack/feat/sealed-content-manifests
seansica Sep 9, 2026
90286da
feat(release-tracks): add rollback and release version controls
seansica Sep 9, 2026
aab7f0d
feat(release-tracks): add guided schedule autocomplete
seansica Sep 9, 2026
51ee79d
Merge pull request #956 from mitre-attack/feat/virtual-track-scheduli…
seansica Sep 9, 2026
6fb03e4
feat(release-tracks): scope composition provenance to snapshots
seansica Sep 9, 2026
56a4f12
chore(release-tracks): merge rollback controls into beta
seansica Sep 9, 2026
880c49e
feat(release-tracks): separate draft conversion from snapshot deletion
seansica Sep 9, 2026
02b759d
fix(release-tracks): clarify snapshot card headers
seansica Sep 10, 2026
5f6b572
feat(release-tracks): show snapshot causes and creator avatars
seansica Sep 10, 2026
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
330 changes: 188 additions & 142 deletions docs/usage.md

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ import { MultipleChoiceDialogComponent } from './components/multiple-choice-dial
import { NavigationComponent } from './components/navigation/navigation.component';
import { ReferenceEditDialogComponent } from './components/reference-edit-dialog/reference-edit-dialog.component';
import { ReleasePreviewDialogComponent } from './components/release-preview-dialog/release-preview-dialog.component';
import { ReleaseVersionDialogComponent } from './components/release-version-dialog/release-version-dialog.component';
import { SnapshotDescriptionDialogComponent } from './components/snapshot-description-dialog/snapshot-description-dialog.component';
import { HistoryTimelineComponent } from './components/stix/stix-page-tabs/history-timeline/history-timeline.component';
import { MembershipSectionComponent } from './components/stix/stix-page-tabs/membership-section/membership-section.component';
Expand Down Expand Up @@ -151,6 +152,7 @@ import { IconViewComponent } from './components/icon-view/icon-view.component';
import { ObjectStatusComponent } from './components/object-status/object-status.component';
import { RecentActivityComponent } from './components/recent-activity/recent-activity.component';
import { NotesEditorComponent } from './components/stix/stix-page-tabs/notes-editor/notes-editor.component';
import { ReleaseReviewDialogComponent } from './components/release-review-dialog/release-review-dialog.component';
import { IdentityPropertyComponent } from './components/stix/identity-property/identity-property.component';

import { CitationEditComponent } from './components/stix/citation-property/citation-edit/citation-edit.component';
Expand Down Expand Up @@ -273,6 +275,7 @@ export function initConfig(appConfigService: AppConfigService) {
ReferenceSidebarComponent,
ReferenceEditDialogComponent,
ReleasePreviewDialogComponent,
ReleaseVersionDialogComponent,
SnapshotDescriptionDialogComponent,
MultipleChoiceDialogComponent,
ValidationResultsComponent,
Expand Down Expand Up @@ -350,6 +353,7 @@ export function initConfig(appConfigService: AppConfigService) {
OrderedListViewComponent,
OrderedListEditComponent,
NotesEditorComponent,
ReleaseReviewDialogComponent,
ObjectStatusComponent,
RecentActivityComponent,
IdentityPropertyComponent,
Expand Down
63 changes: 49 additions & 14 deletions src/app/classes/release-tracks/api.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
import type { WorkflowStatusType } from 'src/app/utils/types';
import type { Composition } from './composition';
import type { Composition, CompositionResolution } from './composition';
import type { ReleaseTrackConfig } from './config';
import {
ReleaseTrackType,
type ExportFormatType,
type ReleasePreviewFormatType,
} from './enums';
import type { SnapshotSchedule } from './release-track';
import type { SnapshotCreationCause } from './snapshot-creation-cause';
import type { SnapshotCreationActor } from './snapshot-creation-actor';

export type StixObjectRef = string | { id: string; modified?: string };

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 +31,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 All @@ -45,6 +45,10 @@ export type ReleasePayload = (
| { increment?: undefined; version?: undefined }
) & { description?: string };

export interface RetagReleasePayload {
version: string;
}

export interface ClonePayload {
name?: string;
}
Expand All @@ -62,10 +66,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 +77,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 +86,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 All @@ -91,6 +121,7 @@ export interface ReleasePreviewSummaryBase {
track_id: string;
type: ReleaseTrackType;
source_snapshot_modified: string;
release_snapshot_modified?: string;
version: string;
version_bounds: {
lower: { version: string; modified: string } | null;
Expand All @@ -115,6 +146,7 @@ export interface StandardReleasePreviewSummary extends ReleasePreviewSummaryBase
changes: {
promoted_count: number;
};
relationships?: PreviewRelationshipChanges;
}

export interface VirtualReleasePreviewSummary extends ReleasePreviewSummaryBase {
Expand Down Expand Up @@ -149,12 +181,18 @@ export interface SnapshotBundleHashes {
}

export interface ReleaseTrackSnapshotHistoryItem {
creation_cause?: SnapshotCreationCause;
creation_actor?: SnapshotCreationActor;
id?: string;
modified?: string | Date;
version?: string | null;
graph_manifest_id?: string;
/** Exact draft snapshot retained when a standard release was created. */
release_source_modified?: string | Date;
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 Expand Up @@ -192,10 +230,7 @@ export interface ReleaseTrackSnapshotHistoryItem {
[key: string]: any;
};
statistics?: Record<string, any>;
composition_resolution?: {
total_objects?: number;
[key: string]: any;
};
composition_resolution?: CompositionResolution | null;
stix?: {
id?: string;
modified?: string | Date;
Expand Down
2 changes: 1 addition & 1 deletion src/app/classes/release-tracks/component-track.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export interface ComponentSnapshotResolution {
track_id: string;
track_name: string;
track_type: string;
resolved_snapshot_id: Date;
resolved_snapshot_id: Date | string;
resolved_version?: string | null;
strategy_used: string;
filters_applied?: ComponentTrackFilters;
Expand Down
11 changes: 2 additions & 9 deletions src/app/classes/release-tracks/composition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,18 @@
// aggregates content from component tracks
// -----------------------------------------------------------------------------

import { DeduplicationStrategyType, SnapshotTierType } from './enums';
import { DeduplicationStrategyType } from './enums';
import { ComponentTrack, ComponentSnapshotResolution } from './component-track';
import { WorkflowStatusType } from 'src/app/utils/types';

export interface Composition {
component_tracks?: ComponentTrack[];
deduplication?: {
strategy?: DeduplicationStrategyType;
// When resolving conflicts between versions, which snapshot tier should be
// preferred (e.g., prefer from 'staged' over 'candidate' or vice-versa).
tier_resolution?: SnapshotTierType;
// When resolving conflicts, prefer the object with the highest workflow
// status (e.g., 'reviewed' over 'awaiting-review').
status_resolution?: WorkflowStatusType;
};
}

export interface CompositionResolution {
resolved_at?: Date;
resolved_at?: Date | string;
component_snapshots?: ComponentSnapshotResolution[];
deduplication?: {
total_objects_before?: number;
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/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,6 @@ export * from './enums';
export * from './history';
export * from './release-track';
export * from './snapshot';
export * from './snapshot-creation-cause';
export * from './snapshot-creation-actor';
export * from './tiers';
4 changes: 3 additions & 1 deletion 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 All @@ -20,5 +22,5 @@ export interface ReleaseTrack {
export interface SnapshotSchedule {
mode?: SnapshotScheduleModeType;
cron?: string | null;
dates?: Date[] | undefined;
dates?: (Date | string)[];
}
11 changes: 11 additions & 0 deletions src/app/classes/release-tracks/snapshot-creation-actor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/** Snapshot-local invocation identity, distinct from the original track creator. */
export interface SnapshotCreationActor {
kind: 'user' | 'system' | 'unknown';
user_account_id?: string;
user?: {
id: string;
name?: string;
displayName?: string;
username?: string;
};
}
47 changes: 47 additions & 0 deletions src/app/classes/release-tracks/snapshot-creation-cause.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
export enum SnapshotCreationCause {
Unknown = 'unknown',
TrackCreated = 'track_created',
ReleaseTagged = 'release_tagged',
TrackCloned = 'track_cloned',
BundleImported = 'bundle_imported',
MetadataUpdated = 'metadata_updated',
ConfigurationUpdated = 'configuration_updated',
CandidatesAdded = 'candidates_added',
CandidateRemoved = 'candidate_removed',
CandidatesReviewed = 'candidates_reviewed',
CandidatesPromoted = 'candidates_promoted',
CandidateVersionUpdated = 'candidate_version_updated',
StagedDemoted = 'staged_demoted',
CandidatesAutoPromoted = 'candidates_auto_promoted',
MemberSynced = 'member_synced',
CompositionUpdated = 'composition_updated',
ManualSnapshot = 'manual_snapshot',
ScheduledSnapshot = 'scheduled_snapshot',
QuarantinePromoted = 'quarantine_promoted',
}

export const SNAPSHOT_CREATION_CAUSE_LABELS: Record<
SnapshotCreationCause,
string
> = {
[SnapshotCreationCause.Unknown]: 'Creation cause unavailable',
[SnapshotCreationCause.TrackCreated]: 'Track created',
[SnapshotCreationCause.ReleaseTagged]: 'Release tagged',
[SnapshotCreationCause.TrackCloned]: 'Track copied',
[SnapshotCreationCause.BundleImported]: 'Bundle imported',
[SnapshotCreationCause.MetadataUpdated]: 'Track details updated',
[SnapshotCreationCause.ConfigurationUpdated]: 'Configuration updated',
[SnapshotCreationCause.CandidatesAdded]: 'Candidates added',
[SnapshotCreationCause.CandidateRemoved]: 'Candidate removed',
[SnapshotCreationCause.CandidatesReviewed]: 'Candidates reviewed',
[SnapshotCreationCause.CandidatesPromoted]: 'Candidates promoted',
[SnapshotCreationCause.CandidateVersionUpdated]: 'Candidate version updated',
[SnapshotCreationCause.StagedDemoted]: 'Staged objects demoted',
[SnapshotCreationCause.CandidatesAutoPromoted]:
'Candidates automatically promoted',
[SnapshotCreationCause.MemberSynced]: 'Object changes synchronized',
[SnapshotCreationCause.CompositionUpdated]: 'Composition updated',
[SnapshotCreationCause.ManualSnapshot]: 'Manual snapshot',
[SnapshotCreationCause.ScheduledSnapshot]: 'Scheduled snapshot',
[SnapshotCreationCause.QuarantinePromoted]: 'Quarantined object promoted',
};
27 changes: 27 additions & 0 deletions src/app/classes/release-tracks/snapshot.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,33 @@
import { ReleaseTrackSnapshot } from './snapshot';

describe('ReleaseTrackSnapshot', () => {
it('preserves snapshot actor separately from the original track creator', () => {
const actor = {
kind: 'user',
user_account_id: 'second',
user: { id: 'second', displayName: 'Second Editor' },
};
const snapshot = new ReleaseTrackSnapshot({
created_by_ref: 'first',
creation_actor: actor,
});
expect(snapshot.creation_actor).toEqual(actor);
expect(snapshot.serialize().creation_actor).toEqual(actor);
expect(snapshot.toSummary().creation_actor).toEqual(actor);
expect(
new ReleaseTrackSnapshot({ created_by_ref: 'first' }).creation_actor
).toEqual({ kind: 'unknown' });
});

it('preserves server creation causes and handles legacy snapshots', () => {
const snapshot = new ReleaseTrackSnapshot({
creation_cause: 'configuration_updated',
});
expect(snapshot.creation_cause).toBe('configuration_updated');
expect(snapshot.serialize().creation_cause).toBe('configuration_updated');
expect(snapshot.toSummary().creation_cause).toBe('configuration_updated');
expect(new ReleaseTrackSnapshot({}).creation_cause).toBe('unknown');
});
it('should preserve snapshot-local notes separately from track metadata', () => {
const snapshot = new ReleaseTrackSnapshot({
description: 'Long-lived track purpose',
Expand Down
Loading
Loading