fhirsmith 0.14.0 → 0.14.2
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/CHANGELOG.md +64 -0
- package/README.md +3 -2
- package/config-template.json +20 -0
- package/library/fhir-openapi-schema.js +441 -0
- package/library/html-server.js +9 -3
- package/library/languages.js +15 -0
- package/library/npm-audit.js +265 -0
- package/library/openapi-doc.js +557 -0
- package/library/same-origin.js +62 -0
- package/library/utilities.js +21 -0
- package/nginx.md +17 -9
- package/package.json +10 -4
- package/packages/openapi.js +13 -0
- package/packages/openapi.yaml +603 -0
- package/packages/packages-template.html +4 -2
- package/packages/packages.js +286 -81
- package/packages/readme.md +120 -0
- package/publisher/publisher-template.html +1 -0
- package/publisher/publisher.js +95 -42
- package/registry/api.js +117 -41
- package/registry/crawler.js +24 -0
- package/registry/fhirsmith-releases.js +170 -0
- package/registry/model.js +10 -0
- package/registry/openapi.js +13 -0
- package/registry/openapi.yaml +311 -0
- package/registry/readme.md +107 -208
- package/registry/registry-template.html +4 -1
- package/registry/registry.js +183 -20
- package/server.js +25 -6
- package/testing/openapi-schemas.config.js +58 -0
- package/testing/openapi-schemas.json +1920 -0
- package/testing/openapi.js +15 -0
- package/testing/openapi.yaml +273 -0
- package/testing/readme.md +75 -1
- package/testing/render.js +270 -29
- package/testing/search.js +1 -1
- package/testing/store.js +152 -8
- package/testing/testing-template.html +2 -0
- package/testing/testing.js +307 -9
- package/translations/Messages.properties +1 -0
- package/tx/README.md +58 -0
- package/tx/cm/cm-database.js +10 -1
- package/tx/cm/cm-package.js +8 -1
- package/tx/cs/cs-rxnorm.js +23 -6
- package/tx/cs/cs-snomed.js +3 -72
- package/tx/html/tx-template.html +2 -0
- package/tx/library/canonical-resource.js +50 -1
- package/tx/library/codesystem.js +1 -0
- package/tx/library/conceptmap.js +1 -0
- package/tx/library/valueset.js +16 -0
- package/tx/library.js +17 -2
- package/tx/openapi-operations.js +363 -0
- package/tx/openapi-schemas.config.js +118 -0
- package/tx/openapi-schemas.json +7903 -0
- package/tx/openapi.js +289 -0
- package/tx/openapi.yaml +151 -0
- package/tx/operation-context.js +15 -0
- package/tx/params.js +3 -12
- package/tx/provider.js +10 -1
- package/tx/sct/editions.js +70 -0
- package/tx/tx-html.js +6 -0
- package/tx/tx.js +55 -0
- package/tx/vs/vs-database.js +14 -5
- package/tx/vs/vs-package.js +10 -0
- package/tx/workers/expand.js +37 -18
- package/tx/workers/metadata.js +0 -13
- package/tx/workers/translate.js +25 -1
- package/tx/workers/validate.js +74 -24
- package/tx/workers/worker.js +32 -43
- package/utilities/extract-schema-json.js +167 -0
- package/utilities/generate-openapi-schemas.js +49 -0
- package/utilities/submit-test-report.js +1 -1
- package/xig/xig.js +2 -4
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
// library/npm-audit.js
|
|
2
|
+
// Periodic self-check of this server's installed npm packages against the npm advisory
|
|
3
|
+
// database - the same lookup `npm audit` does, made directly so it needs neither the npm
|
|
4
|
+
// binary nor a particular working directory. Reports only; it never changes anything.
|
|
5
|
+
//
|
|
6
|
+
// The package list comes from node_modules/.package-lock.json (what is actually
|
|
7
|
+
// installed), falling back to package-lock.json. Dev dependencies are left out, and
|
|
8
|
+
// fhirsmith itself is included, so an advisory published against fhirsmith shows up too.
|
|
9
|
+
//
|
|
10
|
+
// Config (all optional), top level of config.json:
|
|
11
|
+
// "npmAudit": { "enabled": true, "intervalHours": 24 }
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const axios = require('axios');
|
|
16
|
+
const escape = require('escape-html');
|
|
17
|
+
|
|
18
|
+
const DEFAULT_URL = 'https://registry.npmjs.org/-/npm/v1/security/advisories/bulk';
|
|
19
|
+
const SEVERITIES = ['critical', 'high', 'moderate', 'low', 'info'];
|
|
20
|
+
const HOUR_MS = 60 * 60 * 1000;
|
|
21
|
+
const FIRST_RUN_DELAY_MS = 60 * 1000; // let startup finish first
|
|
22
|
+
|
|
23
|
+
function describeAgo(time, now = Date.now()) {
|
|
24
|
+
const mins = Math.max(0, Math.floor((now - time) / 60000));
|
|
25
|
+
if (mins < 1) {
|
|
26
|
+
return 'just now';
|
|
27
|
+
}
|
|
28
|
+
if (mins < 60) {
|
|
29
|
+
return `${mins} minute${mins === 1 ? '' : 's'} ago`;
|
|
30
|
+
}
|
|
31
|
+
const hours = Math.floor(mins / 60);
|
|
32
|
+
if (hours < 48) {
|
|
33
|
+
return `${hours} hour${hours === 1 ? '' : 's'} ago`;
|
|
34
|
+
}
|
|
35
|
+
return `${Math.floor(hours / 24)} days ago`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
class NpmAudit {
|
|
39
|
+
constructor(config = {}, logger = null, appDir = path.join(__dirname, '..'), stats = null) {
|
|
40
|
+
this.enabled = config.enabled !== false;
|
|
41
|
+
this.intervalHours = config.intervalHours > 0 ? config.intervalHours : 24;
|
|
42
|
+
this.url = config.url || DEFAULT_URL;
|
|
43
|
+
this.timeout = config.timeout || 60000;
|
|
44
|
+
this.logger = logger;
|
|
45
|
+
this.appDir = appDir;
|
|
46
|
+
this.stats = stats;
|
|
47
|
+
this.timers = [];
|
|
48
|
+
this.running = false;
|
|
49
|
+
// the outcome of the last completed check
|
|
50
|
+
this.checkedAt = null; // Date of the last successful check
|
|
51
|
+
this.packageCount = 0;
|
|
52
|
+
this.findings = []; // [{name, installed: [versions], id, severity, title, url, range}]
|
|
53
|
+
this.lastError = null; // message from the last attempt, if it failed
|
|
54
|
+
this.lastAttempt = null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* name -> [installed versions], production packages only, plus this package itself
|
|
59
|
+
*/
|
|
60
|
+
collectPackages() {
|
|
61
|
+
let lock = null;
|
|
62
|
+
for (const file of [path.join(this.appDir, 'node_modules', '.package-lock.json'), path.join(this.appDir, 'package-lock.json')]) {
|
|
63
|
+
try {
|
|
64
|
+
lock = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
65
|
+
break;
|
|
66
|
+
} catch (e) {
|
|
67
|
+
// try the next one
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (!lock || !lock.packages) {
|
|
71
|
+
throw new Error('No package-lock.json found to audit');
|
|
72
|
+
}
|
|
73
|
+
const found = new Map();
|
|
74
|
+
const add = (name, version) => {
|
|
75
|
+
if (!found.has(name)) {
|
|
76
|
+
found.set(name, new Set());
|
|
77
|
+
}
|
|
78
|
+
found.get(name).add(version);
|
|
79
|
+
};
|
|
80
|
+
for (const [key, info] of Object.entries(lock.packages)) {
|
|
81
|
+
if (!key || !info || info.dev || info.link || !info.version) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const marker = 'node_modules/';
|
|
85
|
+
const name = info.name || key.substring(key.lastIndexOf(marker) + marker.length);
|
|
86
|
+
add(name, info.version);
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(this.appDir, 'package.json'), 'utf8'));
|
|
90
|
+
if (pkg.name && pkg.version) {
|
|
91
|
+
add(pkg.name, pkg.version);
|
|
92
|
+
}
|
|
93
|
+
} catch (e) {
|
|
94
|
+
// no package.json - nothing to add
|
|
95
|
+
}
|
|
96
|
+
const result = {};
|
|
97
|
+
for (const [name, versions] of found) {
|
|
98
|
+
result[name] = [...versions].sort();
|
|
99
|
+
}
|
|
100
|
+
return result;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async run() {
|
|
104
|
+
if (this.running) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
this.running = true;
|
|
108
|
+
this.lastAttempt = new Date();
|
|
109
|
+
if (this.stats) {
|
|
110
|
+
this.stats.task('npm audit', 'Checking');
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
const packages = this.collectPackages();
|
|
114
|
+
const response = await axios.post(this.url, packages, {
|
|
115
|
+
timeout: this.timeout,
|
|
116
|
+
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }
|
|
117
|
+
});
|
|
118
|
+
const data = response.data && typeof response.data === 'object' ? response.data : {};
|
|
119
|
+
const findings = [];
|
|
120
|
+
for (const [name, advisories] of Object.entries(data)) {
|
|
121
|
+
for (const a of Array.isArray(advisories) ? advisories : []) {
|
|
122
|
+
findings.push({
|
|
123
|
+
name,
|
|
124
|
+
installed: packages[name] || [],
|
|
125
|
+
id: a.id,
|
|
126
|
+
severity: SEVERITIES.includes(a.severity) ? a.severity : 'info',
|
|
127
|
+
title: a.title || '',
|
|
128
|
+
url: a.url || '',
|
|
129
|
+
range: a.vulnerable_versions || ''
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
findings.sort((a, b) => SEVERITIES.indexOf(a.severity) - SEVERITIES.indexOf(b.severity) || a.name.localeCompare(b.name));
|
|
134
|
+
this.findings = findings;
|
|
135
|
+
this.packageCount = Object.keys(packages).length;
|
|
136
|
+
this.checkedAt = new Date();
|
|
137
|
+
this.lastError = null;
|
|
138
|
+
const summary = this.summary();
|
|
139
|
+
if (findings.length > 0) {
|
|
140
|
+
if (this.logger) {
|
|
141
|
+
this.logger.warn(`npm audit: ${summary} in ${this.packageCount} packages: ` +
|
|
142
|
+
findings.map(f => `${f.name} (${f.severity}) ${f.url}`).join('; '));
|
|
143
|
+
}
|
|
144
|
+
if (this.stats) {
|
|
145
|
+
this.stats.taskError('npm audit', summary);
|
|
146
|
+
}
|
|
147
|
+
} else {
|
|
148
|
+
if (this.logger) {
|
|
149
|
+
this.logger.info(`npm audit: no known vulnerabilities in ${this.packageCount} packages`);
|
|
150
|
+
}
|
|
151
|
+
if (this.stats) {
|
|
152
|
+
this.stats.taskDone('npm audit', `No known vulnerabilities (${this.packageCount} packages)`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
} catch (error) {
|
|
156
|
+
this.lastError = error.message;
|
|
157
|
+
if (this.logger) {
|
|
158
|
+
this.logger.warn('npm audit could not be run: ' + error.message);
|
|
159
|
+
}
|
|
160
|
+
if (this.stats) {
|
|
161
|
+
this.stats.taskError('npm audit', 'Could not check: ' + error.message);
|
|
162
|
+
}
|
|
163
|
+
} finally {
|
|
164
|
+
this.running = false;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
start() {
|
|
169
|
+
if (!this.enabled) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (this.stats) {
|
|
173
|
+
this.stats.addTask('npm audit', `${this.intervalHours} hr`);
|
|
174
|
+
}
|
|
175
|
+
// run() handles its own errors, so the promise never rejects
|
|
176
|
+
const first = setTimeout(() => void this.run(), FIRST_RUN_DELAY_MS);
|
|
177
|
+
const repeat = setInterval(() => void this.run(), this.intervalHours * HOUR_MS);
|
|
178
|
+
for (const t of [first, repeat]) {
|
|
179
|
+
if (t.unref) {
|
|
180
|
+
t.unref();
|
|
181
|
+
}
|
|
182
|
+
this.timers.push(t);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
stop() {
|
|
187
|
+
for (const t of this.timers) {
|
|
188
|
+
clearTimeout(t);
|
|
189
|
+
clearInterval(t);
|
|
190
|
+
}
|
|
191
|
+
this.timers = [];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
counts() {
|
|
195
|
+
const counts = {};
|
|
196
|
+
for (const f of this.findings) {
|
|
197
|
+
counts[f.severity] = (counts[f.severity] || 0) + 1;
|
|
198
|
+
}
|
|
199
|
+
return counts;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// "3 known vulnerabilities (1 critical, 2 high)"
|
|
203
|
+
summary() {
|
|
204
|
+
const n = this.findings.length;
|
|
205
|
+
if (n === 0) {
|
|
206
|
+
return 'no known vulnerabilities';
|
|
207
|
+
}
|
|
208
|
+
const counts = this.counts();
|
|
209
|
+
const parts = SEVERITIES.filter(s => counts[s]).map(s => `${counts[s]} ${s}`);
|
|
210
|
+
return `${n} known vulnerabilit${n === 1 ? 'y' : 'ies'} (${parts.join(', ')})`;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
isSerious() {
|
|
214
|
+
return this.findings.some(f => f.severity === 'critical' || f.severity === 'high');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Home page: nothing unless there is something to report
|
|
219
|
+
*/
|
|
220
|
+
renderBanner(now = Date.now()) {
|
|
221
|
+
if (!this.enabled || !this.checkedAt || this.findings.length === 0) {
|
|
222
|
+
return '';
|
|
223
|
+
}
|
|
224
|
+
const cls = this.isSerious() ? 'alert-danger' : 'alert-warning';
|
|
225
|
+
return `<div class="alert ${cls}" role="alert"><strong>Security:</strong> npm audit found ` +
|
|
226
|
+
`${escape(this.summary())} in the packages this server uses ` +
|
|
227
|
+
`(checked ${escape(describeAgo(this.checkedAt.getTime(), now))}). ` +
|
|
228
|
+
'The server administrator should upgrade to the latest FHIRsmith release. ' +
|
|
229
|
+
'<a href="/dashboard#npm-audit">Details</a></div>';
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Dashboard: always shown, so a check that is failing or switched off is visible too
|
|
234
|
+
*/
|
|
235
|
+
renderDashboard(now = Date.now()) {
|
|
236
|
+
let html = '<div id="npm-audit">';
|
|
237
|
+
if (!this.enabled) {
|
|
238
|
+
return html + '<p><strong>npm audit:</strong> disabled (npmAudit.enabled = false)</p></div>';
|
|
239
|
+
}
|
|
240
|
+
if (!this.checkedAt) {
|
|
241
|
+
html += '<p><strong>npm audit:</strong> ';
|
|
242
|
+
html += this.lastError ? `<span style="color:#b00">could not be run: ${escape(this.lastError)}</span>` : 'not run yet';
|
|
243
|
+
return html + '</p></div>';
|
|
244
|
+
}
|
|
245
|
+
const colour = this.findings.length === 0 ? '#070' : (this.isSerious() ? '#b00' : '#b60');
|
|
246
|
+
html += `<p><strong>npm audit:</strong> <span style="color:${colour}">${escape(this.summary())}</span>` +
|
|
247
|
+
` in ${this.packageCount} packages, checked ${escape(describeAgo(this.checkedAt.getTime(), now))}`;
|
|
248
|
+
if (this.lastError) {
|
|
249
|
+
html += ` <span style="color:#b00">(the latest check failed: ${escape(this.lastError)})</span>`;
|
|
250
|
+
}
|
|
251
|
+
html += '</p>';
|
|
252
|
+
if (this.findings.length > 0) {
|
|
253
|
+
html += '<table class="grid"><tr><th>Severity</th><th>Package</th><th>Installed</th><th>Vulnerable</th><th>Advisory</th></tr>';
|
|
254
|
+
for (const f of this.findings) {
|
|
255
|
+
const link = /^https:\/\//.test(f.url) ? `<a href="${escape(f.url)}">${escape(f.title || f.url)}</a>` : escape(f.title);
|
|
256
|
+
html += `<tr><td>${escape(f.severity)}</td><td>${escape(f.name)}</td><td>${escape(f.installed.join(', '))}</td>` +
|
|
257
|
+
`<td>${escape(f.range)}</td><td>${link}</td></tr>`;
|
|
258
|
+
}
|
|
259
|
+
html += '</table>';
|
|
260
|
+
}
|
|
261
|
+
return html + '</div>';
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
module.exports = { NpmAudit, describeAgo };
|