Skip to content
Closed
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
10 changes: 9 additions & 1 deletion .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -489,7 +489,15 @@
"requestfinished",
"LOCF",
"Unack",
"Tabnabbing"
"Tabnabbing",
"STIG",
"DISA",
"ISSM",
"remediations",
"rethrew",
"notexample",
"Deployers",
"stig"
],
"dictionaries": ["npm", "softwareTerms", "node", "html", "css", "bash", "en_US", "en-gb", "misc"],
"ignorePaths": [
Expand Down
62 changes: 62 additions & 0 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -1365,6 +1365,68 @@ can be used to manage user information and roles.
Open MCT provides an example [user](example/exampleUser/exampleUserCreator.js) and [user provider](example/exampleUser/ExampleUserProvider.js) which
can be used as a starting point for creating a custom user provider.

## Audit API

`openmct.audit` emits structured audit records (who / what / when / outcome) for
operator actions that already carry a user context: import and export as JSON,
role changes, notebook entry creation and deletion, and fault acknowledgement
and shelving. Records are kept in memory only; nothing is persisted or
transmitted unless a provider is registered.

Each record has the shape:

```javascript
{
id: string, // unique id for this record
source: 'openmct',
timestamp: string, // ISO 8601 UTC
action: string, // e.g. 'import', 'notebook.entry.create', 'user.role.change'
outcome: 'success' | 'failure',
actor: { id: string | null, username: string | null, role: string | null },
target: string | null, // key string of the domain object acted upon, if any
details: Object // action-specific context; never contains raw errors
}
```

`outcome` describes the operator action as carried out by the application. For
actions that persist through `openmct.objects.mutate` (notebook entries), the
write is queued in the active transaction or saved asynchronously, so a later
provider failure is reported through the persistence error path rather than by
rewriting the audit record. Import and export await their writes and report
`'failure'` when a write is rejected.

Providers receive every completed record and may return a promise. A provider
that throws or rejects is logged and does not affect the originating action or
other providers:

```javascript
openmct.audit.addProvider({
record(auditRecord) {
return fetch('/audit', { method: 'POST', body: JSON.stringify(auditRecord) });
}
});
openmct.audit.removeProvider(provider);
openmct.audit.hasProviders(); // boolean
```

Plugins may also emit their own records; `outcome` defaults to `'success'` and
`target` accepts an identifier or key string. The returned promise resolves with
the dispatched record once every provider has settled (accepted or rejected it),
so awaiting it gives confirmed delivery; built-in hooks do not await it so that
audit delivery never delays the operator action:

```javascript
await openmct.audit.record({
action: 'my-plugin.publish',
outcome: 'failure',
target: domainObject.identifier,
details: { reason: 'Timeout' }
});
```

`openmct.audit` is an `EventEmitter`; `openmct.audit.on('record', listener)`
(and `once` / `off`) observe records in-process with standard emitter semantics.

## Visibility-Based Rendering in View Providers

To enhance performance and resource efficiency in OpenMCT, a visibility-based rendering feature has been added. This feature is designed to defer the execution of rendering logic for views that are not currently visible. It ensures that views are only updated when they are in the viewport, similar to how modern browsers handle rendering of inactive tabs but optimized for the OpenMCT tabbed display. It also works when views are scrolled outside the viewport (e.g., in a Display Layout).
Expand Down
200 changes: 200 additions & 0 deletions docs/security/asd-stig-nist-800-53-review.md

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Required issue link is missing

The contribution rules require every pull request to link an issue. The description uses “Closes N/A,” leaving no required traceability record.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no tracking issue on this fork for this review; the work was requested directly, so "Closes N/A" is deliberate. If a tracking issue is opened I will link it in the description.

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions src/MCT.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { createApp, markRaw } from 'vue';

import ActionsAPI from './api/actions/ActionsAPI.js';
import AnnotationAPI from './api/annotation/AnnotationAPI.js';
import AuditLogger from './api/audit/AuditLogger.js';
import BrandingAPI from './api/Branding.js';
import CompositionAPI from './api/composition/CompositionAPI.js';
import EditorAPI from './api/Editor.js';
Expand Down Expand Up @@ -195,6 +196,14 @@ export class MCT extends EventEmitter {
*/
this.user = new UserAPI(this);

/**
* Structured audit records (who / what / when / outcome) for operator
* actions. Providers subscribe to receive records; nothing is persisted
* or transmitted by default.
* @type {AuditLogger}
*/
this.audit = new AuditLogger(this);
Comment on lines +199 to +205

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Senior API approval required

The new public openmct.audit API requires senior-developer approval under CONTRIBUTING.md. Confirm that approval before merge.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged. openmct.audit is a new public API and per CONTRIBUTING.md needs senior-developer approval before merge; this PR is intentionally left unmerged for that review. The API surface is small (record, addProvider, removeProvider, hasProviders, EventEmitter on/off/once) and documented in API.md.

Comment thread
devin-ai-integration[bot] marked this conversation as resolved.

/**
* An interface for managing notifications and alerts.
* @type {NotificationAPI}
Expand Down
234 changes: 234 additions & 0 deletions src/api/audit/AuditLogger.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2024, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/

import { EventEmitter } from 'eventemitter3';
import { v4 as uuid } from 'uuid';

/**
* @typedef {import('openmct').OpenMCT} OpenMCT
* @typedef {import('openmct').Identifier} Identifier
*/

/**
* @typedef {'success' | 'failure'} AuditOutcome
*/

/**
* @typedef {Object} AuditActor
* @property {string | null} id user id, or null when no user provider is configured
* @property {string | null} username user name, or null when no user provider is configured
* @property {string | null} role active role, or null when none is selected
*/

/**
* @typedef {Object} AuditRecord
* @property {string} id unique id for this record
* @property {string} source constant component identifier ("openmct")
* @property {string} timestamp ISO 8601 UTC timestamp from the system clock
* @property {string} action the operator action, e.g. "import", "notebook.entry.create"
* @property {AuditOutcome} outcome whether the action succeeded
* @property {AuditActor} actor who performed the action
* @property {string | null} target key string of the domain object acted upon, if any
* @property {Object} details structured, action-specific context
*/

/**
* @typedef {Object} AuditRecordInput
* @property {string} action
* @property {AuditOutcome} [outcome='success']
* @property {Identifier | string} [target]
* @property {Object} [details]
*/

/**
* @typedef {Object} AuditProvider
* @property {(record: AuditRecord) => void | Promise<void>} record receives each completed audit record
*/

const SOURCE = 'openmct';
const OUTCOMES = new Set(['success', 'failure']);

/**
* Emits structured audit records (who / what / when / outcome) for operator
* actions. The logger does not persist or transmit records itself; consumers
* subscribe with {@link AuditLogger#addProvider} or listen for the `record`
* event and forward records to a sink of their choosing.
*
* @extends EventEmitter
*/
export default class AuditLogger extends EventEmitter {
/** @type {OpenMCT} */
#openmct;
/** @type {Set<AuditProvider>} */
#providers = new Set();

/**
* @param {OpenMCT} openmct
*/
constructor(openmct) {
super();
this.#openmct = openmct;
}

/**
* Register a provider that receives every audit record.
* @param {AuditProvider} provider
* @returns {() => void} a function that removes the provider
*/
addProvider(provider) {
if (!provider || typeof provider.record !== 'function') {
throw new Error('Audit providers must implement a record(auditRecord) method');
}

this.#providers.add(provider);

return () => this.removeProvider(provider);
}

/**
* @param {AuditProvider} provider
*/
removeProvider(provider) {
this.#providers.delete(provider);
}

/**
* @returns {boolean} true if at least one provider is registered
*/
hasProviders() {
return this.#providers.size > 0;
}

/**
* Build and dispatch an audit record. Never throws: failures in actor
* resolution or in a provider are logged to the console and do not
* interrupt the operator action being audited.
*
* The returned promise settles once every provider has accepted or
* rejected the record, so callers that need confirmed delivery can await it.
*
* @param {AuditRecordInput} input
* @returns {Promise<AuditRecord | undefined>} the dispatched record
*/
async record(input) {
if (!input || typeof input.action !== 'string' || input.action.length === 0) {
console.error('AuditLogger.record called without an action');

return undefined;
}

const outcome = OUTCOMES.has(input.outcome) ? input.outcome : 'success';
// stamp the time of the action itself, before any asynchronous identity lookup
const timestamp = new Date().toISOString();
const record = {
id: uuid(),
source: SOURCE,
timestamp,
action: input.action,
outcome,
actor: await this.#resolveActor(),
target: this.#normalizeTarget(input.target),
details: input.details ? { ...input.details } : {}
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
};

await this.#dispatch(record);

return record;
}

/**
* @returns {Promise<AuditActor>}
*/
async #resolveActor() {
const actor = { id: null, username: null, role: null };
const userAPI = this.#openmct.user;

if (!userAPI?.hasProvider?.()) {
return actor;
}

try {
// capture the role synchronously so it reflects the moment of the action
actor.role = userAPI.getActiveRole?.() ?? null;
const user = await userAPI.getCurrentUser();
if (user) {
actor.id = user.getId?.() ?? null;
actor.username = user.getName?.() ?? null;
}
} catch (error) {
console.error('AuditLogger could not resolve the current user:', error);
}

return actor;
}

/**
* @param {Identifier | string | undefined} target
* @returns {string | null}
*/
#normalizeTarget(target) {
if (target === undefined || target === null) {
return null;
}

if (typeof target === 'string') {
return target;
}

try {
return this.#openmct.objects.makeKeyString(target);
} catch (error) {
return null;
}
}

/**
* @param {AuditRecord} record
* @returns {Promise<void>} settles when every provider has settled
*/
#dispatch(record) {
const deliveries = [];
for (const provider of this.#providers) {
try {
const result = provider.record(record);
if (typeof result?.then === 'function') {
deliveries.push(
result.then(undefined, (error) => {
console.error('Audit provider failed to accept record:', error);
})
);
}
} catch (error) {
console.error('Audit provider failed to accept record:', error);
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
}
}

// emitted through the EventEmitter so on/once/off semantics are preserved
try {
this.emit('record', record);
} catch (error) {
console.error('Audit record listener failed:', error);
}

return Promise.all(deliveries).then(() => undefined);
}
}
Loading
Loading