1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-12 14:34:02 +00:00

collect events from cipher form

This commit is contained in:
✨ Audrey ✨
2025-03-18 18:13:18 -04:00
parent ae4f0a7ee4
commit b4eaf3348c
6 changed files with 159 additions and 16 deletions

View File

@@ -1,5 +1,18 @@
import { Primitive } from "type-fest";
export type EcsEventType =
| "access"
| "admin"
| "allowed"
| "creation"
| "deletion"
| "denied"
| "end"
| "error"
| "info"
| "start"
| "user";
/** Elastic Common Schema log format - core fields.
*/
export interface EcsFormat {
@@ -18,18 +31,7 @@ export interface EcsFormat {
event: {
kind?: "alert" | "enrichment" | "event" | "metric" | "state";
category?: "api" | "authentication" | "iam" | "process" | "session";
type?:
| "access"
| "admin"
| "allowed"
| "creation"
| "deletion"
| "denied"
| "end"
| "error"
| "info"
| "start"
| "user";
type?: EcsEventType;
outcome?: "failure" | "success" | "unknown";
};
}

View File

@@ -1,4 +1,4 @@
export { EcsFormat } from "./core";
export { EcsFormat, EcsEventType } from "./core";
export { ErrorFormat } from "./error";
export { EventFormat } from "./event";
export { LogFormat } from "./log";

View File

@@ -3,8 +3,10 @@ import { EcsFormat } from "./core";
export type ServiceFormat = EcsFormat & {
/** documents the program providing the log */
service: {
/** Which kind of client is it? */
name: "android" | "cli" | "desktop" | "extension" | "ios" | "web";
/** Which kind of client is it?
* @remarks this contains the output of `BrowserPlatformUtilsService.getDeviceString()` in practice.
*/
name: string;
/** identifies the service as a type of client device */
type: "client";
@@ -18,6 +20,6 @@ export type ServiceFormat = EcsFormat & {
environment: "production" | "testing" | "development" | "local";
/** the unique identifier(s) for this client installation */
version: "2025.3.1-innovation-sprint";
version: string;
};
};

View File

@@ -0,0 +1,113 @@
import { BehaviorSubject, SubjectLike, filter, first, from, map, zip } from "rxjs";
import { Primitive } from "type-fest";
import { Account, AccountService } from "../../auth/abstractions/account.service";
import { AppIdService } from "../../platform/abstractions/app-id.service";
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
import { UserActionEvent } from "../achievements/types";
import { ServiceFormat, UserFormat, EcsEventType } from "./ecs-format";
import { SemanticLogger } from "./semantic-logger.abstraction";
export abstract class UserEventLogProvider {
abstract create: (account: Account) => UserEventLogger;
}
type BaselineType = Omit<ServiceFormat & UserFormat, "@timestamp">;
type EventInfo = {
action: string;
labels?: Record<string, Primitive>;
tags?: Array<string>;
};
export class UserEventLogger {
constructor(
idService: AppIdService,
utilService: PlatformUtilsService,
accountService: AccountService,
private now: () => number,
private log: SemanticLogger,
private events$: SubjectLike<UserActionEvent>,
) {
zip(
from(idService.getAppId()),
from(utilService.getApplicationVersion()),
accountService.activeAccount$.pipe(
filter((account) => !!account),
first(),
),
)
.pipe(
map(
([appId, version, account]) =>
({
event: {
kind: "event",
category: "session",
},
service: {
name: utilService.getDeviceString(),
type: "client",
node: {
name: appId,
},
environment: "local",
version,
},
user: {
// `account` verified not-null via `filter`
id: account!.id,
email: (account!.emailVerified && account!.email) || undefined,
},
}) satisfies BaselineType,
),
)
.subscribe((next) => this.baseline$.next(next));
}
private readonly baseline$ = new BehaviorSubject<BaselineType | null>(null);
creation(event: EventInfo) {
this.collect("creation", event);
}
deletion(event: EventInfo) {
this.collect("deletion", event);
}
info(event: EventInfo) {
this.collect("info", event);
}
access(event: EventInfo) {
this.collect("access", event);
}
private collect(type: EcsEventType, info: EventInfo) {
const { value: baseline } = this.baseline$;
if (!baseline) {
// TODO: buffer logs and stream them when `baseline$` becomes available.
this.log.error("baseline log not available; dropping user event");
return;
}
const event = structuredClone(this.baseline$.value) as UserActionEvent;
event["@timestamp"] = this.now();
event.event.type = type;
event.action = info.action;
event.tags = info.tags && info.tags.filter((t) => !!t);
if (info.labels) {
const entries = Object.keys(info.labels)
.filter((k) => !!info.labels![k])
.map((k) => [k, info.labels![k]] as const);
const labels = Object.fromEntries(entries);
event.labels = labels;
}
this.events$.next(event);
}
}

View File

@@ -2,6 +2,7 @@ import { PolicyService } from "../admin-console/abstractions/policy/policy.servi
import { ExtensionService } from "./extension/extension.service";
import { LogProvider } from "./log";
import { UserEventLogProvider } from "./log/logger";
/** Provides access to commonly-used cross-cutting services. */
export type SystemServiceProvider = {
@@ -13,4 +14,6 @@ export type SystemServiceProvider = {
/** Event monitoring and diagnostic interfaces */
readonly log: LogProvider;
readonly event: UserEventLogProvider;
};