apply requested changes
Signed-off-by: Paul Schultz <pschultz@pobox.com>
This commit is contained in:
@@ -7,7 +7,7 @@
|
||||
'@backstage/plugin-scaffolder-node': minor
|
||||
---
|
||||
|
||||
feat: add auditor to coreServices
|
||||
feat: add auditor to `coreServices`
|
||||
|
||||
This change introduces a new `auditor` service to the `coreServices` in Backstage.
|
||||
The auditor service enables plugins to emit audit events for security-relevant actions.
|
||||
|
||||
-2
@@ -635,13 +635,11 @@ export interface Config {
|
||||
auditor?: {
|
||||
/**
|
||||
* Configuration for the auditing to the console
|
||||
* @visibility frontend
|
||||
*/
|
||||
console: {
|
||||
/**
|
||||
* Enables auditing to console
|
||||
* @default true
|
||||
* @visibility frontend
|
||||
*/
|
||||
enabled: boolean;
|
||||
};
|
||||
|
||||
@@ -18,8 +18,6 @@ import type { JsonObject } from '@backstage/types';
|
||||
import type { Request } from 'express';
|
||||
|
||||
/**
|
||||
* TODO: Rigorously define each level
|
||||
*
|
||||
* low (default): normal usage
|
||||
* medium: accessing write endpoints
|
||||
* high: non-root permission changes
|
||||
@@ -31,12 +29,20 @@ export type AuditorEventSeverityLevel = 'low' | 'medium' | 'high' | 'critical';
|
||||
/** @public */
|
||||
export type AuditorCreateEvent<TRootMeta extends JsonObject> = (options: {
|
||||
/**
|
||||
* Use kebab-case to name audit events (e.g., "user-login", "file-download").
|
||||
* Use kebab-case to name audit events (e.g., "user-login", "file-download", "fetch"). Represents a logical group of similar events or operations. For example, "fetch" could be used as an eventId encompassing various fetch methods like "by-id" or "by-location".
|
||||
*
|
||||
* The `pluginId` already provides plugin/module context, so avoid redundant prefixes in the `eventId`.
|
||||
*/
|
||||
eventId: string;
|
||||
|
||||
/**
|
||||
* Use kebab-case to name sub-events (e.g., "by-id", "by-user").
|
||||
*
|
||||
* (Optional) The ID for a sub-event or related action within the main event. This allows further categorization of events within a logical group. For example, if the `eventId` is "fetch", the `subEventId` could be "by-id" or "by-location" to specify the method used for fetching.
|
||||
*/
|
||||
subEventId?: string;
|
||||
|
||||
/** (Optional) The severity level for the audit event. */
|
||||
severityLevel?: AuditorEventSeverityLevel;
|
||||
|
||||
/** (Optional) The associated HTTP request, if applicable. */
|
||||
|
||||
@@ -89,22 +89,42 @@ some example entities.
|
||||
|
||||
## Audit Events
|
||||
|
||||
- **`ancestry-fetch`**: Tracks `GET` requests to the `/entities/by-name/:kind/:namespace/:name/ancestry` endpoint which return the ancestry of an entity.
|
||||
- **`batch-fetch`**: Tracks `POST` requests to the `/entities/by-refs` endpoint which return a batch of entities.
|
||||
- **`delete`**: Tracks `DELETE` requests to the `/entities/by-uid/:uid` endpoint which delete an entity. Note: this will not be a permanent deletion and the entity will be restored if the parent location is still present in the catalog.
|
||||
- **`facet-fetch`**: Tracks `GET` requests to the `/entity-facets` endpoint which return the facets of an entity.
|
||||
- **`fetch`**: Tracks `GET` requests to the `/entities` endpoint which returns a list of entities.
|
||||
- **`fetch-by-name`**: Tracks `GET` requests to the `/entities/by-name/:kind/:namespace/:name` endpoint which return an entity matching the specified entity ref.
|
||||
- **`fetch-by-uid`**: Tracks `GET` requests to the `/entities/by-uid/:uid` endpoint which return an entity matching the specified entity uid.
|
||||
- **`fetch-by-query`**: Tracks `GET` requests to the `/entities/by-query` endpoint which returns a list of entities matching the specified query.
|
||||
- **`refresh`**: Tracks `POST` requests to the `/entities/refresh` endpoint which schedules the specified entity to be refreshed.
|
||||
- **`validate`**: Tracks `POST` requests to the `/entities/validate` endpoint which validates the specified entity.
|
||||
- **`location-analyze`**: Tracks `POST` requests to the `/locations/analyze` endpoint which analyzes the specified location.
|
||||
- **`location-create`**: Tracks `POST` requests to the `/locations` endpoint which creates a location.
|
||||
- **`location-delete`**: Tracks `DELETE` requests to the `/locations/:id` endpoint which deletes a location as well as all child entities associated with it.
|
||||
- **`location-fetch`**: Tracks `GET` requests to the `/locations` endpoint which returns a list of locations.
|
||||
- **`location-fetch-by-entity-ref`**: Tracks `GET` requests to the `/locations/by-entity` endpoint which returns a list of locations associated with the specified entity ref.
|
||||
- **`location-fetch-by-id`**: Tracks `GET` requests to the `/locations/:id` endpoint which returns a location matching the specified location id.
|
||||
The Catalog backend emits audit events for various operations. Events are grouped logically by `eventId`, with `subEventId` providing further distinction within an operation group.
|
||||
|
||||
**Entity Events:**
|
||||
|
||||
- **`entity-fetch`**: Retrieves entities.
|
||||
|
||||
- **`all`**: Fetching all entities. (GET `/entities`)
|
||||
- **`by-id`**: Fetching a single entity using its UID. (GET `/entities/by-uid/:uid`)
|
||||
- **`by-name`**: Fetching a single entity using its kind, namespace, and name. (GET `/entities/by-name/:kind/:namespace/:name`)
|
||||
- **`by-query`**: Fetching multiple entities using a filter query. (GET `/entities/by-query`)
|
||||
- **`by-refs`**: Fetching a batch of entities by their entity refs. (POST `/entities/by-refs`)
|
||||
- **`ancestry`**: Fetching the ancestry of an entity. (GET `/entities/by-name/:kind/:namespace/:name/ancestry`)
|
||||
|
||||
- **`entity-mutate`**: Modifies entities.
|
||||
|
||||
- **`delete`**: Deleting a single entity. Note: this will not be a permanent deletion and the entity will be restored if the parent location is still present in the catalog. (DELETE `/entities/by-uid/:uid`)
|
||||
- **`refresh`**: Scheduling an entity refresh. (POST `/entities/refresh`)
|
||||
|
||||
- **`entity-validate`**: Validates an entity. (POST `/entities/validate`)
|
||||
|
||||
- **`entity-facets`**: Retrieves entity facets. (GET `/entity-facets`)
|
||||
|
||||
**Location Events:**
|
||||
|
||||
- **`location-fetch`**: Retrieves locations.
|
||||
|
||||
- **`all`**: Fetching all locations. (GET `/locations`)
|
||||
- **`by-id`**: Fetching a single location by ID. (GET `/locations/:id`)
|
||||
- **`by-entity`**: Fetching locations associated with an entity ref. (GET `/locations/by-entity`)
|
||||
|
||||
- **`location-mutate`**: Modifies locations.
|
||||
|
||||
- **`create`**: Creating a new location. (POST `/locations`)
|
||||
- **`delete`**: Deleting a location and its associated entities. (DELETE `/locations/:id`)
|
||||
|
||||
- **`location-analyze`**: Analyzes a location. (POST `/locations/analyze`)
|
||||
|
||||
## Links
|
||||
|
||||
|
||||
@@ -128,7 +128,9 @@ export async function createRouter(
|
||||
const { authorizationToken, ...restBody } = req.body;
|
||||
|
||||
const auditorEvent = await auditor?.createEvent({
|
||||
eventId: 'refresh',
|
||||
eventId: 'entity-mutate',
|
||||
subEventId: 'refresh',
|
||||
severityLevel: 'medium',
|
||||
meta: {
|
||||
entityRef: restBody.entityRef,
|
||||
},
|
||||
@@ -162,7 +164,8 @@ export async function createRouter(
|
||||
router
|
||||
.get('/entities', async (req, res) => {
|
||||
const auditorEvent = await auditor?.createEvent({
|
||||
eventId: 'CatalogEntityFetch',
|
||||
eventId: 'entity-fetch',
|
||||
subEventId: 'all',
|
||||
request: req,
|
||||
});
|
||||
|
||||
@@ -256,7 +259,8 @@ export async function createRouter(
|
||||
})
|
||||
.get('/entities/by-query', async (req, res) => {
|
||||
const auditorEvent = await auditor?.createEvent({
|
||||
eventId: 'fetch-by-query',
|
||||
eventId: 'entity-fetch',
|
||||
subEventId: 'by-query',
|
||||
request: req,
|
||||
});
|
||||
|
||||
@@ -306,7 +310,8 @@ export async function createRouter(
|
||||
const { uid } = req.params;
|
||||
|
||||
const auditorEvent = await auditor?.createEvent({
|
||||
eventId: 'fetch-by-uid',
|
||||
eventId: 'entity-fetch',
|
||||
subEventId: 'by-uid',
|
||||
request: req,
|
||||
meta: {
|
||||
uid: uid,
|
||||
@@ -337,7 +342,8 @@ export async function createRouter(
|
||||
const { uid } = req.params;
|
||||
|
||||
const auditorEvent = await auditor?.createEvent({
|
||||
eventId: 'delete',
|
||||
eventId: 'entity-mutate',
|
||||
subEventId: 'delete',
|
||||
severityLevel: 'medium',
|
||||
request: req,
|
||||
meta: {
|
||||
@@ -365,7 +371,8 @@ export async function createRouter(
|
||||
const entityRef = stringifyEntityRef({ kind, namespace, name });
|
||||
|
||||
const auditorEvent = await auditor?.createEvent({
|
||||
eventId: 'fetch-by-name',
|
||||
eventId: 'entity-fetch',
|
||||
subEventId: 'by-name',
|
||||
request: req,
|
||||
meta: {
|
||||
entityRef: entityRef,
|
||||
@@ -399,7 +406,8 @@ export async function createRouter(
|
||||
const entityRef = stringifyEntityRef({ kind, namespace, name });
|
||||
|
||||
const auditorEvent = await auditor?.createEvent({
|
||||
eventId: 'ancestry-fetch',
|
||||
eventId: 'entity-fetch',
|
||||
subEventId: 'ancestry',
|
||||
request: req,
|
||||
meta: {
|
||||
entityRef: entityRef,
|
||||
@@ -434,7 +442,8 @@ export async function createRouter(
|
||||
)
|
||||
.post('/entities/by-refs', async (req, res) => {
|
||||
const auditorEvent = await auditor?.createEvent({
|
||||
eventId: 'batch-fetch',
|
||||
eventId: 'entity-fetch',
|
||||
subEventId: 'by-refs',
|
||||
request: req,
|
||||
});
|
||||
|
||||
@@ -470,7 +479,7 @@ export async function createRouter(
|
||||
})
|
||||
.get('/entity-facets', async (req, res) => {
|
||||
const auditorEvent = await auditor?.createEvent({
|
||||
eventId: 'facet-fetch',
|
||||
eventId: 'entity-facets',
|
||||
request: req,
|
||||
});
|
||||
|
||||
@@ -500,7 +509,8 @@ export async function createRouter(
|
||||
const dryRun = yn(req.query.dryRun, { default: false });
|
||||
|
||||
const auditorEvent = await auditor?.createEvent({
|
||||
eventId: 'location-create',
|
||||
eventId: 'location-mutate',
|
||||
subEventId: 'create',
|
||||
severityLevel: dryRun ? 'low' : 'medium',
|
||||
request: req,
|
||||
meta: {
|
||||
@@ -545,6 +555,7 @@ export async function createRouter(
|
||||
.get('/locations', async (req, res) => {
|
||||
const auditorEvent = await auditor?.createEvent({
|
||||
eventId: 'location-fetch',
|
||||
subEventId: 'all',
|
||||
request: req,
|
||||
});
|
||||
|
||||
@@ -568,7 +579,8 @@ export async function createRouter(
|
||||
const { id } = req.params;
|
||||
|
||||
const auditorEvent = await auditor?.createEvent({
|
||||
eventId: 'location-fetch-by-id',
|
||||
eventId: 'location-fetch',
|
||||
subEventId: 'by-id',
|
||||
request: req,
|
||||
meta: {
|
||||
id: id,
|
||||
@@ -598,7 +610,8 @@ export async function createRouter(
|
||||
const { id } = req.params;
|
||||
|
||||
const auditorEvent = await auditor?.createEvent({
|
||||
eventId: 'location-delete',
|
||||
eventId: 'location-mutate',
|
||||
subEventId: 'delete',
|
||||
severityLevel: 'medium',
|
||||
request: req,
|
||||
meta: {
|
||||
@@ -628,7 +641,8 @@ export async function createRouter(
|
||||
const locationRef = `${kind}:${namespace}/${name}`;
|
||||
|
||||
const auditorEvent = await auditor?.createEvent({
|
||||
eventId: 'location-fetch-by-entity-ref',
|
||||
eventId: 'location-fetch',
|
||||
subEventId: 'by-entity',
|
||||
request: req,
|
||||
meta: {
|
||||
locationRef: locationRef,
|
||||
@@ -713,7 +727,7 @@ export async function createRouter(
|
||||
if (orchestrator) {
|
||||
router.post('/validate-entity', async (req, res) => {
|
||||
const auditorEvent = await auditor?.createEvent({
|
||||
eventId: 'validate',
|
||||
eventId: 'entity-validate',
|
||||
request: req,
|
||||
});
|
||||
|
||||
|
||||
@@ -66,15 +66,26 @@ catalog locations from the [create-app template](https://github.com/backstage/ba
|
||||
|
||||
## Audit Events
|
||||
|
||||
- **`parameter-schema-fetch`**: Tracks`GET` requests to the `/v2/templates/:namespace/:kind/:name/parameter-schema` endpoint which return template parameter schemas
|
||||
- **`installed-actions-fetch`**: Tracks`GET` requests to the `/v2/actions` endpoint which grabs the list of installed actions
|
||||
- **`task-creation`**: Tracks`POST` requests to the `/v2/tasks` endpoint which creates tasks that the scaffolder executes
|
||||
- **`task-list-fetch`**: Tracks`GET` requests to the `/v2/tasks` endpoint which fetches details of all tasks in the scaffolder.
|
||||
- **`task-fetch`**: Tracks`GET` requests to the `/v2/tasks/:taskId` endpoint which fetches details of a specified task `:taskId`
|
||||
- **`task-cancellation`**: Tracks`POST` requests to the `/v2/tasks/:taskId/cancel` endpoint which cancels a running task
|
||||
- **`task-retry`**: Tracks`POST` requests to the `/v2/tasks/:taskId/retry` endpoint which retries a failed task
|
||||
- **`task-stream`**: Tracks`GET` requests to the `/v2/tasks/:taskId/eventstream` endpoint which returns an event stream of the task logs of task `:taskId`
|
||||
- **`task-event-fetch`**: Tracks`GET` requests to the `/v2/tasks/:taskId/events` endpoint which returns a snapshot of the task logs of task `:taskId`
|
||||
- **`task-dry-run`**: Tracks`POST` requests to the `/v2/dry-run` endpoint which creates a dry-run task. All audit logs for events associated with dry runs have the `meta.isDryLog` flag set to `true`.
|
||||
- **`stale-task-cancellation`**: Tracks automated cancellation of stale tasks
|
||||
- **`task-execution`**: Tracks the`initiation` and `completion` of a real scaffolder task execution (will not occur during dry runs)
|
||||
The Scaffolder backend emits audit events for various operations. Events are grouped logically by `eventId`, with `subEventId` providing further distinction when needed.
|
||||
|
||||
**Template Events:**
|
||||
|
||||
- **`template-parameter-schema`**: Retrieves template parameter schemas. (GET `/v2/templates/:namespace/:kind/:name/parameter-schema`)
|
||||
|
||||
**Action Events:**
|
||||
|
||||
- **`action-fetch`**: Retrieves installed actions. (GET `/v2/actions`)
|
||||
|
||||
**Task Events:**
|
||||
|
||||
- **`task`**: Operations related to Scaffolder tasks.
|
||||
- **`create`**: Creates a new task. (POST `/v2/tasks`)
|
||||
- **`list`**: Fetches details of all tasks. (GET `/v2/tasks`)
|
||||
- **`get`**: Fetches details of a specific task. (GET `/v2/tasks/:taskId`)
|
||||
- **`cancel`**: Cancels a running task. (POST `/v2/tasks/:taskId/cancel`)
|
||||
- **`retry`**: Retries a failed task. (POST `/v2/tasks/:taskId/retry`)
|
||||
- **`stream`**: Retrieves a stream of task logs. (GET `/v2/tasks/:taskId/eventstream`)
|
||||
- **`events`**: Retrieves a snapshot of task logs. (GET `/v2/tasks/:taskId/events`)
|
||||
- **`dry-run`**: Creates a dry-run task. (POST `/v2/dry-run`) All audit logs for events associated with dry runs have the `meta.isDryLog` flag set to `true`.
|
||||
- **`stale-cancel`**: Automated cancellation of stale tasks.
|
||||
- **`execute`**: Tracks the initiation and completion of a real scaffolder task execution. This event will not occur during dry runs.
|
||||
|
||||
@@ -40,9 +40,9 @@ import {
|
||||
} from '@backstage/types';
|
||||
import { Logger } from 'winston';
|
||||
import ObservableImpl from 'zen-observable';
|
||||
import { DefaultWorkspaceService, WorkspaceService } from './WorkspaceService';
|
||||
import { readDuration } from './helper';
|
||||
import { InternalTaskSecrets, TaskStore } from './types';
|
||||
import { DefaultWorkspaceService, WorkspaceService } from './WorkspaceService';
|
||||
|
||||
type TaskState = {
|
||||
checkpoints: {
|
||||
@@ -206,7 +206,8 @@ export class TaskManager implements TaskContext {
|
||||
}
|
||||
|
||||
const auditorEvent = await this.auditor?.createEvent({
|
||||
eventId: 'task-execution',
|
||||
eventId: 'task',
|
||||
subEventId: 'execution',
|
||||
severityLevel: 'medium',
|
||||
meta: {
|
||||
taskId: this.task.taskId,
|
||||
@@ -472,7 +473,8 @@ export class StorageTaskBroker implements TaskBroker {
|
||||
await Promise.all(
|
||||
tasks.map(async task => {
|
||||
const auditorEvent = await this.auditor?.createEvent({
|
||||
eventId: 'stale-task-cancellation',
|
||||
eventId: 'task',
|
||||
subEventId: 'stale-cancel',
|
||||
severityLevel: 'medium',
|
||||
meta: {
|
||||
taskId: task.taskId,
|
||||
|
||||
@@ -175,7 +175,8 @@ export class TaskWorker {
|
||||
|
||||
async runOneTask(task: TaskContext) {
|
||||
await this.auditor?.createEvent({
|
||||
eventId: 'task-execution',
|
||||
eventId: 'task',
|
||||
subEventId: 'execution',
|
||||
severityLevel: 'medium',
|
||||
meta: {
|
||||
taskId: task.taskId,
|
||||
|
||||
@@ -462,7 +462,7 @@ export async function createRouter(
|
||||
const requestedTemplateRef = `${req.params.kind}:${req.params.namespace}/${req.params.name}`;
|
||||
|
||||
const auditorEvent = await auditor?.createEvent({
|
||||
eventId: 'parameter-schema-fetch',
|
||||
eventId: 'template-parameter-schema',
|
||||
request: req,
|
||||
meta: { templateRef: requestedTemplateRef },
|
||||
});
|
||||
@@ -512,7 +512,7 @@ export async function createRouter(
|
||||
)
|
||||
.get('/v2/actions', async (req, res) => {
|
||||
const auditorEvent = await auditor?.createEvent({
|
||||
eventId: 'installed-actions-fetch',
|
||||
eventId: 'action-fetch',
|
||||
request: req,
|
||||
});
|
||||
|
||||
@@ -541,7 +541,8 @@ export async function createRouter(
|
||||
});
|
||||
|
||||
const auditorEvent = await auditor?.createEvent({
|
||||
eventId: 'installed-actions-fetch',
|
||||
eventId: 'task',
|
||||
subEventId: 'create',
|
||||
severityLevel: 'medium',
|
||||
request: req,
|
||||
meta: {
|
||||
@@ -646,7 +647,8 @@ export async function createRouter(
|
||||
})
|
||||
.get('/v2/tasks', async (req, res) => {
|
||||
const auditorEvent = await auditor?.createEvent({
|
||||
eventId: 'task-list-fetch',
|
||||
eventId: 'task',
|
||||
subEventId: 'list',
|
||||
request: req,
|
||||
});
|
||||
|
||||
@@ -708,7 +710,8 @@ export async function createRouter(
|
||||
const { taskId } = req.params;
|
||||
|
||||
const auditorEvent = await auditor?.createEvent({
|
||||
eventId: 'task-fetch',
|
||||
eventId: 'task',
|
||||
subEventId: 'get',
|
||||
request: req,
|
||||
meta: {
|
||||
taskId: taskId,
|
||||
@@ -742,7 +745,8 @@ export async function createRouter(
|
||||
const { taskId } = req.params;
|
||||
|
||||
const auditorEvent = await auditor?.createEvent({
|
||||
eventId: 'task-cancellation',
|
||||
eventId: 'task',
|
||||
subEventId: 'cancel',
|
||||
severityLevel: 'medium',
|
||||
request: req,
|
||||
meta: { taskId: taskId },
|
||||
@@ -771,7 +775,8 @@ export async function createRouter(
|
||||
const { taskId } = req.params;
|
||||
|
||||
const auditorEvent = await auditor?.createEvent({
|
||||
eventId: 'task-retry',
|
||||
eventId: 'task',
|
||||
subEventId: 'retry',
|
||||
severityLevel: 'medium',
|
||||
request: req,
|
||||
meta: { taskId: taskId },
|
||||
@@ -799,7 +804,8 @@ export async function createRouter(
|
||||
const { taskId } = req.params;
|
||||
|
||||
const auditorEvent = await auditor?.createEvent({
|
||||
eventId: 'task-stream',
|
||||
eventId: 'task',
|
||||
subEventId: 'stream',
|
||||
request: req,
|
||||
meta: { taskId: taskId },
|
||||
});
|
||||
@@ -868,7 +874,8 @@ export async function createRouter(
|
||||
const { taskId } = req.params;
|
||||
|
||||
const auditorEvent = await auditor?.createEvent({
|
||||
eventId: 'task-event-fetch',
|
||||
eventId: 'task',
|
||||
subEventId: 'events',
|
||||
request: req,
|
||||
meta: {
|
||||
taskId: taskId,
|
||||
@@ -919,7 +926,8 @@ export async function createRouter(
|
||||
})
|
||||
.post('/v2/dry-run', async (req, res) => {
|
||||
const auditorEvent = await auditor?.createEvent({
|
||||
eventId: 'task-dry-run',
|
||||
eventId: 'task',
|
||||
subEventId: 'dry-run',
|
||||
request: req,
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user