feat(research): add missed breach tracking system for framework effectiveness measurement
Implements comprehensive system for tracking governance framework false negatives: Backend: - src/models/MissedBreach.model.js - Schema with severity, cost tracking, miss reasons - src/controllers/missedBreach.controller.js - CRUD operations and statistics - src/routes/missedBreach.routes.js - Admin-only API endpoints - src/routes/index.js - Route integration at /api/admin/missed-breaches Functionality: - Report missed breaches with classification (NO_RULE_EXISTS, RULE_TOO_NARROW, etc.) - Track actual/estimated costs of missed violations - Calculate effectiveness rate: detected / (detected + missed) - Breakdown by miss reason with examples - Link to original audit logs where available Statistics: - Total missed breaches by severity - Average time to detection - Cost impact analysis - Effectiveness comparison vs audit logs Purpose: - Measure true framework detection rate (not just blocked actions) - Identify blind spots in governance rules - Calculate realistic cost avoidance (avoiding "framework theater") - Support research integrity claims with empirical data Related: Cross-environment audit sync (production metrics) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
1f57c13d98
commit
48913a43f7
4 changed files with 628 additions and 0 deletions
249
src/controllers/missedBreach.controller.js
Normal file
249
src/controllers/missedBreach.controller.js
Normal file
|
|
@ -0,0 +1,249 @@
|
|||
/**
|
||||
* Missed Breach Controller
|
||||
*
|
||||
* Tracks governance framework false negatives for research integrity
|
||||
*/
|
||||
|
||||
const MissedBreach = require('../models/MissedBreach.model');
|
||||
const AuditLog = require('../models/AuditLog.model');
|
||||
|
||||
/**
|
||||
* Report a new missed breach
|
||||
* POST /api/admin/missed-breaches
|
||||
*/
|
||||
async function reportMissedBreach(req, res) {
|
||||
try {
|
||||
const {
|
||||
title,
|
||||
incidentDate,
|
||||
severity,
|
||||
description,
|
||||
activityType,
|
||||
missReason,
|
||||
missReasonDetails,
|
||||
linkedAuditLog,
|
||||
sessionId,
|
||||
actualCost,
|
||||
estimatedCost,
|
||||
environment,
|
||||
tags
|
||||
} = req.body;
|
||||
|
||||
// Validate required fields
|
||||
if (!title || !incidentDate || !severity || !description || !activityType || !missReason) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Missing required fields: title, incidentDate, severity, description, activityType, missReason'
|
||||
});
|
||||
}
|
||||
|
||||
const missedBreach = new MissedBreach({
|
||||
title,
|
||||
incidentDate: new Date(incidentDate),
|
||||
severity,
|
||||
description,
|
||||
activityType,
|
||||
missReason,
|
||||
missReasonDetails,
|
||||
linkedAuditLog,
|
||||
sessionId,
|
||||
actualCost,
|
||||
estimatedCost,
|
||||
environment: environment || process.env.NODE_ENV || 'development',
|
||||
tags: tags || [],
|
||||
reportedBy: req.user?._id || null,
|
||||
status: 'REPORTED'
|
||||
});
|
||||
|
||||
await missedBreach.save();
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
missedBreach: missedBreach.toObject()
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error reporting missed breach:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to report missed breach'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all missed breaches
|
||||
* GET /api/admin/missed-breaches?status=VERIFIED&severity=HIGH
|
||||
*/
|
||||
async function getMissedBreaches(req, res) {
|
||||
try {
|
||||
const {
|
||||
status,
|
||||
severity,
|
||||
missReason,
|
||||
environment,
|
||||
startDate,
|
||||
endDate,
|
||||
limit = 100
|
||||
} = req.query;
|
||||
|
||||
const query = {};
|
||||
|
||||
if (status) query.status = status;
|
||||
if (severity) query.severity = severity;
|
||||
if (missReason) query.missReason = missReason;
|
||||
if (environment && environment !== 'all') query.environment = environment;
|
||||
|
||||
if (startDate && endDate) {
|
||||
query.incidentDate = {
|
||||
$gte: new Date(startDate),
|
||||
$lte: new Date(endDate)
|
||||
};
|
||||
}
|
||||
|
||||
const breaches = await MissedBreach.find(query)
|
||||
.sort({ incidentDate: -1 })
|
||||
.limit(parseInt(limit))
|
||||
.populate('reportedBy', 'name email')
|
||||
.populate('verifiedBy', 'name email')
|
||||
.lean();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
count: breaches.length,
|
||||
breaches
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fetching missed breaches:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to fetch missed breaches'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get missed breach statistics
|
||||
* GET /api/admin/missed-breaches/statistics
|
||||
*/
|
||||
async function getMissedBreachStatistics(req, res) {
|
||||
try {
|
||||
const { startDate, endDate } = req.query;
|
||||
|
||||
const start = startDate ? new Date(startDate) : new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
|
||||
const end = endDate ? new Date(endDate) : new Date();
|
||||
|
||||
// Get missed breach stats
|
||||
const missedStats = await MissedBreach.getStatistics(start, end);
|
||||
|
||||
// Get audit log stats for comparison
|
||||
const auditStats = await AuditLog.getStatistics(start, end);
|
||||
|
||||
// Calculate effectiveness rate
|
||||
const effectiveness = await MissedBreach.calculateEffectivenessRate(
|
||||
start,
|
||||
end,
|
||||
auditStats
|
||||
);
|
||||
|
||||
// Get breakdown by reason
|
||||
const byReason = await MissedBreach.getBreakdownByReason(start, end);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
dateRange: { start, end },
|
||||
missedBreaches: missedStats,
|
||||
auditLogs: auditStats,
|
||||
effectiveness,
|
||||
breakdownByReason: byReason
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fetching missed breach statistics:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to fetch statistics'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a missed breach (verify, remediate, etc.)
|
||||
* PATCH /api/admin/missed-breaches/:id
|
||||
*/
|
||||
async function updateMissedBreach(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const updates = req.body;
|
||||
|
||||
// If verifying, record who verified it
|
||||
if (updates.status === 'VERIFIED' && req.user) {
|
||||
updates.verifiedBy = req.user._id;
|
||||
}
|
||||
|
||||
const missedBreach = await MissedBreach.findByIdAndUpdate(
|
||||
id,
|
||||
updates,
|
||||
{ new: true, runValidators: true }
|
||||
);
|
||||
|
||||
if (!missedBreach) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Missed breach not found'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
missedBreach: missedBreach.toObject()
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error updating missed breach:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to update missed breach'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a missed breach (if it was a false positive)
|
||||
* DELETE /api/admin/missed-breaches/:id
|
||||
*/
|
||||
async function deleteMissedBreach(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const missedBreach = await MissedBreach.findByIdAndDelete(id);
|
||||
|
||||
if (!missedBreach) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Missed breach not found'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Missed breach deleted'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error deleting missed breach:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to delete missed breach'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
reportMissedBreach,
|
||||
getMissedBreaches,
|
||||
getMissedBreachStatistics,
|
||||
updateMissedBreach,
|
||||
deleteMissedBreach
|
||||
};
|
||||
346
src/models/MissedBreach.model.js
Normal file
346
src/models/MissedBreach.model.js
Normal file
|
|
@ -0,0 +1,346 @@
|
|||
/*
|
||||
* Copyright 2025 John G Stroh
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* MissedBreach Model
|
||||
*
|
||||
* Tracks incidents where the governance framework FAILED to catch a violation
|
||||
* Critical for research integrity and framework improvement
|
||||
*
|
||||
* Purpose:
|
||||
* - Measure framework effectiveness (true positive rate)
|
||||
* - Identify blind spots in governance rules
|
||||
* - Calculate realistic cost avoidance (detected vs missed)
|
||||
* - Improve framework through pattern analysis
|
||||
*/
|
||||
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
const missedBreachSchema = new mongoose.Schema({
|
||||
// Incident identification
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
description: 'Brief description of the missed breach'
|
||||
},
|
||||
|
||||
incidentDate: {
|
||||
type: Date,
|
||||
required: true,
|
||||
index: true,
|
||||
description: 'When the breach occurred (not when it was discovered)'
|
||||
},
|
||||
|
||||
discoveredDate: {
|
||||
type: Date,
|
||||
default: Date.now,
|
||||
index: true,
|
||||
description: 'When the breach was discovered'
|
||||
},
|
||||
|
||||
// Severity and impact
|
||||
severity: {
|
||||
type: String,
|
||||
enum: ['LOW', 'MEDIUM', 'HIGH', 'CRITICAL'],
|
||||
required: true,
|
||||
index: true,
|
||||
description: 'Actual severity of the breach'
|
||||
},
|
||||
|
||||
actualCost: {
|
||||
type: Number,
|
||||
default: null,
|
||||
description: 'Actual cost incurred (if known), in dollars'
|
||||
},
|
||||
|
||||
estimatedCost: {
|
||||
type: Number,
|
||||
default: null,
|
||||
description: 'Estimated cost if actual unknown, in dollars'
|
||||
},
|
||||
|
||||
// What happened
|
||||
description: {
|
||||
type: String,
|
||||
required: true,
|
||||
description: 'Detailed description of what went wrong'
|
||||
},
|
||||
|
||||
activityType: {
|
||||
type: String,
|
||||
enum: [
|
||||
'CLIENT_COMMUNICATION',
|
||||
'CODE_GENERATION',
|
||||
'DOCUMENTATION',
|
||||
'DEPLOYMENT',
|
||||
'COMPLIANCE_REVIEW',
|
||||
'DATA_MANAGEMENT',
|
||||
'INFRASTRUCTURE',
|
||||
'OTHER'
|
||||
],
|
||||
required: true,
|
||||
description: 'Type of activity that caused the breach'
|
||||
},
|
||||
|
||||
// Why it was missed
|
||||
missReason: {
|
||||
type: String,
|
||||
enum: [
|
||||
'NO_RULE_EXISTS', // No governance rule covered this case
|
||||
'RULE_TOO_NARROW', // Rule exists but didn't match this case
|
||||
'CLASSIFICATION_ERROR', // Activity classifier misclassified
|
||||
'ENFORCEMENT_GAP', // Rule exists but wasn't enforced
|
||||
'USER_OVERRIDE', // User bypassed framework (--no-verify)
|
||||
'UNKNOWN'
|
||||
],
|
||||
required: true,
|
||||
index: true,
|
||||
description: 'Why the framework missed this breach'
|
||||
},
|
||||
|
||||
missReasonDetails: {
|
||||
type: String,
|
||||
default: null,
|
||||
description: 'Additional context on why it was missed'
|
||||
},
|
||||
|
||||
// Link to original decision (if available)
|
||||
linkedAuditLog: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'AuditLog',
|
||||
default: null,
|
||||
description: 'Audit log entry where framework allowed this action'
|
||||
},
|
||||
|
||||
sessionId: {
|
||||
type: String,
|
||||
default: null,
|
||||
index: true,
|
||||
description: 'Session where the breach occurred (if known)'
|
||||
},
|
||||
|
||||
// Remediation
|
||||
remediation: {
|
||||
type: String,
|
||||
default: null,
|
||||
description: 'What was done to fix the breach'
|
||||
},
|
||||
|
||||
preventionAdded: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
description: 'Whether a new rule/fix was added to prevent recurrence'
|
||||
},
|
||||
|
||||
newRuleId: {
|
||||
type: String,
|
||||
default: null,
|
||||
description: 'ID of new governance rule added (e.g., inst_085)'
|
||||
},
|
||||
|
||||
// Reporting
|
||||
reportedBy: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
default: null,
|
||||
description: 'User who reported the missed breach'
|
||||
},
|
||||
|
||||
verifiedBy: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
default: null,
|
||||
description: 'Admin who verified this was truly a missed breach'
|
||||
},
|
||||
|
||||
status: {
|
||||
type: String,
|
||||
enum: ['REPORTED', 'VERIFIED', 'REMEDIATED', 'FALSE_POSITIVE'],
|
||||
default: 'REPORTED',
|
||||
index: true,
|
||||
description: 'Current status of the breach report'
|
||||
},
|
||||
|
||||
// Metadata
|
||||
environment: {
|
||||
type: String,
|
||||
enum: ['development', 'production', 'staging'],
|
||||
default: 'development',
|
||||
index: true
|
||||
},
|
||||
|
||||
tags: {
|
||||
type: [String],
|
||||
default: [],
|
||||
description: 'Tags for categorization and searching'
|
||||
}
|
||||
|
||||
}, {
|
||||
timestamps: true,
|
||||
collection: 'missedBreaches'
|
||||
});
|
||||
|
||||
// Indexes for common queries
|
||||
missedBreachSchema.index({ incidentDate: -1 });
|
||||
missedBreachSchema.index({ severity: 1, incidentDate: -1 });
|
||||
missedBreachSchema.index({ missReason: 1, incidentDate: -1 });
|
||||
missedBreachSchema.index({ status: 1, incidentDate: -1 });
|
||||
missedBreachSchema.index({ environment: 1, incidentDate: -1 });
|
||||
|
||||
// Virtual for time to detection
|
||||
missedBreachSchema.virtual('timeToDetection').get(function() {
|
||||
if (!this.discoveredDate || !this.incidentDate) return null;
|
||||
return Math.floor((this.discoveredDate - this.incidentDate) / (1000 * 60 * 60 * 24)); // Days
|
||||
});
|
||||
|
||||
// Static methods
|
||||
|
||||
/**
|
||||
* Get missed breach statistics
|
||||
*/
|
||||
missedBreachSchema.statics.getStatistics = async function(startDate, endDate) {
|
||||
const matchStage = {};
|
||||
|
||||
if (startDate && endDate) {
|
||||
matchStage.incidentDate = { $gte: startDate, $lte: endDate };
|
||||
}
|
||||
|
||||
const stats = await this.aggregate([
|
||||
{ $match: matchStage },
|
||||
{
|
||||
$group: {
|
||||
_id: null,
|
||||
totalMissed: { $sum: 1 },
|
||||
bySeverity: {
|
||||
$push: '$severity'
|
||||
},
|
||||
byReason: {
|
||||
$push: '$missReason'
|
||||
},
|
||||
totalActualCost: {
|
||||
$sum: { $ifNull: ['$actualCost', 0] }
|
||||
},
|
||||
totalEstimatedCost: {
|
||||
$sum: { $ifNull: ['$estimatedCost', 0] }
|
||||
},
|
||||
avgTimeToDetection: {
|
||||
$avg: {
|
||||
$divide: [
|
||||
{ $subtract: ['$discoveredDate', '$incidentDate'] },
|
||||
1000 * 60 * 60 * 24 // Convert to days
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
return stats[0] || null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculate framework effectiveness rate
|
||||
* Requires audit log stats for comparison
|
||||
*/
|
||||
missedBreachSchema.statics.calculateEffectivenessRate = async function(startDate, endDate, auditStats) {
|
||||
const missedStats = await this.getStatistics(startDate, endDate);
|
||||
|
||||
if (!missedStats || !auditStats) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const detected = auditStats.blocked || 0;
|
||||
const missed = missedStats.totalMissed || 0;
|
||||
const total = detected + missed;
|
||||
|
||||
if (total === 0) return null;
|
||||
|
||||
return {
|
||||
detected,
|
||||
missed,
|
||||
total,
|
||||
detectionRate: (detected / total) * 100,
|
||||
missRate: (missed / total) * 100,
|
||||
effectiveness: detected / total
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Find breaches by reason
|
||||
*/
|
||||
missedBreachSchema.statics.findByReason = function(reason, options = {}) {
|
||||
const query = { missReason: reason };
|
||||
|
||||
if (options.status) {
|
||||
query.status = options.status;
|
||||
}
|
||||
|
||||
return this.find(query)
|
||||
.sort({ incidentDate: -1 })
|
||||
.limit(options.limit || 0);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get breach breakdown by reason
|
||||
*/
|
||||
missedBreachSchema.statics.getBreakdownByReason = async function(startDate, endDate) {
|
||||
const matchStage = {};
|
||||
|
||||
if (startDate && endDate) {
|
||||
matchStage.incidentDate = { $gte: startDate, $lte: endDate };
|
||||
}
|
||||
|
||||
const breakdown = await this.aggregate([
|
||||
{ $match: matchStage },
|
||||
{
|
||||
$group: {
|
||||
_id: '$missReason',
|
||||
count: { $sum: 1 },
|
||||
totalCost: {
|
||||
$sum: {
|
||||
$ifNull: [
|
||||
'$actualCost',
|
||||
{ $ifNull: ['$estimatedCost', 0] }
|
||||
]
|
||||
}
|
||||
},
|
||||
examples: {
|
||||
$push: {
|
||||
title: '$title',
|
||||
severity: '$severity',
|
||||
incidentDate: '$incidentDate'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
_id: 0,
|
||||
reason: '$_id',
|
||||
count: 1,
|
||||
totalCost: 1,
|
||||
recentExamples: { $slice: ['$examples', 3] }
|
||||
}
|
||||
},
|
||||
{ $sort: { count: -1 } }
|
||||
]);
|
||||
|
||||
return breakdown;
|
||||
};
|
||||
|
||||
const MissedBreach = mongoose.model('MissedBreach', missedBreachSchema);
|
||||
|
||||
module.exports = MissedBreach;
|
||||
|
|
@ -30,6 +30,7 @@ const relationshipsRoutes = require('./relationships.routes');
|
|||
const contactRoutes = require('./contact.routes');
|
||||
const inboxRoutes = require('./inbox.routes');
|
||||
const crmRoutes = require('./crm.routes');
|
||||
const missedBreachRoutes = require('./missedBreach.routes');
|
||||
|
||||
// Development/test routes (only in development)
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
|
|
@ -61,6 +62,7 @@ router.use('/relationships', relationshipsRoutes);
|
|||
router.use('/contact', contactRoutes);
|
||||
router.use('/inbox', inboxRoutes);
|
||||
router.use('/crm', crmRoutes);
|
||||
router.use('/admin/missed-breaches', missedBreachRoutes);
|
||||
|
||||
// API root endpoint - redirect browsers to documentation
|
||||
router.get('/', (req, res) => {
|
||||
|
|
|
|||
31
src/routes/missedBreach.routes.js
Normal file
31
src/routes/missedBreach.routes.js
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
/**
|
||||
* Missed Breach Routes
|
||||
*
|
||||
* API endpoints for tracking governance framework false negatives
|
||||
*/
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const missedBreachController = require('../controllers/missedBreach.controller');
|
||||
const { authenticateToken, requireRole } = require('../middleware/auth.middleware');
|
||||
|
||||
// All routes require authentication and admin role
|
||||
router.use(authenticateToken);
|
||||
router.use(requireRole('admin'));
|
||||
|
||||
// POST /api/admin/missed-breaches - Report new missed breach
|
||||
router.post('/', missedBreachController.reportMissedBreach);
|
||||
|
||||
// GET /api/admin/missed-breaches - Get all missed breaches
|
||||
router.get('/', missedBreachController.getMissedBreaches);
|
||||
|
||||
// GET /api/admin/missed-breaches/statistics - Get statistics
|
||||
router.get('/statistics', missedBreachController.getMissedBreachStatistics);
|
||||
|
||||
// PATCH /api/admin/missed-breaches/:id - Update missed breach
|
||||
router.patch('/:id', missedBreachController.updateMissedBreach);
|
||||
|
||||
// DELETE /api/admin/missed-breaches/:id - Delete missed breach
|
||||
router.delete('/:id', missedBreachController.deleteMissedBreach);
|
||||
|
||||
module.exports = router;
|
||||
Loading…
Add table
Reference in a new issue