Skip to content
Open
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
31 changes: 31 additions & 0 deletions enterprise-admin-session-redaction-guard/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Enterprise Admin Session Redaction Guard

Issue: [#19 Enterprise Tooling](https://github.com/SCIBASE-AI/SCIBASE.AI/issues/19)

This module adds an enterprise governance slice for recorded admin sessions. It checks whether privileged session replays are safe to export or share with institutional auditors by validating redaction, retention, access scope, and evidence completeness.

## Why this fits enterprise tooling

Universities and R&D organizations need auditability without exposing credentials, patient identifiers, billing records, embargoed project data, or identity-provider secrets. Session replay is useful for incident review, but only if the replay package proves that sensitive frames were masked and that reviewer access is bounded.

## Guard coverage

- Detects unredacted secrets, tokens, passwords, patient identifiers, billing accounts, private emails, and embargo markers in admin-session events.
- Requires ticket, reason, and reviewer approval for critical privileged actions.
- Blocks raw playback retention above 30 days unless an explicit legal hold is attached.
- Requires export bundles to include an audit digest, redaction manifest, and viewer access policy.
- Limits shared replay links to approved groups and short-lived expiry windows.

## Local verification

```bash
node test.js
node demo.js
```

Demo artifacts:

- `demo.svg` shows the guard scope and expected verification result.
- `demo.mp4` is a short video rendering of the same verification summary for bounty review.

No external services, private data, credentials, package installs, or live payment integrations are required.
42 changes: 42 additions & 0 deletions enterprise-admin-session-redaction-guard/demo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"use strict";

const { evaluateSessionRecording } = require("./index");

const demoRecording = {
recordingId: "rec-demo-019",
workspaceId: "enterprise-research-tenant",
actor: "it-admin@research.example",
events: [
{
id: "open-idp-settings",
type: "identity_provider_change",
reason: "Migrate institution SSO metadata",
ticketId: "IAM-2048",
reviewerApproval: "review-113",
contains: ["token", "email"],
redaction: { mode: "tokenized", evidenceId: "redact-idp-2048" },
},
{
id: "export-audit-trail",
type: "data_export",
reason: "External auditor requested tenant admin activity packet",
ticketId: "AUD-552",
reviewerApproval: "review-114",
contains: ["billing_account"],
redaction: { mode: "masked", evidenceId: "redact-aud-552" },
},
],
retention: { rawDays: 21, metadataDays: 730 },
exportBundle: {
auditDigestId: "digest-demo-019",
redactionManifestId: "manifest-demo-019",
viewerAccessPolicyId: "viewer-policy-demo-019",
},
share: {
enabled: true,
groups: ["security-reviewers", "incident-commanders"],
expiresInHours: 24,
},
};

console.log(JSON.stringify(evaluateSessionRecording(demoRecording), null, 2));
Binary file not shown.
20 changes: 20 additions & 0 deletions enterprise-admin-session-redaction-guard/demo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
195 changes: 195 additions & 0 deletions enterprise-admin-session-redaction-guard/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
"use strict";

const SENSITIVE_MARKERS = new Set([
"api_key",
"billing_account",
"email",
"embargoed_project",
"password",
"patient_identifier",
"secret",
"token",
]);

const CRITICAL_ACTIONS = new Set([
"billing_export",
"data_export",
"identity_provider_change",
"policy_change",
"role_change",
]);

const ALLOWED_SHARE_GROUPS = new Set([
"security-reviewers",
"compliance-auditors",
"incident-commanders",
]);

function evaluateSessionRecording(recording, options = {}) {
const findings = [];
const maxRawRetentionDays = options.maxRawRetentionDays ?? 30;
const maxShareHours = options.maxShareHours ?? 72;

requireField(recording, "recordingId", findings);
requireField(recording, "workspaceId", findings);
requireField(recording, "actor", findings);

const events = Array.isArray(recording.events) ? recording.events : [];
if (events.length === 0) {
findings.push(finding("hold", "events.empty", "No admin-session events were provided."));
}

for (const event of events) {
inspectSensitiveEvent(event, findings);
inspectCriticalAction(event, findings);
}

inspectRetention(recording.retention || {}, maxRawRetentionDays, findings);
inspectExportBundle(recording.exportBundle || {}, findings);
inspectSharing(recording.share || {}, maxShareHours, findings);

const severityRank = { info: 0, revise: 1, hold: 2 };
const highest = findings.reduce((max, item) => Math.max(max, severityRank[item.severity]), 0);
const decision = highest === 2 ? "hold" : highest === 1 ? "revise" : "release";
const score = Math.max(0, 100 - findings.reduce((total, item) => total + (item.severity === "hold" ? 30 : 12), 0));

return {
decision,
score,
findings,
requiredActions: findings
.filter((item) => item.severity !== "info")
.map((item) => item.action),
auditPacket: {
recordingId: recording.recordingId || "unknown",
workspaceId: recording.workspaceId || "unknown",
checkedAt: new Date("2026-07-05T00:00:00.000Z").toISOString(),
sensitiveEventCount: events.filter((event) => hasSensitiveMarkers(event)).length,
criticalActionCount: events.filter((event) => CRITICAL_ACTIONS.has(event.type)).length,
exportReady: decision === "release",
},
};
}

function requireField(recording, field, findings) {
if (!recording || !recording[field]) {
findings.push(finding("revise", `recording.${field}.missing`, `Missing ${field}.`));
}
}

function inspectSensitiveEvent(event, findings) {
const markers = sensitiveMarkers(event);
if (markers.length === 0) return;

if (!event.redaction || !["masked", "tokenized"].includes(event.redaction.mode)) {
findings.push(finding(
"hold",
"redaction.missing",
`Event ${event.id || event.type || "unknown"} exposes sensitive markers: ${markers.join(", ")}.`,
"Mask or tokenize all sensitive fields before session replay can be exported."
));
}

if (!event.redaction || !event.redaction.evidenceId) {
findings.push(finding(
"revise",
"redaction.evidence.missing",
`Event ${event.id || event.type || "unknown"} lacks redaction evidence.`,
"Attach a redaction evidence id for auditor traceability."
));
}
}

function inspectCriticalAction(event, findings) {
if (!CRITICAL_ACTIONS.has(event.type)) return;

if (!event.reason || !event.ticketId) {
findings.push(finding(
"hold",
"critical_action.context.missing",
`Critical action ${event.type} lacks reason or ticket id.`,
"Add a business reason and linked ticket for the privileged action."
));
}

if (!event.reviewerApproval) {
findings.push(finding(
"revise",
"critical_action.approval.missing",
`Critical action ${event.type} lacks reviewer approval evidence.`,
"Attach reviewer approval before sharing the replay with enterprise auditors."
));
}
}

function inspectRetention(retention, maxRawRetentionDays, findings) {
if (typeof retention.rawDays !== "number") {
findings.push(finding("revise", "retention.raw_days.missing", "Raw playback retention is not declared."));
return;
}

if (retention.rawDays > maxRawRetentionDays && !retention.legalHoldId) {
findings.push(finding(
"hold",
"retention.raw_days.too_long",
`Raw playback retention is ${retention.rawDays} days without legal hold.`,
`Reduce raw playback retention to ${maxRawRetentionDays} days or attach a legal hold id.`
));
}
}

function inspectExportBundle(bundle, findings) {
for (const field of ["auditDigestId", "redactionManifestId", "viewerAccessPolicyId"]) {
if (!bundle[field]) {
findings.push(finding(
"revise",
`export.${field}.missing`,
`Export bundle is missing ${field}.`,
"Include a complete export bundle for enterprise review."
));
}
}
}

function inspectSharing(share, maxShareHours, findings) {
if (!share.enabled) return;

const groups = Array.isArray(share.groups) ? share.groups : [];
const unknownGroups = groups.filter((group) => !ALLOWED_SHARE_GROUPS.has(group));
if (unknownGroups.length > 0) {
findings.push(finding(
"hold",
"sharing.group.unapproved",
`Replay shared with unapproved groups: ${unknownGroups.join(", ")}.`,
"Restrict replay sharing to approved security, compliance, or incident groups."
));
}

if (typeof share.expiresInHours !== "number" || share.expiresInHours > maxShareHours) {
findings.push(finding(
"revise",
"sharing.expiry.invalid",
"Replay share expiry is missing or too long.",
`Set replay sharing expiry to ${maxShareHours} hours or less.`
));
}
}

function sensitiveMarkers(event) {
const markers = Array.isArray(event.contains) ? event.contains : [];
return markers.filter((marker) => SENSITIVE_MARKERS.has(marker));
}

function hasSensitiveMarkers(event) {
return sensitiveMarkers(event).length > 0;
}

function finding(severity, code, message, action = "Update the session recording package and rerun the guard.") {
return { severity, code, message, action };
}

module.exports = {
evaluateSessionRecording,
SENSITIVE_MARKERS,
CRITICAL_ACTIONS,
};
86 changes: 86 additions & 0 deletions enterprise-admin-session-redaction-guard/test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
"use strict";

const assert = require("node:assert/strict");
const { evaluateSessionRecording } = require("./index");

const baseRecording = {
recordingId: "rec-enterprise-001",
workspaceId: "institution-lab-7",
actor: "admin@example.edu",
events: [
{
id: "evt-1",
type: "role_change",
reason: "Temporary incident commander access",
ticketId: "SEC-1842",
reviewerApproval: "approval-771",
contains: ["email"],
redaction: { mode: "masked", evidenceId: "redact-evt-1" },
},
{
id: "evt-2",
type: "identity_provider_change",
reason: "Rotate SAML signing certificate",
ticketId: "IAM-390",
reviewerApproval: "approval-772",
contains: ["token"],
redaction: { mode: "tokenized", evidenceId: "redact-evt-2" },
},
],
retention: { rawDays: 14, metadataDays: 365 },
exportBundle: {
auditDigestId: "audit-digest-1",
redactionManifestId: "manifest-1",
viewerAccessPolicyId: "policy-1",
},
share: {
enabled: true,
groups: ["security-reviewers", "compliance-auditors"],
expiresInHours: 48,
},
};

function clone(value) {
return JSON.parse(JSON.stringify(value));
}

{
const result = evaluateSessionRecording(baseRecording);
assert.equal(result.decision, "release");
assert.equal(result.auditPacket.exportReady, true);
assert.equal(result.findings.length, 0);
}

{
const recording = clone(baseRecording);
delete recording.events[1].redaction;
const result = evaluateSessionRecording(recording);
assert.equal(result.decision, "hold");
assert(result.findings.some((item) => item.code === "redaction.missing"));
}

{
const recording = clone(baseRecording);
recording.retention.rawDays = 90;
const result = evaluateSessionRecording(recording);
assert.equal(result.decision, "hold");
assert(result.requiredActions.some((action) => action.includes("legal hold")));
}

{
const recording = clone(baseRecording);
delete recording.exportBundle.viewerAccessPolicyId;
const result = evaluateSessionRecording(recording);
assert.equal(result.decision, "revise");
assert(result.findings.some((item) => item.code === "export.viewerAccessPolicyId.missing"));
}

{
const recording = clone(baseRecording);
recording.share.groups.push("all-staff");
const result = evaluateSessionRecording(recording);
assert.equal(result.decision, "hold");
assert(result.findings.some((item) => item.code === "sharing.group.unapproved"));
}

console.log("enterprise admin session redaction guard tests passed");