@activemind/scd 1.4.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.
Files changed (79) hide show
  1. package/LICENSE.md +35 -0
  2. package/README.md +417 -0
  3. package/bin/scd.js +140 -0
  4. package/lib/audit-report.js +93 -0
  5. package/lib/audit-sync.js +172 -0
  6. package/lib/audit.js +356 -0
  7. package/lib/cli-helpers.js +108 -0
  8. package/lib/commands/accept.js +28 -0
  9. package/lib/commands/audit.js +17 -0
  10. package/lib/commands/configure.js +200 -0
  11. package/lib/commands/doctor.js +14 -0
  12. package/lib/commands/exceptions.js +19 -0
  13. package/lib/commands/export-findings.js +46 -0
  14. package/lib/commands/findings.js +306 -0
  15. package/lib/commands/ignore.js +28 -0
  16. package/lib/commands/init.js +16 -0
  17. package/lib/commands/insights.js +24 -0
  18. package/lib/commands/install.js +15 -0
  19. package/lib/commands/list.js +109 -0
  20. package/lib/commands/remove.js +16 -0
  21. package/lib/commands/repo.js +862 -0
  22. package/lib/commands/report.js +234 -0
  23. package/lib/commands/resolve.js +25 -0
  24. package/lib/commands/rules.js +185 -0
  25. package/lib/commands/scan.js +519 -0
  26. package/lib/commands/scope.js +341 -0
  27. package/lib/commands/sync.js +40 -0
  28. package/lib/commands/uninstall.js +15 -0
  29. package/lib/commands/version.js +33 -0
  30. package/lib/comment-map.js +388 -0
  31. package/lib/config.js +325 -0
  32. package/lib/context-modifiers.js +211 -0
  33. package/lib/deep-analyzer.js +225 -0
  34. package/lib/doctor.js +236 -0
  35. package/lib/exception-manager.js +675 -0
  36. package/lib/export-findings.js +376 -0
  37. package/lib/file-context.js +380 -0
  38. package/lib/file-filter.js +204 -0
  39. package/lib/file-manifest.js +145 -0
  40. package/lib/git-utils.js +102 -0
  41. package/lib/global-config.js +239 -0
  42. package/lib/hooks-manager.js +130 -0
  43. package/lib/init-repo.js +147 -0
  44. package/lib/insights-analyzer.js +416 -0
  45. package/lib/insights-output.js +160 -0
  46. package/lib/installer.js +128 -0
  47. package/lib/output-constants.js +32 -0
  48. package/lib/output-terminal.js +407 -0
  49. package/lib/push-queue.js +322 -0
  50. package/lib/remove-repo.js +108 -0
  51. package/lib/repo-context.js +187 -0
  52. package/lib/report-html.js +1154 -0
  53. package/lib/report-index.js +157 -0
  54. package/lib/report-json.js +136 -0
  55. package/lib/report-markdown.js +250 -0
  56. package/lib/resolve-manager.js +148 -0
  57. package/lib/rule-registry.js +205 -0
  58. package/lib/scan-cache.js +171 -0
  59. package/lib/scan-context.js +312 -0
  60. package/lib/scan-schema.js +67 -0
  61. package/lib/scanner-full.js +681 -0
  62. package/lib/scanner-manual.js +348 -0
  63. package/lib/scanner-secrets.js +83 -0
  64. package/lib/scope.js +331 -0
  65. package/lib/store-verify.js +395 -0
  66. package/lib/store.js +310 -0
  67. package/lib/taint-register.js +196 -0
  68. package/lib/version-check.js +46 -0
  69. package/package.json +37 -0
  70. package/rules/rule-loader.js +324 -0
  71. package/rules/rules-aspx-cs.json +399 -0
  72. package/rules/rules-aspx.json +222 -0
  73. package/rules/rules-infra-leakage.json +434 -0
  74. package/rules/rules-js.json +664 -0
  75. package/rules/rules-php.json +521 -0
  76. package/rules/rules-python.json +466 -0
  77. package/rules/rules-secrets.json +99 -0
  78. package/rules/rules-sensitive-files.json +475 -0
  79. package/rules/rules-ts.json +76 -0
