@ansvar/us-regulations-mcp 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +190 -0
- package/README.md +275 -0
- package/data/.gitkeep +0 -0
- package/data/regulations.db +0 -0
- package/data/seed/applicability/rules.json +74 -0
- package/data/seed/mappings/ccpa-nist-csf.json +144 -0
- package/data/seed/mappings/hipaa-nist-800-53.json +377 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +41 -0
- package/dist/index.js.map +1 -0
- package/dist/ingest/adapters/california-leginfo.d.ts +72 -0
- package/dist/ingest/adapters/california-leginfo.d.ts.map +1 -0
- package/dist/ingest/adapters/california-leginfo.js +270 -0
- package/dist/ingest/adapters/california-leginfo.js.map +1 -0
- package/dist/ingest/adapters/ecfr.d.ts +76 -0
- package/dist/ingest/adapters/ecfr.d.ts.map +1 -0
- package/dist/ingest/adapters/ecfr.js +355 -0
- package/dist/ingest/adapters/ecfr.js.map +1 -0
- package/dist/ingest/adapters/regulations-gov.d.ts +47 -0
- package/dist/ingest/adapters/regulations-gov.d.ts.map +1 -0
- package/dist/ingest/adapters/regulations-gov.js +91 -0
- package/dist/ingest/adapters/regulations-gov.js.map +1 -0
- package/dist/ingest/framework.d.ts +84 -0
- package/dist/ingest/framework.d.ts.map +1 -0
- package/dist/ingest/framework.js +8 -0
- package/dist/ingest/framework.js.map +1 -0
- package/dist/tools/action-items.d.ts +23 -0
- package/dist/tools/action-items.d.ts.map +1 -0
- package/dist/tools/action-items.js +118 -0
- package/dist/tools/action-items.js.map +1 -0
- package/dist/tools/applicability.d.ts +26 -0
- package/dist/tools/applicability.d.ts.map +1 -0
- package/dist/tools/applicability.js +49 -0
- package/dist/tools/applicability.js.map +1 -0
- package/dist/tools/compare.d.ts +20 -0
- package/dist/tools/compare.d.ts.map +1 -0
- package/dist/tools/compare.js +35 -0
- package/dist/tools/compare.js.map +1 -0
- package/dist/tools/definitions.d.ts +22 -0
- package/dist/tools/definitions.d.ts.map +1 -0
- package/dist/tools/definitions.js +43 -0
- package/dist/tools/definitions.js.map +1 -0
- package/dist/tools/evidence.d.ts +23 -0
- package/dist/tools/evidence.d.ts.map +1 -0
- package/dist/tools/evidence.js +27 -0
- package/dist/tools/evidence.js.map +1 -0
- package/dist/tools/list.d.ts +25 -0
- package/dist/tools/list.d.ts.map +1 -0
- package/dist/tools/list.js +66 -0
- package/dist/tools/list.js.map +1 -0
- package/dist/tools/map.d.ts +26 -0
- package/dist/tools/map.d.ts.map +1 -0
- package/dist/tools/map.js +58 -0
- package/dist/tools/map.js.map +1 -0
- package/dist/tools/registry.d.ts +19 -0
- package/dist/tools/registry.d.ts.map +1 -0
- package/dist/tools/registry.js +260 -0
- package/dist/tools/registry.js.map +1 -0
- package/dist/tools/search.d.ts +15 -0
- package/dist/tools/search.d.ts.map +1 -0
- package/dist/tools/search.js +94 -0
- package/dist/tools/search.js.map +1 -0
- package/dist/tools/section.d.ts +19 -0
- package/dist/tools/section.d.ts.map +1 -0
- package/dist/tools/section.js +50 -0
- package/dist/tools/section.js.map +1 -0
- package/package.json +76 -0
- package/scripts/build-db.ts +268 -0
- package/scripts/ingest.ts +214 -0
- package/scripts/load-seed-data.ts +133 -0
- package/scripts/quality-test.ts +346 -0
- package/scripts/test-mcp-tools.ts +187 -0
- package/scripts/test-remaining-tools.ts +107 -0
- package/src/index.ts +55 -0
- package/src/ingest/adapters/california-leginfo.ts +322 -0
- package/src/ingest/adapters/ecfr.ts +403 -0
- package/src/ingest/adapters/regulations-gov.ts +112 -0
- package/src/ingest/framework.ts +92 -0
- package/src/tools/action-items.ts +164 -0
- package/src/tools/applicability.ts +91 -0
- package/src/tools/compare.ts +61 -0
- package/src/tools/definitions.ts +79 -0
- package/src/tools/evidence.ts +53 -0
- package/src/tools/list.ts +120 -0
- package/src/tools/map.ts +100 -0
- package/src/tools/registry.ts +275 -0
- package/src/tools/search.ts +132 -0
- package/src/tools/section.ts +85 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import type { Database } from 'better-sqlite3';
|
|
2
|
+
import { getSection, SectionData } from './section.js';
|
|
3
|
+
|
|
4
|
+
export interface ActionItemsInput {
|
|
5
|
+
regulation: string;
|
|
6
|
+
sections: string[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface ActionItem {
|
|
10
|
+
section: string;
|
|
11
|
+
title: string;
|
|
12
|
+
required_state: string;
|
|
13
|
+
priority: 'high' | 'medium' | 'low';
|
|
14
|
+
evidence_needed: string[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ActionItemsResult {
|
|
18
|
+
regulation: string;
|
|
19
|
+
action_items: ActionItem[];
|
|
20
|
+
total_items: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Extract action items priority from section text.
|
|
25
|
+
* - High: Contains "shall", "must", "required"
|
|
26
|
+
* - Medium: Contains "should", "recommended"
|
|
27
|
+
* - Low: Everything else
|
|
28
|
+
*/
|
|
29
|
+
function extractPriority(text: string): 'high' | 'medium' | 'low' {
|
|
30
|
+
const lowerText = text.toLowerCase();
|
|
31
|
+
|
|
32
|
+
if (
|
|
33
|
+
lowerText.includes('shall') ||
|
|
34
|
+
lowerText.includes('must') ||
|
|
35
|
+
lowerText.includes('required')
|
|
36
|
+
) {
|
|
37
|
+
return 'high';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (lowerText.includes('should') || lowerText.includes('recommended')) {
|
|
41
|
+
return 'medium';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return 'low';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Extract evidence keywords from section text.
|
|
49
|
+
* Looks for common audit artifacts and documentation types.
|
|
50
|
+
*/
|
|
51
|
+
function extractEvidenceNeeded(text: string): string[] {
|
|
52
|
+
const lowerText = text.toLowerCase();
|
|
53
|
+
const evidence: Set<string> = new Set();
|
|
54
|
+
|
|
55
|
+
// Common evidence types
|
|
56
|
+
const evidenceKeywords: Record<string, string> = {
|
|
57
|
+
'risk assessment': 'risk assessment report',
|
|
58
|
+
'risk analysis': 'risk assessment report',
|
|
59
|
+
'audit log': 'audit logs',
|
|
60
|
+
'access log': 'access logs',
|
|
61
|
+
'security incident': 'incident response documentation',
|
|
62
|
+
'breach': 'breach notification records',
|
|
63
|
+
'policy': 'written policies',
|
|
64
|
+
'procedure': 'written procedures',
|
|
65
|
+
'training': 'training records',
|
|
66
|
+
'documentation': 'supporting documentation',
|
|
67
|
+
'encryption': 'encryption documentation',
|
|
68
|
+
'access control': 'access control matrix',
|
|
69
|
+
'authentication': 'authentication logs',
|
|
70
|
+
'authorization': 'authorization records',
|
|
71
|
+
'backup': 'backup records',
|
|
72
|
+
'disaster recovery': 'disaster recovery plan',
|
|
73
|
+
'business continuity': 'business continuity plan',
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
for (const [keyword, evidenceType] of Object.entries(evidenceKeywords)) {
|
|
77
|
+
if (lowerText.includes(keyword)) {
|
|
78
|
+
evidence.add(evidenceType);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return Array.from(evidence);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Generate compliance action items from regulation sections.
|
|
87
|
+
* Extracts priority and evidence requirements from section text.
|
|
88
|
+
*/
|
|
89
|
+
export async function getComplianceActionItems(
|
|
90
|
+
db: Database,
|
|
91
|
+
input: ActionItemsInput
|
|
92
|
+
): Promise<ActionItemsResult> {
|
|
93
|
+
const { regulation, sections } = input;
|
|
94
|
+
|
|
95
|
+
if (!regulation) {
|
|
96
|
+
throw new Error('Regulation is required');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!sections || sections.length === 0) {
|
|
100
|
+
throw new Error('At least one section must be specified');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (sections.length > 20) {
|
|
104
|
+
throw new Error('Cannot process more than 20 sections at once');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Fetch section data
|
|
108
|
+
const actionItems: ActionItem[] = [];
|
|
109
|
+
|
|
110
|
+
for (const sectionNumber of sections) {
|
|
111
|
+
const sectionData = await getSection(db, {
|
|
112
|
+
regulation,
|
|
113
|
+
section: sectionNumber,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
if (!sectionData) {
|
|
117
|
+
console.warn(`Section ${sectionNumber} not found in ${regulation}, skipping`);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Extract action item components
|
|
122
|
+
const priority = extractPriority(sectionData.text);
|
|
123
|
+
const evidenceNeeded = extractEvidenceNeeded(sectionData.text);
|
|
124
|
+
|
|
125
|
+
// Generate required state description
|
|
126
|
+
const requiredState = generateRequiredState(sectionData);
|
|
127
|
+
|
|
128
|
+
actionItems.push({
|
|
129
|
+
section: `§${sectionData.section_number}`,
|
|
130
|
+
title: sectionData.title || `Section ${sectionData.section_number}`,
|
|
131
|
+
required_state: requiredState,
|
|
132
|
+
priority,
|
|
133
|
+
evidence_needed: evidenceNeeded.length > 0 ? evidenceNeeded : ['supporting documentation'],
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
regulation,
|
|
139
|
+
action_items: actionItems,
|
|
140
|
+
total_items: actionItems.length,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Generate a concise required state description from section data.
|
|
146
|
+
* Extracts first sentence or up to 200 characters from section text.
|
|
147
|
+
*/
|
|
148
|
+
function generateRequiredState(section: SectionData): string {
|
|
149
|
+
const text = section.text.trim();
|
|
150
|
+
|
|
151
|
+
// Try to extract first sentence
|
|
152
|
+
const firstSentence = text.match(/^[^.!?]+[.!?]/);
|
|
153
|
+
if (firstSentence) {
|
|
154
|
+
let sentence = firstSentence[0].trim();
|
|
155
|
+
if (sentence.length > 200) {
|
|
156
|
+
sentence = sentence.substring(0, 197) + '...';
|
|
157
|
+
}
|
|
158
|
+
return `Comply with requirements of §${section.section_number}: ${sentence}`;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Fallback: first 200 characters
|
|
162
|
+
const truncated = text.length > 200 ? text.substring(0, 197) + '...' : text;
|
|
163
|
+
return `Comply with requirements of §${section.section_number}: ${truncated}`;
|
|
164
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { Database } from 'better-sqlite3';
|
|
2
|
+
|
|
3
|
+
export interface ApplicabilityInput {
|
|
4
|
+
sector: string;
|
|
5
|
+
subsector?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface ApplicabilityRule {
|
|
9
|
+
regulation: string;
|
|
10
|
+
sector: string;
|
|
11
|
+
subsector: string | null;
|
|
12
|
+
applies: boolean;
|
|
13
|
+
confidence: 'definite' | 'likely' | 'possible';
|
|
14
|
+
basis_section: string | null;
|
|
15
|
+
notes: string | null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ApplicabilityResult {
|
|
19
|
+
sector: string;
|
|
20
|
+
subsector: string | null;
|
|
21
|
+
applicable_regulations: ApplicabilityRule[];
|
|
22
|
+
total_applicable: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Check which regulations apply to a specific sector/subsector.
|
|
27
|
+
* Returns regulations that apply with confidence levels.
|
|
28
|
+
*/
|
|
29
|
+
export async function checkApplicability(
|
|
30
|
+
db: Database,
|
|
31
|
+
input: ApplicabilityInput
|
|
32
|
+
): Promise<ApplicabilityResult> {
|
|
33
|
+
const { sector, subsector } = input;
|
|
34
|
+
|
|
35
|
+
if (!sector || sector.trim().length === 0) {
|
|
36
|
+
throw new Error('Sector is required (e.g., "healthcare", "financial", "retail")');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Query applicability rules
|
|
40
|
+
let sql = `
|
|
41
|
+
SELECT
|
|
42
|
+
regulation,
|
|
43
|
+
sector,
|
|
44
|
+
subsector,
|
|
45
|
+
applies,
|
|
46
|
+
confidence,
|
|
47
|
+
basis_section,
|
|
48
|
+
notes
|
|
49
|
+
FROM applicability_rules
|
|
50
|
+
WHERE sector = ?
|
|
51
|
+
`;
|
|
52
|
+
|
|
53
|
+
const params: string[] = [sector];
|
|
54
|
+
|
|
55
|
+
if (subsector) {
|
|
56
|
+
sql += ' AND (subsector = ? OR subsector IS NULL)';
|
|
57
|
+
params.push(subsector);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
sql += ' ORDER BY applies DESC, confidence ASC';
|
|
61
|
+
|
|
62
|
+
const rows = db.prepare(sql).all(...params) as Array<{
|
|
63
|
+
regulation: string;
|
|
64
|
+
sector: string;
|
|
65
|
+
subsector: string | null;
|
|
66
|
+
applies: number; // Database stores as INTEGER (0 or 1)
|
|
67
|
+
confidence: 'definite' | 'likely' | 'possible';
|
|
68
|
+
basis_section: string | null;
|
|
69
|
+
notes: string | null;
|
|
70
|
+
}>;
|
|
71
|
+
|
|
72
|
+
// Filter to only regulations that apply
|
|
73
|
+
const applicableRules = rows
|
|
74
|
+
.filter((row) => row.applies === 1)
|
|
75
|
+
.map((row) => ({
|
|
76
|
+
regulation: row.regulation,
|
|
77
|
+
sector: row.sector,
|
|
78
|
+
subsector: row.subsector,
|
|
79
|
+
applies: row.applies === 1,
|
|
80
|
+
confidence: row.confidence,
|
|
81
|
+
basis_section: row.basis_section,
|
|
82
|
+
notes: row.notes,
|
|
83
|
+
}));
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
sector,
|
|
87
|
+
subsector: subsector || null,
|
|
88
|
+
applicable_regulations: applicableRules,
|
|
89
|
+
total_applicable: applicableRules.length,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { Database } from 'better-sqlite3';
|
|
2
|
+
import { searchRegulations, SearchResult } from './search.js';
|
|
3
|
+
|
|
4
|
+
export interface CompareInput {
|
|
5
|
+
topic: string;
|
|
6
|
+
regulations: string[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface CompareResult {
|
|
10
|
+
topic: string;
|
|
11
|
+
comparisons: Array<{
|
|
12
|
+
regulation: string;
|
|
13
|
+
matches: SearchResult[];
|
|
14
|
+
total_matches: number;
|
|
15
|
+
}>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Compare requirements across multiple regulations for a specific topic.
|
|
20
|
+
* Uses search_regulations internally to find relevant sections in each regulation.
|
|
21
|
+
*/
|
|
22
|
+
export async function compareRequirements(
|
|
23
|
+
db: Database,
|
|
24
|
+
input: CompareInput
|
|
25
|
+
): Promise<CompareResult> {
|
|
26
|
+
const { topic, regulations } = input;
|
|
27
|
+
|
|
28
|
+
if (!topic || topic.trim().length === 0) {
|
|
29
|
+
throw new Error('Topic is required');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!regulations || regulations.length === 0) {
|
|
33
|
+
throw new Error('At least one regulation must be specified');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (regulations.length > 10) {
|
|
37
|
+
throw new Error('Cannot compare more than 10 regulations at once');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Search each regulation independently
|
|
41
|
+
const comparisons = await Promise.all(
|
|
42
|
+
regulations.map(async (regulation) => {
|
|
43
|
+
const results = await searchRegulations(db, {
|
|
44
|
+
query: topic,
|
|
45
|
+
regulations: [regulation],
|
|
46
|
+
limit: 5, // Top 5 matches per regulation
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
regulation,
|
|
51
|
+
matches: results,
|
|
52
|
+
total_matches: results.length,
|
|
53
|
+
};
|
|
54
|
+
})
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
topic,
|
|
59
|
+
comparisons,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { Database } from 'better-sqlite3';
|
|
2
|
+
|
|
3
|
+
export interface DefinitionsInput {
|
|
4
|
+
term: string;
|
|
5
|
+
regulation?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface Definition {
|
|
9
|
+
regulation: string;
|
|
10
|
+
term: string;
|
|
11
|
+
definition: string;
|
|
12
|
+
section: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface DefinitionsResult {
|
|
16
|
+
term: string;
|
|
17
|
+
definitions: Definition[];
|
|
18
|
+
total_definitions: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function escapeSqlLike(str: string): string {
|
|
22
|
+
return str.replace(/[%_]/g, '\\$&');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Look up official term definitions across regulations.
|
|
27
|
+
* Uses LIKE search to match partial terms (e.g., "health" matches "protected health information").
|
|
28
|
+
*/
|
|
29
|
+
export async function getDefinitions(
|
|
30
|
+
db: Database,
|
|
31
|
+
input: DefinitionsInput
|
|
32
|
+
): Promise<DefinitionsResult> {
|
|
33
|
+
const { term, regulation } = input;
|
|
34
|
+
|
|
35
|
+
if (!term || term.trim().length === 0) {
|
|
36
|
+
throw new Error('Term is required');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Build query with optional regulation filter
|
|
40
|
+
let sql = `
|
|
41
|
+
SELECT
|
|
42
|
+
regulation,
|
|
43
|
+
term,
|
|
44
|
+
definition,
|
|
45
|
+
section
|
|
46
|
+
FROM definitions
|
|
47
|
+
WHERE term LIKE ?
|
|
48
|
+
`;
|
|
49
|
+
|
|
50
|
+
// Use LIKE for partial matching (case-insensitive)
|
|
51
|
+
const params: string[] = [`%${escapeSqlLike(term)}%`];
|
|
52
|
+
|
|
53
|
+
if (regulation) {
|
|
54
|
+
sql += ' AND regulation = ?';
|
|
55
|
+
params.push(regulation);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
sql += ' ORDER BY regulation, term';
|
|
59
|
+
|
|
60
|
+
const rows = db.prepare(sql).all(...params) as Array<{
|
|
61
|
+
regulation: string;
|
|
62
|
+
term: string;
|
|
63
|
+
definition: string;
|
|
64
|
+
section: string;
|
|
65
|
+
}>;
|
|
66
|
+
|
|
67
|
+
const definitions = rows.map((row) => ({
|
|
68
|
+
regulation: row.regulation,
|
|
69
|
+
term: row.term,
|
|
70
|
+
definition: row.definition,
|
|
71
|
+
section: row.section,
|
|
72
|
+
}));
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
term,
|
|
76
|
+
definitions,
|
|
77
|
+
total_definitions: definitions.length,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { Database } from 'better-sqlite3';
|
|
2
|
+
|
|
3
|
+
export interface EvidenceInput {
|
|
4
|
+
regulation: string;
|
|
5
|
+
section: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface EvidenceRequirement {
|
|
9
|
+
evidence_type: string;
|
|
10
|
+
description: string;
|
|
11
|
+
required: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface EvidenceResult {
|
|
15
|
+
regulation: string;
|
|
16
|
+
section: string;
|
|
17
|
+
evidence_requirements: EvidenceRequirement[];
|
|
18
|
+
notes: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Get evidence requirements for compliance with a specific section.
|
|
23
|
+
* MVP: Returns placeholder until evidence data is seeded.
|
|
24
|
+
* Future: Will query evidence_requirements table.
|
|
25
|
+
*/
|
|
26
|
+
export async function getEvidenceRequirements(
|
|
27
|
+
db: Database,
|
|
28
|
+
input: EvidenceInput
|
|
29
|
+
): Promise<EvidenceResult> {
|
|
30
|
+
const { regulation, section } = input;
|
|
31
|
+
|
|
32
|
+
if (!regulation || !section) {
|
|
33
|
+
throw new Error('Both regulation and section are required');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Verify section exists
|
|
37
|
+
const sectionExists = db
|
|
38
|
+
.prepare('SELECT 1 FROM sections WHERE regulation = ? AND section_number = ?')
|
|
39
|
+
.get(regulation, section);
|
|
40
|
+
|
|
41
|
+
if (!sectionExists) {
|
|
42
|
+
throw new Error(`Section ${section} not found in regulation ${regulation}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// MVP: Return empty array with note about future implementation
|
|
46
|
+
// In production, this would query an evidence_requirements table
|
|
47
|
+
return {
|
|
48
|
+
regulation,
|
|
49
|
+
section,
|
|
50
|
+
evidence_requirements: [],
|
|
51
|
+
notes: 'Evidence requirements data not yet available. This feature will be implemented in a future release with specific audit artifacts, policies, and procedures required for compliance.',
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import type { Database } from 'better-sqlite3';
|
|
2
|
+
|
|
3
|
+
export interface ListInput {
|
|
4
|
+
regulation?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface RegulationInfo {
|
|
8
|
+
id: string;
|
|
9
|
+
full_name: string;
|
|
10
|
+
citation: string;
|
|
11
|
+
effective_date: string | null;
|
|
12
|
+
jurisdiction: string | null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface Chapter {
|
|
16
|
+
name: string;
|
|
17
|
+
sections: string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface RegulationStructure {
|
|
21
|
+
regulation: RegulationInfo;
|
|
22
|
+
chapters: Chapter[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface ListResult {
|
|
26
|
+
regulations?: RegulationInfo[];
|
|
27
|
+
structure?: RegulationStructure;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function listRegulations(
|
|
31
|
+
db: Database,
|
|
32
|
+
input: ListInput
|
|
33
|
+
): Promise<ListResult> {
|
|
34
|
+
const { regulation } = input;
|
|
35
|
+
|
|
36
|
+
if (regulation) {
|
|
37
|
+
// Get specific regulation with chapters
|
|
38
|
+
const regRow = db.prepare(`
|
|
39
|
+
SELECT id, full_name, citation, effective_date, jurisdiction
|
|
40
|
+
FROM regulations
|
|
41
|
+
WHERE id = ?
|
|
42
|
+
`).get(regulation) as {
|
|
43
|
+
id: string;
|
|
44
|
+
full_name: string;
|
|
45
|
+
citation: string;
|
|
46
|
+
effective_date: string | null;
|
|
47
|
+
jurisdiction: string | null;
|
|
48
|
+
} | undefined;
|
|
49
|
+
|
|
50
|
+
if (!regRow) {
|
|
51
|
+
return { regulations: [] };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Get sections grouped by chapter
|
|
55
|
+
const sections = db.prepare(`
|
|
56
|
+
SELECT section_number, title, chapter
|
|
57
|
+
FROM sections
|
|
58
|
+
WHERE regulation = ?
|
|
59
|
+
ORDER BY section_number
|
|
60
|
+
`).all(regulation) as Array<{
|
|
61
|
+
section_number: string;
|
|
62
|
+
title: string | null;
|
|
63
|
+
chapter: string | null;
|
|
64
|
+
}>;
|
|
65
|
+
|
|
66
|
+
// Group by chapter
|
|
67
|
+
const chapterMap = new Map<string, Chapter>();
|
|
68
|
+
for (const section of sections) {
|
|
69
|
+
const chapterKey = section.chapter || 'General';
|
|
70
|
+
if (!chapterMap.has(chapterKey)) {
|
|
71
|
+
chapterMap.set(chapterKey, {
|
|
72
|
+
name: chapterKey,
|
|
73
|
+
sections: [],
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
chapterMap.get(chapterKey)!.sections.push(section.section_number);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
structure: {
|
|
81
|
+
regulation: {
|
|
82
|
+
id: regRow.id,
|
|
83
|
+
full_name: regRow.full_name,
|
|
84
|
+
citation: regRow.citation,
|
|
85
|
+
effective_date: regRow.effective_date,
|
|
86
|
+
jurisdiction: regRow.jurisdiction,
|
|
87
|
+
},
|
|
88
|
+
chapters: Array.from(chapterMap.values()),
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// List all regulations
|
|
94
|
+
const rows = db.prepare(`
|
|
95
|
+
SELECT
|
|
96
|
+
id,
|
|
97
|
+
full_name,
|
|
98
|
+
citation,
|
|
99
|
+
effective_date,
|
|
100
|
+
jurisdiction
|
|
101
|
+
FROM regulations
|
|
102
|
+
ORDER BY id
|
|
103
|
+
`).all() as Array<{
|
|
104
|
+
id: string;
|
|
105
|
+
full_name: string;
|
|
106
|
+
citation: string;
|
|
107
|
+
effective_date: string | null;
|
|
108
|
+
jurisdiction: string | null;
|
|
109
|
+
}>;
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
regulations: rows.map(row => ({
|
|
113
|
+
id: row.id,
|
|
114
|
+
full_name: row.full_name,
|
|
115
|
+
citation: row.citation,
|
|
116
|
+
effective_date: row.effective_date,
|
|
117
|
+
jurisdiction: row.jurisdiction,
|
|
118
|
+
})),
|
|
119
|
+
};
|
|
120
|
+
}
|
package/src/tools/map.ts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { Database } from 'better-sqlite3';
|
|
2
|
+
|
|
3
|
+
export interface MapControlsInput {
|
|
4
|
+
framework: string;
|
|
5
|
+
control?: string;
|
|
6
|
+
regulation?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface ControlMapping {
|
|
10
|
+
framework: string;
|
|
11
|
+
control_id: string;
|
|
12
|
+
control_name: string;
|
|
13
|
+
regulation: string;
|
|
14
|
+
sections: string[];
|
|
15
|
+
coverage: 'full' | 'partial' | 'related';
|
|
16
|
+
notes: string | null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface MapControlsResult {
|
|
20
|
+
framework: string;
|
|
21
|
+
mappings: ControlMapping[];
|
|
22
|
+
total_mappings: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Map NIST controls (800-53, CSF) to regulation sections.
|
|
27
|
+
* Can filter by specific control ID or regulation.
|
|
28
|
+
*/
|
|
29
|
+
export async function mapControls(
|
|
30
|
+
db: Database,
|
|
31
|
+
input: MapControlsInput
|
|
32
|
+
): Promise<MapControlsResult> {
|
|
33
|
+
const { framework, control, regulation } = input;
|
|
34
|
+
|
|
35
|
+
if (!framework) {
|
|
36
|
+
throw new Error('Framework is required (e.g., "NIST_CSF", "NIST_800_53")');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Build query with optional filters
|
|
40
|
+
let sql = `
|
|
41
|
+
SELECT
|
|
42
|
+
framework,
|
|
43
|
+
control_id,
|
|
44
|
+
control_name,
|
|
45
|
+
regulation,
|
|
46
|
+
sections,
|
|
47
|
+
coverage,
|
|
48
|
+
notes
|
|
49
|
+
FROM control_mappings
|
|
50
|
+
WHERE framework = ?
|
|
51
|
+
`;
|
|
52
|
+
|
|
53
|
+
const params: string[] = [framework];
|
|
54
|
+
|
|
55
|
+
if (control) {
|
|
56
|
+
sql += ' AND control_id = ?';
|
|
57
|
+
params.push(control);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (regulation) {
|
|
61
|
+
sql += ' AND regulation = ?';
|
|
62
|
+
params.push(regulation);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
sql += ' ORDER BY control_id, regulation';
|
|
66
|
+
|
|
67
|
+
const rows = db.prepare(sql).all(...params) as Array<{
|
|
68
|
+
framework: string;
|
|
69
|
+
control_id: string;
|
|
70
|
+
control_name: string;
|
|
71
|
+
regulation: string;
|
|
72
|
+
sections: string;
|
|
73
|
+
coverage: 'full' | 'partial' | 'related';
|
|
74
|
+
notes: string | null;
|
|
75
|
+
}>;
|
|
76
|
+
|
|
77
|
+
// Parse sections JSON
|
|
78
|
+
const mappings = rows.map((row) => ({
|
|
79
|
+
framework: row.framework,
|
|
80
|
+
control_id: row.control_id,
|
|
81
|
+
control_name: row.control_name,
|
|
82
|
+
regulation: row.regulation,
|
|
83
|
+
sections: (() => {
|
|
84
|
+
try {
|
|
85
|
+
return JSON.parse(row.sections);
|
|
86
|
+
} catch {
|
|
87
|
+
console.warn(`Invalid sections JSON for ${row.control_id}`);
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
})(),
|
|
91
|
+
coverage: row.coverage,
|
|
92
|
+
notes: row.notes,
|
|
93
|
+
}));
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
framework,
|
|
97
|
+
mappings,
|
|
98
|
+
total_mappings: mappings.length,
|
|
99
|
+
};
|
|
100
|
+
}
|