apply requested changes

Signed-off-by: Paul Schultz <pschultz@pobox.com>
This commit is contained in:
Paul Schultz
2024-12-03 15:02:59 -06:00
parent e0ae80edee
commit a3f0bd7a6a
9 changed files with 122 additions and 62 deletions
+1 -1
View File
@@ -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
View File
@@ -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. */
+36 -16
View File
@@ -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,
});
+23 -12
View File
@@ -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,
});