@@ -0,0 +1,225 @@
1
+ /**
2
+ * deep-analyzer.js
3
+ * Routes --deep analysis to scd-server. All AI logic lives server-side.
4
+ * Uses http.request instead of fetch to avoid Node.js fetch socket timeout issues.
5
+ */
6
+
7
+ 'use strict';
8
+ const { RESET, BOLD, DIM, RED, GREEN, YELLOW, CYAN } = require('./output-constants');
9
+
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+ const http = require('http');
13
+ const https = require('https');
14
+ const url = require('url');
15
+
16
+ const CONTEXT_LINES = 8;
17
+
18
+ function extractContext(filePath, lineNum, lines = CONTEXT_LINES) {
19
+ try {
20
+ const abs = path.resolve(process.cwd(), filePath);
21
+ const content = fs.readFileSync(abs, 'utf8').split('\n');
22
+ const start = Math.max(0, lineNum - lines - 1);
23
+ const end = Math.min(content.length, lineNum + lines);
24
+ return content
25
+ .slice(start, end)
26
+ .map((l, i) => `${start + i + 1}: ${l}`)
27
+ .join('\n');
28
+ } catch {
29
+ return '(source not available)';
30
+ }
31
+ }
32
+
33
+ function printTeaser() {
34
+ console.log('');
35
+ console.log(` ${CYAN}ℹ Deep analysis requires scd-server with the Deep Analysis Pack.${RESET}`);
36
+ console.log(` ${DIM} See https://securecodebydesign.com for subscription options.${RESET}`);
37
+ console.log('');
38
+ }
39
+
40
+ /**
41
+ * Simple HTTP POST using http.request — avoids fetch socket timeout issues.
42
+ */
43
+ function httpPost(targetUrl, body, token, timeoutMs) {
44
+ return new Promise((resolve, reject) => {
45
+ const parsed = new url.URL(targetUrl);
46
+ const isHttps = parsed.protocol === 'https:';
47
+ const lib = isHttps ? https : http;
48
+ const bodyStr = typeof body === 'string' ? body : JSON.stringify(body);
49
+
50
+ const options = {
51
+ hostname: parsed.hostname,
52
+ port: parsed.port || (isHttps ? 443 : 80),
53
+ path: parsed.pathname + parsed.search,
54
+ method: 'POST',
55
+ headers: {
56
+ 'Content-Type': 'application/json',
57
+ 'Authorization': 'Bearer ' + token,
58
+ 'Content-Length': Buffer.byteLength(bodyStr),
59
+ },
60
+ timeout: timeoutMs,
61
+ };
62
+
63
+ const req = lib.request(options, (res) => {
64
+ const chunks = [];
65
+ res.on('data', chunk => chunks.push(chunk));
66
+ res.on('end', () => {
67
+ const raw = Buffer.concat(chunks).toString('utf8');
68
+ resolve({ status: res.statusCode, body: raw });
69
+ });
70
+ });
71
+
72
+ req.on('timeout', () => {
73
+ req.destroy(new Error(`Request timed out after ${timeoutMs}ms`));
74
+ });
75
+
76
+ req.on('error', reject);
77
+ req.write(bodyStr);
78
+ req.end();
79
+ });
80
+ }
81
+
82
+ async function deepAnalyze(findings, opts = {}) {
83
+ const { centralUrl, token, repoId, scanId, trustLevel = 'balanced', verbose = false } = opts;
84
+
85
+ if (!centralUrl) { printTeaser(); return new Map(); }
86
+
87
+ if (!token) {
88
+ console.log('\nRED❌ --deep requires an scd-server API token.' + RESET);
89
+ console.log(DIM + ' Run: scd configure --token <token>' + RESET + '\n');
90
+ return new Map();
91
+ }
92
+
93
+ const eligible = Array.from(findings).filter(f =>
94
+ f.severity === 'CRITICAL' || f.severity === 'HIGH'
95
+ );
96
+
97
+ if (eligible.length === 0) return new Map();
98
+
99
+ if (verbose) {
100
+ process.stdout.write(`${DIM} 🔍 Sending ${eligible.length} findings to scd-server for deep analysis...${RESET}\n`);
101
+ }
102
+
103
+ const payload = eligible.map(f => ({
104
+ ruleId: f.ruleId,
105
+ name: f.name || f.ruleId,
106
+ severity: f.severity,
107
+ file: f.filePath,
108
+ line: f.line,
109
+ snippet: f.snippet || null,
110
+ context: extractContext(f.filePath, f.line),
111
+ problem: f.description || null,
112
+ }));
113
+
114
+ try {
115
+ const { getDeepTimeout } = require('./global-config');
116
+ const response = await httpPost(
117
+ centralUrl + '/api/v1/deep/analyze',
118
+ { repoId, scanId, trust_level: trustLevel, findings: payload },
119
+ token,
120
+ getDeepTimeout()
121
+ );
122
+
123
+ if (response.status === 503 || response.status === 404) {
124
+ printTeaser();
125
+ return new Map();
126
+ }
127
+
128
+ if (response.status !== 200) {
129
+ let errMsg = `HTTP ${response.status}`;
130
+ try { errMsg = JSON.parse(response.body).error || errMsg; } catch {}
131
+ throw new Error(errMsg);
132
+ }
133
+
134
+ const data = JSON.parse(response.body);
135
+
136
+ const results = new Map();
137
+ for (const result of (data.results || [])) {
138
+ const fp = result.file || result.filePath;
139
+ if (!fp) continue;
140
+ if (!results.has(fp)) results.set(fp, []);
141
+ results.get(fp).push(result);
142
+ }
143
+ return results;
144
+
145
+ } catch (err) {
146
+ console.log(`\nYELLOW⚠️ Deep analysis unavailable: ${err.message}${RESET}`);
147
+ console.log(DIM + ' Scan completed. Run again with --deep when scd-server is reachable.' + RESET + '\n');
148
+ return new Map();
149
+ }
150
+ }
151
+
152
+ const SEV_COLORS = { CRITICAL: RED, HIGH: YELLOW };
153
+
154
+ function formatDeepSection(findings, deepResults) {
155
+ if (!deepResults || deepResults.size === 0) return '';
156
+
157
+
158
+ const lines = [];
159
+ lines.push('');
160
+ lines.push('─'.repeat(60));
161
+ lines.push(` ${CYAN}${BOLD}↓ Deep analysis (--deep) ↓${RESET}`);
162
+ lines.push('─'.repeat(60));
163
+
164
+ for (const [filePath, analyses] of deepResults) {
165
+ if (!Array.isArray(analyses) || analyses.length === 0) continue;
166
+
167
+ if (analyses[0]?._error) {
168
+ lines.push(`\n ${RED}❌ ${filePath}${RESET}`);
169
+ lines.push(` ${DIM}Error: ${analyses[0]._error}${RESET}`);
170
+ continue;
171
+ }
172
+
173
+ for (const analysis of analyses) {
174
+ const original = findings.find(f =>
175
+ f.ruleId === analysis.ruleId && f.line === analysis.line &&
176
+ (f.filePath === filePath || f.filePath.endsWith(filePath) || filePath.endsWith(f.filePath))
177
+ );
178
+ const sev = original?.severity ?? 'HIGH';
179
+ const color = SEV_COLORS[sev] ?? YELLOW;
180
+ const sevIcon = sev === 'CRITICAL' ? '🔴' : '🟠';
181
+
182
+ lines.push('');
183
+ lines.push(` ${color}${BOLD}${sevIcon} ${analysis.ruleId} · ${filePath}:${analysis.line}${RESET}`);
184
+
185
+ if (analysis.confirmed === false) {
186
+ lines.push(` ${GREEN}${BOLD}✓ Likely false positive${RESET} ${DIM}(confidence: ${analysis.confidence})${RESET}`);
187
+ if (analysis.false_positive_reason) lines.push(` ${DIM}${analysis.false_positive_reason}${RESET}`);
188
+ continue;
189
+ }
190
+
191
+ // Normalise confidence — 7b model may return numeric (0-1) instead of string
192
+ const rawConf = analysis.confidence;
193
+ const conf = typeof rawConf === 'number'
194
+ ? (rawConf >= 0.8 ? 'HIGH' : rawConf >= 0.5 ? 'MEDIUM' : 'LOW')
195
+ : (rawConf || 'LOW');
196
+ const confColor = conf === 'HIGH' ? RED : conf === 'MEDIUM' ? YELLOW : DIM;
197
+ lines.push(` ${BOLD}Assessment:${RESET} ${RED}✗ Confirmed${RESET} ${DIM}confidence: ${confColor}${conf}${RESET}`);
198
+
199
+ if (analysis.attack_scenario) {
200
+ lines.push('');
201
+ lines.push(` ${BOLD}Attack:${RESET}`);
202
+ const words = analysis.attack_scenario.split(' ');
203
+ let cur = ' ';
204
+ for (const w of words) {
205
+ if (cur.length + w.length > 82) { lines.push(`${DIM}${cur}${RESET}`); cur = ' ' + w + ' '; }
206
+ else cur += w + ' ';
207
+ }
208
+ if (cur.trim()) lines.push(`${DIM}${cur}${RESET}`);
209
+ }
210
+
211
+ if (analysis.fix_code) {
212
+ lines.push('');
213
+ lines.push(` ${BOLD}Fix:${RESET}`);
214
+ for (const fl of analysis.fix_code.split('\n')) lines.push(` ${GREEN}${fl}${RESET}`);
215
+ }
216
+
217
+ if (analysis.fix_explanation) lines.push(` ${DIM}↳ ${analysis.fix_explanation}${RESET}`);
218
+ }
219
+ }
220
+
221
+ lines.push('');
222
+ return lines.join('\n');
223
+ }
224
+
225
+ module.exports = { deepAnalyze, formatDeepSection };
package/lib/doctor.js ADDED
@@ -0,0 +1,236 @@
1
+ const { RESET, BOLD, DIM, RED, GREEN, YELLOW, CYAN } = require('./output-constants');
2
+ /**
3
+ * doctor.js
4
+ * Checks that hooks are active, up to date, and working.
5
+ * Maps to "Layer 1 – Technical self-check" in the architecture.
6
+ */
7
+
8
+ const { execSync } = require('child_process');
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const os = require('os');
12
+
13
+ const HOOKS_DIR = path.join(os.homedir(), '.scd', 'hooks');
14
+ const STALE_ATTEMPTS = 10;
15
+ const GRACE_DAYS = 7;
16
+
17
+ async function doctor() {
18
+ console.log(CYAN + '\n Secure Code by Design – System check' + RESET + '\n');
19
+
20
+ let allOk = true;
21
+
22
+ // 1. Check global hooks path
23
+ const NULL_DEVICE = process.platform === 'win32' ? 'NUL' : '/dev/null';
24
+ try {
25
+ const hooksPath = execSync('git config --global core.hooksPath', {
26
+ encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],
27
+ }).trim();
28
+ if (hooksPath === HOOKS_DIR) {
29
+ ok('Global hooks configured', hooksPath);
30
+ } else if (hooksPath === NULL_DEVICE) {
31
+ fail('Global hooks disabled — core.hooksPath set to ' + hooksPath);
32
+ console.log(DIM + ' This disables hooks for ALL repos on this machine.' + RESET);
33
+ console.log(DIM + ' Fix: git config --global core.hooksPath "' + HOOKS_DIR + '"' + RESET);
34
+ allOk = false;
35
+ } else {
36
+ warn('Global hooks pointing to unexpected directory', hooksPath);
37
+ console.log(DIM + ' Expected: ' + HOOKS_DIR + RESET);
38
+ console.log(DIM + ' Fix: git config --global core.hooksPath "' + HOOKS_DIR + '"' + RESET);
39
+ }
40
+ } catch {
41
+ fail('Global hooks NOT configured');
42
+ console.log(DIM + ' Run: scd install' + RESET);
43
+ allOk = false;
44
+ }
45
+
46
+ // 2. Check hook files exist and are executable
47
+ const isWindows = process.platform === 'win32';
48
+ for (const hook of ['pre-commit', 'pre-push']) {
49
+ const hookPath = path.join(HOOKS_DIR, hook);
50
+ if (fs.existsSync(hookPath)) {
51
+ if (isWindows) {
52
+ // Windows does not have executable bits — presence is sufficient
53
+ ok(`${hook} hook active`);
54
+ } else {
55
+ try {
56
+ fs.accessSync(hookPath, fs.constants.X_OK);
57
+ ok(`${hook} hook active`);
58
+ } catch {
59
+ fail(`${hook} hook exists but is not executable`);
60
+ console.log(`${DIM} Run: chmod +x ${hookPath}${RESET}`);
61
+ allOk = false;
62
+ }
63
+ }
64
+ } else {
65
+ fail(`${hook} hook missing`);
66
+ allOk = false;
67
+ }
68
+ }
69
+
70
+ // 3. Check current repo (if in one)
71
+ try {
72
+ const repoRoot = execSync('git rev-parse --show-toplevel', { encoding: 'utf8', stdio: 'pipe' }).trim();
73
+ ok('Inside a git repo', repoRoot);
74
+
75
+ // Check if scd repo hooks --disable has been used for this repo
76
+ const { getHookStatus } = require('./hooks-manager');
77
+ const hookStatus = getHookStatus(repoRoot);
78
+ if (hookStatus.status === 'disabled') {
79
+ warn('Hooks disabled for this repo', 'core.hooksPath → ' + hookStatus.hooksPath);
80
+ console.log(DIM + ' Re-enable with: scd repo hooks --enable' + RESET);
81
+ } else if (hookStatus.status === 'global-broken') {
82
+ fail('Global hooks disabled — core.hooksPath set to /dev/null');
83
+ console.log(DIM + ' Fix: git config --global core.hooksPath "' + HOOKS_DIR + '"' + RESET);
84
+ allOk = false;
85
+ }
86
+
87
+ // Check if local .git/hooks would override global (old-style hook setup)
88
+ const localHooksDir = path.join(repoRoot, '.git', 'hooks');
89
+ const localPrePush = path.join(localHooksDir, 'pre-push');
90
+ if (fs.existsSync(localPrePush)) {
91
+ const content = fs.readFileSync(localPrePush, 'utf8');
92
+ if (!content.includes('scd')) {
93
+ warn('Local pre-push hook found that is not Secure Code by Design', localPrePush);
94
+ console.log(DIM + ' Local hooks override global. Verify that your hooks work together.' + RESET);
95
+ }
96
+ }
97
+ } catch {
98
+ info('Not inside a git repo');
99
+ }
100
+
101
+ // 4. Push queue status
102
+ try {
103
+ const { getCentralUrl } = require('./global-config');
104
+ const centralUrl = getCentralUrl();
105
+ if (centralUrl) {
106
+ const { queueSize, staleCount, isPastGrace } = require('./push-queue');
107
+ const pending = queueSize();
108
+ const stale = staleCount();
109
+ const grace = isPastGrace();
110
+
111
+ ok('Central URL configured', centralUrl);
112
+
113
+ if (stale > 0) {
114
+ warn(`Push queue has ${stale} stale event(s) (${STALE_ATTEMPTS}+ failed attempts)`);
115
+ console.log(DIM + ' Run: scd repo --verify --clean to purge stale events' + RESET);
116
+ allOk = false;
117
+ } else if (grace) {
118
+ warn(`Push queue has events older than ${GRACE_DAYS} days – central may be unreachable`);
119
+ allOk = false;
120
+ } else if (pending > 0) {
121
+ info(`Push queue: ${pending} event(s) pending sync`);
122
+ } else {
123
+ ok('Push queue empty – all events synced');
124
+ }
125
+ } else {
126
+ info('Push queue inactive (no central URL configured)');
127
+ console.log(DIM + ' scd configure --central-url https://your-server:3000' + RESET);
128
+ }
129
+ } catch {
130
+ // Non-fatal if push-queue module unavailable
131
+ }
132
+
133
+ // 5. scd-server health check (if central URL configured)
134
+ try {
135
+ const { getCentralUrl, getCentralToken } = require('./global-config');
136
+ const centralUrl = getCentralUrl();
137
+ if (centralUrl) {
138
+ const token = getCentralToken();
139
+ const http = centralUrl.startsWith('https') ? require('https') : require('http');
140
+ const baseUrl = centralUrl.replace(/\/$/, '');
141
+
142
+ const result = await new Promise((resolve) => {
143
+ const req = http.get(
144
+ baseUrl + '/api/v1/health',
145
+ { headers: token ? { 'Authorization': `Bearer ${token}` } : {}, timeout: 5000 },
146
+ (res) => {
147
+ let data = '';
148
+ res.on('data', chunk => { data += chunk; });
149
+ res.on('end', () => {
150
+ try { resolve({ status: res.statusCode, body: JSON.parse(data) }); }
151
+ catch { resolve({ status: res.statusCode, body: null }); }
152
+ });
153
+ }
154
+ );
155
+ req.on('timeout', () => { req.destroy(); resolve({ status: 0, body: null }); });
156
+ req.on('error', () => resolve({ status: 0, body: null }));
157
+ });
158
+
159
+ if (result.status === 0) {
160
+ fail('scd-server unreachable');
161
+ console.log(`${DIM} ${baseUrl}${RESET}`);
162
+ allOk = false;
163
+ } else if (result.body && result.body.license) {
164
+ const lic = result.body.license;
165
+ if (lic.tier === 'invalid') {
166
+ // Strip support URL from reason — administrator should resolve, not end users
167
+ const reason = (lic.reason || 'unknown reason')
168
+ .replace(/\.?\s*Contact support@[^.]+\.com\.?/gi, '').trim();
169
+ fail(`scd-server license invalid — ${reason}`);
170
+ console.log(DIM + ' Contact your local scd-server administrator.' + RESET);
171
+ allOk = false;
172
+ } else if (lic.tier === 'development') {
173
+ ok('scd-server reachable', `development mode · ${baseUrl}`);
174
+ } else {
175
+ const exp = lic.expiry ? ` · expires ${lic.expiry}` : '';
176
+ ok('scd-server reachable', `${lic.tier}${exp} · ${baseUrl}`);
177
+ }
178
+
179
+ // Show server version + min CLI version
180
+ try {
181
+ const srvVer = result.body.version || null;
182
+ const minVer = result.body.min_cli_version || null;
183
+ const { setServerVersionInfo } = require('./global-config');
184
+ setServerVersionInfo(srvVer, minVer);
185
+
186
+ if (srvVer) console.log(`${DIM} Server version: v${srvVer}${RESET}`);
187
+ if (minVer) console.log(`${DIM} Min CLI version: v${minVer}${RESET}`);
188
+
189
+ const { getVersionWarning } = require('./version-check');
190
+ const versionWarn = getVersionWarning();
191
+ if (versionWarn) {
192
+ console.log('');
193
+ console.log(`${YELLOW} ${versionWarn.replace(/\n /g, '\n ')}${RESET}`);
194
+ allOk = false;
195
+ }
196
+ } catch { /* non-fatal */ }
197
+
198
+ // Show configured timeouts
199
+ try {
200
+ const { getServerTimeout, getDeepTimeout } = require('./global-config');
201
+ const sTout = getServerTimeout();
202
+ const dTout = getDeepTimeout();
203
+ const fmt = ms => ms >= 60000 ? `${Math.round(ms/60000)}m` : `${Math.round(ms/1000)}s`;
204
+ console.log(`${DIM} Server timeout: ${fmt(sTout)} · Deep timeout: ${fmt(dTout)}${RESET}`);
205
+ } catch { /* non-fatal */ }
206
+ } else {
207
+ ok('scd-server reachable', baseUrl);
208
+ }
209
+ }
210
+ } catch { /* non-fatal */ }
211
+
212
+ // 6. Summary
213
+ console.log('');
214
+ if (allOk) {
215
+ console.log(GREEN + BOLD + ' ✅ Everything looks good!' + RESET);
216
+ } else {
217
+ console.log(RED + BOLD + ' ⚠️ Action required (see above)' + RESET);
218
+ }
219
+ console.log('');
220
+ }
221
+
222
+
223
+ function ok(msg, detail = null) {
224
+ console.log(`${GREEN} ✅ ${msg}${RESET}${detail ? DIM + ' – ' + detail + RESET : ''}`);
225
+ }
226
+ function fail(msg) {
227
+ console.log(`${RED} ❌ ${msg}${RESET}`);
228
+ }
229
+ function warn(msg, detail = null) {
230
+ console.log(`${YELLOW} ⚠️ ${msg}${RESET}${detail ? DIM + ' – ' + detail + RESET : ''}`);
231
+ }
232
+ function info(msg) {
233
+ console.log(`${CYAN} ℹ️ ${msg}${RESET}`);
234
+ }
235
+
236
+ module.exports = { doctor };