Add HL7 FHIR validator support
This commit is contained in:
Родитель
09ae95f0dc
Коммит
94924221be
|
@ -130,4 +130,10 @@ testdata/missing_kid_key.json
|
|||
tmp/*
|
||||
|
||||
# full fhir schema
|
||||
schema/fhir-schema-full.json
|
||||
schema/fhir-schema-full.json
|
||||
|
||||
# temp fhir bundle file
|
||||
~temp.fhirbundle.json
|
||||
|
||||
# downloaded HL7 validator for JRE deployment
|
||||
validator_cli.jar
|
||||
|
|
|
@ -2,10 +2,18 @@
|
|||
"eslint.alwaysShowStatus": true,
|
||||
"eslint.format.enable": true,
|
||||
"cSpell.words": [
|
||||
"badcrl",
|
||||
"clearline",
|
||||
"Codeable",
|
||||
"Excludable",
|
||||
"execa",
|
||||
"exitcode",
|
||||
"fhir",
|
||||
"fhirbundle",
|
||||
"fhirhealthcard",
|
||||
"fhirout",
|
||||
"fhirvalidator",
|
||||
"fidm",
|
||||
"grayscale",
|
||||
"healthcard",
|
||||
"istextorbinary",
|
||||
|
@ -13,8 +21,12 @@
|
|||
"jwks",
|
||||
"jwkset",
|
||||
"jwspayload",
|
||||
"libressl",
|
||||
"logfile",
|
||||
"loglevel",
|
||||
"loinc",
|
||||
"LOINC",
|
||||
"myfhirbundle",
|
||||
"npmpackage",
|
||||
"outdir",
|
||||
"pako",
|
||||
|
@ -22,6 +34,8 @@
|
|||
"qrcode",
|
||||
"qrnumeric",
|
||||
"repos",
|
||||
"testdata"
|
||||
"testdata",
|
||||
"testif",
|
||||
"uuidv"
|
||||
]
|
||||
}
|
13
README.md
13
README.md
|
@ -97,6 +97,7 @@ To validate health card artifacts, use the `shc-validator.ts` script, or simply
|
|||
(choices: "fhirbundle", "jwspayload", "jws", "healthcard", "fhirhealthcard", "qrnumeric", "qr", "jwkset")
|
||||
-l, --loglevel <loglevel> set the minimum log level (choices: "debug", "info", "warning", "error", "fatal", default: "warning")
|
||||
-P, --profile <profile> vaccination profile to validate (choices: "any", "usa-covid19-immunization", default: "any")
|
||||
-V, --validator <validator> FHIR bundle validator (choices: "default", "fhirvalidator" (requires Java runtime or Docker))
|
||||
-d, --directory <directory> trusted issuer directory to validate against
|
||||
-o, --logout <path> output path for log (if not specified log will be printed on console)
|
||||
-f, --fhirout <path> output path for the extracted FHIR bundle
|
||||
|
@ -160,9 +161,15 @@ const results = validate.jws(jwsString);
|
|||
results.then(console.log)
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
Validation of the FHIR bundle is currently limited. The tool validates a subset of the full FHIR schema; the behavior can be scoped by using the profile option, or changed by modifying the `src/prune-fhir-schema.ts` script. Extensive tests and conformance to the [Vaccination & Testing Implementation Guide](http://build.fhir.org/ig/dvci/vaccine-credential-ig/branches/main/) can be performed using the [FHIR validator](https://wiki.hl7.org/Using_the_FHIR_Validator) tool.
|
||||
## FHIR Validation
|
||||
|
||||
Validation of the FHIR bundle is currently not comprehensive. The tool validates a subset of the full FHIR schema; the behavior can be scoped by using the profile option, or changed by modifying the `src/prune-fhir-schema.ts` script. Extensive tests and conformance to the [Vaccination & Testing Implementation Guide](http://build.fhir.org/ig/dvci/vaccine-credential-ig/branches/main/) can be performed using the [FHIR validator](https://wiki.hl7.org/Using_the_FHIR_Validator) tool.
|
||||
|
||||
This tool can now apply the HL7 FHIR Validator, in place of the limited default validator, with the use of the `--validator fhirvalidator` option. The HL7 FHIR Validator is a Java application and so requires a Java runtime (JRE), or alternatively, Docker to be installed on your system.
|
||||
This tool will attempt to run it with an installed JRE first, if available. If not, it will attempt to instantiate a Docker image (with a JRE). If neither method is succeeds an error will be returned.
|
||||
|
||||
__Note__: Docker may require elevated permissions to execute docker commands, requiring this tool to also run with elevated permissions when attempting to use a Docker image. For example:
|
||||
```
|
||||
# Run shc-validator as sudo ('-E env "PATH=$PATH"' preserves the environment of the current user)
|
||||
sudo -E env "PATH=$PATH" shc-validator --path myfhirbundle.json --type fhirbundle --validator fhirvalidator
|
||||
```
|
|
@ -0,0 +1,9 @@
|
|||
FROM fabric8/java-alpine-openjdk11-jre
|
||||
|
||||
RUN java -version
|
||||
|
||||
RUN curl -L -o validator_cli.jar https://github.com/hapifhir/org.hl7.fhir.core/releases/latest/download/validator_cli.jar
|
||||
|
||||
# Run the validator without anything to validate - to cache the fhir downloads in the image.
|
||||
# "|| :" forces a '0' exit code as the validator will return an error code when doing no actual validation
|
||||
RUN java -jar validator_cli.jar -ig hl7.fhir.r5.core#current || :
|
|
@ -0,0 +1,69 @@
|
|||
import execa from 'execa';
|
||||
import color from 'colors';
|
||||
import Log from './logger';
|
||||
|
||||
function workingAnimation(message: string, interval = 200) {
|
||||
const chars = ['|', '/', '―', '\\'];
|
||||
let x = 0;
|
||||
|
||||
// For the animation, we write to stdout a message, then use process.stdout.clearLine() to erase the line
|
||||
// and on the same line write an updated message. This keeps the message updating on the same console line.
|
||||
// When we run the entire test suite using 'npm run test', process.stdout.clearline() is not available.
|
||||
// We don't really need to print the animation in this case anyway, so we skip the animation when
|
||||
// process.stdout.clearline() is not available.
|
||||
if (!process.stdout.clearLine) return { stop: () => {/*noop*/ } };
|
||||
|
||||
const handle = setInterval(() => {
|
||||
process.stdout.write(`\r ${color.green(chars[x++])} ${message}`);
|
||||
x %= chars.length;
|
||||
}, interval);
|
||||
|
||||
return {
|
||||
stop: () => {
|
||||
clearInterval(handle);
|
||||
process.stdout.clearLine(0);
|
||||
process.stdout.write('\n');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function runCommandSync(command: string, log?: Log): CommandResult {
|
||||
let result;
|
||||
const start = Date.now();
|
||||
|
||||
try {
|
||||
result = execa.commandSync(command) as CommandResult;
|
||||
} catch (failed) {
|
||||
result = failed as CommandResult;
|
||||
}
|
||||
|
||||
log?.debug(resultToString(result, start));
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function runCommand(command: string, message?: string, log?: Log): Promise<CommandResult> {
|
||||
|
||||
let result;
|
||||
const start = Date.now();
|
||||
|
||||
const animation = workingAnimation(message || command);
|
||||
|
||||
try {
|
||||
result = await execa.command(command) as CommandResult;
|
||||
} catch (failed) {
|
||||
result = failed as CommandResult;
|
||||
}
|
||||
|
||||
animation.stop();
|
||||
log?.debug(resultToString(result, start));
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
function resultToString(result: CommandResult, start: number): string {
|
||||
return `Running command : ${result.command}\n \
|
||||
duration: ${((Date.now() - start) / 1000).toFixed(2)} seconds\n \
|
||||
exitcode : ${result.exitCode}\n \
|
||||
stdout: ${result.stdout.split('\n').join("\n ")}\n \
|
||||
stderr: ${result.stderr.split('\n').join("\n ")}`;
|
||||
}
|
|
@ -50,7 +50,14 @@ export enum ErrorCode {
|
|||
INVALID_KEY_UNKNOWN,
|
||||
|
||||
// config errors
|
||||
OPENSSL_NOT_AVAILABLE = 300
|
||||
OPENSSL_NOT_AVAILABLE = 300,
|
||||
|
||||
// FHIR validator errors
|
||||
FHIR_VALIDATOR_ERROR = 400,
|
||||
JRE_OR_DOCKER_NOT_AVAILABLE,
|
||||
DOCKER_ERROR,
|
||||
DOCKER_PERMISSIONS,
|
||||
DOCKER_DAEMON_NOT_RUNNING
|
||||
}
|
||||
|
||||
class ExcludableError {
|
||||
|
|
|
@ -11,6 +11,7 @@ import patientDM from '../schema/patient-dm.json';
|
|||
import Log from './logger';
|
||||
import beautify from 'json-beautify'
|
||||
import { propPath, walkProperties } from './utils';
|
||||
import { validate as fhirValidator } from './fhirValidator';
|
||||
|
||||
// The CDC CVX covid vaccine codes (https://www.cdc.gov/vaccines/programs/iis/COVID-19-related-codes.html),
|
||||
export const cdcCovidCvxCodes = ["207", "208", "210", "212", "217", "218", "219", "500", "501", "502", "503", "504", "505", "506", "507", "508", "509", "510", "511"];
|
||||
|
@ -26,9 +27,14 @@ export enum ValidationProfiles {
|
|||
'usa-covid19-immunization'
|
||||
}
|
||||
|
||||
export enum Validators {
|
||||
'default',
|
||||
'fhirvalidator'
|
||||
}
|
||||
export class FhirOptions {
|
||||
static LogOutputPath = '';
|
||||
static ValidationProfile: ValidationProfiles = ValidationProfiles.any;
|
||||
static ValidationProfile = ValidationProfiles.any;
|
||||
static Validator = Validators.default;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
|
@ -36,6 +42,7 @@ export async function validate(fhirBundleText: string): Promise<Log> {
|
|||
|
||||
const log = new Log('FhirBundle');
|
||||
const profile: ValidationProfiles = FhirOptions.ValidationProfile;
|
||||
const validator: Validators = FhirOptions.Validator;
|
||||
|
||||
if (fhirBundleText.trim() !== fhirBundleText) {
|
||||
log.error(`FHIR bundle has leading or trailing spaces`, ErrorCode.TRAILING_CHARACTERS);
|
||||
|
@ -54,6 +61,26 @@ export async function validate(fhirBundleText: string): Promise<Log> {
|
|||
// failures will be recorded in the log
|
||||
if (!validateSchema(fhirSchema, fhirBundle, log)) return log;
|
||||
|
||||
// use HL7 FHIR validator, if specified
|
||||
if (validator === Validators['fhirvalidator']) {
|
||||
|
||||
log.info(`Applying validator : fhirvalidator`);
|
||||
|
||||
void await fhirValidator(fhirBundleText, log);
|
||||
|
||||
log.hasErrors || log.info("FHIR bundle validated");
|
||||
log.debug("FHIR bundle contents:");
|
||||
log.debug(beautify(fhirBundle, null as unknown as Array<string>, 3, 100));
|
||||
|
||||
return log;
|
||||
}
|
||||
|
||||
log.note(`This tool's default FHIR validation is not as complete as the dedicated HL7 FHIR Validator at ${'http://hl7.org/fhir/validator/'} (hl7.org). \
|
||||
If you have Docker or the Java JRE installed, this tool supports validating against the FHIR validator by using the '--validator fhirvalidator' on the command line. \
|
||||
See README.md for more information.`);
|
||||
|
||||
|
||||
// Begin 'default
|
||||
|
||||
// to continue validation, we must have a list of resources in .entry[]
|
||||
if (!fhirBundle.entry ||
|
||||
|
@ -207,8 +234,8 @@ export async function validate(fhirBundleText: string): Promise<Log> {
|
|||
ValidationProfilesFunctions['usa-covid19-immunization'](fhirBundle.entry, log);
|
||||
}
|
||||
|
||||
log.info("FHIR bundle validated");
|
||||
log.debug("FHIR Bundle Contents:");
|
||||
log.hasErrors || log.info("FHIR bundle validated");
|
||||
log.debug("FHIR bundle contents:");
|
||||
log.debug(beautify(fhirBundle, null as unknown as Array<string>, 3, 100));
|
||||
|
||||
return log;
|
||||
|
@ -292,4 +319,3 @@ const ValidationProfilesFunctions = {
|
|||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,228 @@
|
|||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import Log from '../src/logger';
|
||||
import { ErrorCode } from './error';
|
||||
import color from 'colors';
|
||||
import got from 'got';
|
||||
import { runCommand, runCommandSync } from '../src/command';
|
||||
|
||||
const imageName = 'fhir.validator.image';
|
||||
const dockerFile = 'fhir.validator.Dockerfile';
|
||||
const dockerContainer = 'fhir.validator.container';
|
||||
const validatorJarFile = 'validator_cli.jar';
|
||||
const validatorUrl = 'https://github.com/hapifhir/org.hl7.fhir.core/releases/latest/download/validator_cli.jar';
|
||||
const tempFileName = '~temp.fhirbundle.json';
|
||||
|
||||
|
||||
// share a log between the functions. This can be passed in externally through the validate() function
|
||||
let log: Log;
|
||||
|
||||
|
||||
async function downloadFHIRValidator(): Promise<void> {
|
||||
try {
|
||||
fs.writeFileSync(validatorJarFile, (await got(validatorUrl, { followRedirect: true }).buffer()));
|
||||
} catch (err) {
|
||||
log.debug(`File download error ${(err as Error).toString()}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Runs the FHIR validator using the installed JRE
|
||||
async function runValidatorJRE(artifactPath: string): Promise<CommandResult | null> {
|
||||
|
||||
if (!fs.existsSync(validatorJarFile)) await downloadFHIRValidator();
|
||||
|
||||
if (!fs.existsSync(validatorJarFile)) {
|
||||
log.error(`Failed to download FHIR Validator Jar file ${validatorJarFile} from ${validatorUrl}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
log.info(`Validating ${artifactPath} with FHIR validator.`);
|
||||
|
||||
const result: CommandResult = await runCommand(`java -jar ${validatorJarFile} ${artifactPath}`, `Running HL7 FHIR validator with JRE`, log);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
// Runs the FHIR validator in a Docker container
|
||||
async function runValidatorDocker(artifactPath: string): Promise<CommandResult | null> {
|
||||
|
||||
if (!await Docker.checkPermissions()) return null;
|
||||
|
||||
if (!await Docker.imageExists(imageName)) {
|
||||
log.debug(`Image ${imageName} not found. Attempting to build.`);
|
||||
if (!await Docker.buildImage(dockerFile, imageName)) {
|
||||
log.error('Could not build Docker image.');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const dockerCommand = `java -jar validator_cli.jar data/${artifactPath}`;
|
||||
|
||||
log.info(`Validating ${path.resolve(artifactPath)} with FHIR validator.`);
|
||||
|
||||
// create a new container from image, copies the
|
||||
const command = `docker run --rm --name ${dockerContainer} -v ${path.resolve(path.dirname(artifactPath))}:/data ${imageName} ${dockerCommand}`;
|
||||
|
||||
const result = await runCommand(command, `Running HL7 FHIR validator with Docker (${imageName})`, log);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
export async function validate(fileOrJSON: string, logger = new Log('FHIR Validator')): Promise<Log> {
|
||||
|
||||
log = logger;
|
||||
|
||||
const usingJre = JRE.isAvailable();
|
||||
const usingDocker = !usingJre && Docker.isAvailable();
|
||||
|
||||
if (!usingJre && !usingDocker) {
|
||||
return log.error(
|
||||
`Validator: use of option ${color.italic('--validator fhirvalidator')} requires Docker or JRE to execute the FHIR Validator Java application. See: http://hl7.org/fhir/validator/`,
|
||||
ErrorCode.JRE_OR_DOCKER_NOT_AVAILABLE
|
||||
);
|
||||
}
|
||||
|
||||
if (JSON.parse(fileOrJSON)) {
|
||||
fs.writeFileSync(tempFileName, fileOrJSON); // overwrites by default
|
||||
fileOrJSON = tempFileName;
|
||||
}
|
||||
|
||||
const artifact = path.resolve(fileOrJSON);
|
||||
|
||||
if (!fs.existsSync(artifact)) {
|
||||
return log.error(`Artifact ${artifact} not found.`);
|
||||
}
|
||||
|
||||
const fileName = path.basename(artifact);
|
||||
|
||||
const result: CommandResult | null = await (usingJre ? runValidatorJRE(fileName) : runValidatorDocker(fileName));
|
||||
|
||||
if (fs.existsSync(tempFileName)) {
|
||||
fs.rmSync(tempFileName);
|
||||
}
|
||||
|
||||
// null returned if validator failed before validation actually checked
|
||||
if (result === null) return log;
|
||||
|
||||
// if everything is ok, return
|
||||
if (result && /Information: All OK/.test(result?.stdout)) return log;
|
||||
|
||||
const errors = result?.stdout.match(/(?<=\n\s*Error @ ).+/g) || [];
|
||||
errors.forEach(err => {
|
||||
const formattedError = err; // splitLines(err);
|
||||
log.error(formattedError, ErrorCode.FHIR_VALIDATOR_ERROR);
|
||||
});
|
||||
|
||||
const warnings = result?.stdout.match(/(?<=\n\s*Warning @ ).+/g) || [];
|
||||
warnings.forEach(warn => {
|
||||
const formattedError = warn; // splitLines(warn);
|
||||
log.warn(formattedError, ErrorCode.FHIR_VALIDATOR_ERROR);
|
||||
});
|
||||
|
||||
// if there are no errors or warnings but the validation is not 'All OK'
|
||||
// something is wrong.
|
||||
if (!errors && !warnings) {
|
||||
log.error(`${fileName} : failed to find Errors or 'All OK'`);
|
||||
}
|
||||
|
||||
return log;
|
||||
}
|
||||
|
||||
|
||||
const JRE = {
|
||||
|
||||
isAvailable: (): boolean => {
|
||||
const result = runCommandSync(`java --version`, log);
|
||||
if (result.exitCode === 0) {
|
||||
const version = /^java \d+.+/.exec(result.stdout)?.[0] ?? 'unknown';
|
||||
log?.debug(`Java detected : ${version}`);
|
||||
}
|
||||
|
||||
return result.exitCode === 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
const Docker = {
|
||||
|
||||
isAvailable: (): boolean => {
|
||||
const result = runCommandSync(`docker --version`, log);
|
||||
if (result.exitCode === 0) {
|
||||
const version = /^Docker version \d+.+/.exec(result.stdout)?.[0] ?? 'unknown';
|
||||
log?.debug(`Docker detected : ${version}`);
|
||||
}
|
||||
return result.exitCode === 0;
|
||||
},
|
||||
|
||||
imageExists: async (imageName: string): Promise<boolean> => {
|
||||
return (await runCommand(`docker image inspect ${imageName}`, `Check Docker image ${imageName} exists`, log)).exitCode === 0;
|
||||
},
|
||||
|
||||
containerExists: async (name: string): Promise<boolean> => {
|
||||
const stdout = (await runCommand(`docker ps -a --format '{{.Names}}'`, undefined, log)).stdout;
|
||||
const names: string[] = stdout.replace(/'/g, '').split('\n');
|
||||
return names.includes(name);
|
||||
},
|
||||
|
||||
checkPermissions: async (): Promise<boolean> => {
|
||||
const result = await runCommand(`docker image ls`, undefined, log);
|
||||
if (result.exitCode !== 0) {
|
||||
if (/permission denied/.test(result.stderr)) {
|
||||
log.error(
|
||||
`Selecting the '--validator fhirvalidator' option is attempting to run the HL7 FHIR Validator using a Docker image. However, Docker on this system requires elevated permissions to use. Run this tool as an elevated user or add yourself to the 'docker' group. See README.md for additional information.`,
|
||||
ErrorCode.DOCKER_PERMISSIONS
|
||||
);
|
||||
} else if (/docker daemon is not running/.test(result.stderr)) {
|
||||
log.error(
|
||||
`Selecting the '--validator fhirvalidator' option is attempting to run the HL7 FHIR Validator using a Docker image. However, Docker may not be running. See README.md for additional information.`,
|
||||
ErrorCode.DOCKER_DAEMON_NOT_RUNNING
|
||||
);
|
||||
} else {
|
||||
log.error(
|
||||
`Docker command failed ${result.stderr}`,
|
||||
ErrorCode.DOCKER_ERROR
|
||||
);
|
||||
}
|
||||
}
|
||||
return result.exitCode === 0;
|
||||
},
|
||||
|
||||
cleanupImage: async (imageName: string): Promise<void> => {
|
||||
log && log.debug(`Remove Docker image ${imageName}`);
|
||||
await runCommand(`docker image rm -f ${imageName}`, `Remove Docker image ${imageName}`, log);
|
||||
},
|
||||
|
||||
buildImage: async (dockerFile: string, imageName: string): Promise<boolean> => {
|
||||
|
||||
if (!fs.existsSync(dockerFile)) {
|
||||
log.error(`Cannot find Dockerfile ${dockerFile}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
log.debug(`Building Docker image ${imageName} from ${dockerFile}`);
|
||||
|
||||
const result = await runCommand(`docker build -t ${imageName} -f ${dockerFile} .`, `Build Docker image ${imageName} from ${dockerFile}`, log);
|
||||
|
||||
if (result.exitCode === 0 && await Docker.imageExists(imageName)) {
|
||||
log.debug(`Docker image ${imageName} created.`);
|
||||
} else {
|
||||
log.debug(`Failed to build image ${imageName}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// docker returns build steps on stderr
|
||||
log.debug(result.stdout || result.stderr);
|
||||
|
||||
return result.exitCode === 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
export function jreOrDockerAvailable(): boolean {
|
||||
return JRE.isAvailable() || Docker.isAvailable();
|
||||
}
|
|
@ -15,6 +15,9 @@ export class LogItem {
|
|||
|
||||
|
||||
export enum LogLevels {
|
||||
|
||||
// Always output Note
|
||||
NOTE = -1,
|
||||
// Print out everything
|
||||
DEBUG = 0,
|
||||
// Print out informational messages
|
||||
|
@ -24,9 +27,7 @@ export enum LogLevels {
|
|||
// Only print out errors
|
||||
ERROR,
|
||||
// Only print out fatal errors, where processing can't continue
|
||||
FATAL,
|
||||
// Always output Note
|
||||
NOTE
|
||||
FATAL
|
||||
}
|
||||
|
||||
|
||||
|
@ -103,6 +104,10 @@ export default class Log {
|
|||
});
|
||||
}
|
||||
|
||||
public get hasErrors(): boolean {
|
||||
return !!(this.get(LogLevels.FATAL).length + this.get(LogLevels.ERROR).length);
|
||||
}
|
||||
|
||||
// collects errors from all children into a single collection; specify level to filter >= level
|
||||
flatten(level: LogLevels = LogLevels.DEBUG): { title: string, message: string, code: ErrorCode, level: LogLevels }[] {
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ import { ErrorCode, ExcludableErrors, getExcludeErrorCodes } from './error';
|
|||
import * as utils from './utils'
|
||||
import npmpackage from '../package.json';
|
||||
import { KeySet } from './keys';
|
||||
import { FhirOptions, ValidationProfiles } from './fhirBundle';
|
||||
import { FhirOptions, ValidationProfiles, Validators } from './fhirBundle';
|
||||
import * as versions from './check-for-update';
|
||||
import semver from 'semver';
|
||||
import { JwsValidationOptions } from './jws-compact';
|
||||
|
@ -42,6 +42,7 @@ program.option('-k, --jwkset <key>', 'path to trusted issuer key set');
|
|||
program.option('-e, --exclude <error>', 'error to exclude, can be repeated, can use a * wildcard. Valid options:' +
|
||||
ExcludableErrors.map(e => ` "${e.error}"`).join(),
|
||||
(e: string, errors: string[]) => errors.concat([e]), []);
|
||||
program.addOption(new Option('-V, --validator <validator>', 'the choice of FHIR validator to use').choices(Object.keys(Validators).filter(x => Number.isNaN(Number(x)))).default('default'));
|
||||
program.parse(process.argv);
|
||||
|
||||
export interface CliOptions {
|
||||
|
@ -54,7 +55,8 @@ export interface CliOptions {
|
|||
logout: string;
|
||||
fhirout: string;
|
||||
exclude: string[];
|
||||
clearKeyStore? : boolean;
|
||||
clearKeyStore?: boolean;
|
||||
validator: string;
|
||||
}
|
||||
|
||||
|
||||
|
@ -116,6 +118,14 @@ async function processOptions(options: CliOptions) {
|
|||
ValidationProfiles[options.profile as keyof typeof ValidationProfiles] :
|
||||
FhirOptions.ValidationProfile = ValidationProfiles['any'];
|
||||
|
||||
|
||||
// set the FHIR validator
|
||||
FhirOptions.Validator =
|
||||
options.validator ?
|
||||
Validators[options.validator as keyof typeof Validators] :
|
||||
FhirOptions.Validator = Validators['default'];
|
||||
|
||||
|
||||
|
||||
// requires both --path and --type properties
|
||||
if (options.path.length === 0 || !options.type) {
|
||||
|
|
|
@ -7,13 +7,13 @@ import { ErrorCode } from './error';
|
|||
import { validateSchema } from './schema';
|
||||
import keySetSchema from '../schema/keyset-schema.json';
|
||||
import keys, { KeySet } from './keys';
|
||||
import execa from 'execa';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { isOpensslAvailable, parseJson } from './utils'
|
||||
import { isOpensslAvailable } from './utils'
|
||||
import { Certificate } from '@fidm/x509'
|
||||
import { downloadAndValidateCRL } from './crl-validator';
|
||||
import { runCommandSync } from './command';
|
||||
|
||||
// directory where to write cert files for openssl validation
|
||||
const tmpDir = 'tmp';
|
||||
|
@ -98,7 +98,7 @@ function validateX5c(x5c: string[], log: Log): CertFields | undefined {
|
|||
//
|
||||
const opensslVerifyCommand = "openssl verify " + rootCaArg + caArg + issuerCert;
|
||||
log.debug('Calling openssl for x5c validation: ' + opensslVerifyCommand);
|
||||
const result = execa.commandSync(opensslVerifyCommand);
|
||||
const result = runCommandSync(opensslVerifyCommand, log);
|
||||
if (result.exitCode != 0) {
|
||||
log.debug(result.stderr);
|
||||
throw 'OpenSSL returned an error: exit code ' + result.exitCode.toString();
|
||||
|
@ -219,7 +219,7 @@ export async function verifyAndImportHealthCardIssuerKey(keySet: KeySet, log = n
|
|||
}
|
||||
|
||||
// check for revocation file, we do this before parsing the key because the code below recasts the key to a
|
||||
// JWK object overwritting the crlVersion field
|
||||
// JWK object overwriting the crlVersion field
|
||||
const keyWithCrl: JWK.Key & { crlVersion?: number } = key;
|
||||
if (keyWithCrl?.crlVersion !== undefined) {
|
||||
const crlVersion = keyWithCrl.crlVersion;
|
||||
|
|
|
@ -81,4 +81,11 @@ interface SchemaProperty {
|
|||
additionalProperties?: boolean,
|
||||
enum?: string[],
|
||||
const?: string
|
||||
}
|
||||
}
|
||||
|
||||
interface CommandResult {
|
||||
command: string;
|
||||
exitCode: number;
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ import fs from 'fs';
|
|||
import path from 'path';
|
||||
import pako from 'pako';
|
||||
import jose from 'node-jose';
|
||||
import execa from 'execa';
|
||||
import { runCommandSync } from './command';
|
||||
|
||||
export function parseJson<T>(json: string): T | undefined {
|
||||
try {
|
||||
|
@ -59,7 +59,7 @@ export function inflatePayload(verificationResult: jose.JWS.VerificationResult):
|
|||
|
||||
export function isOpensslAvailable(): boolean {
|
||||
try {
|
||||
const result = execa.commandSync("openssl version");
|
||||
const result = runCommandSync("openssl version");
|
||||
return (result.exitCode == 0);
|
||||
} catch (err) {
|
||||
return false;
|
||||
|
|
|
@ -14,7 +14,7 @@ import * as qr from './qr';
|
|||
import * as image from './image';
|
||||
import keys, { KeySet } from './keys';
|
||||
import * as utils from './utils';
|
||||
import { FhirOptions, ValidationProfiles } from './fhirBundle';
|
||||
import { FhirOptions, ValidationProfiles, Validators } from './fhirBundle';
|
||||
import { CliOptions } from './shc-validator';
|
||||
import { clearTrustedIssuerDirectory, setTrustedIssuerDirectory } from './issuerDirectory';
|
||||
|
||||
|
@ -44,6 +44,13 @@ async function processOptions(options: CliOptions) {
|
|||
clearTrustedIssuerDirectory();
|
||||
}
|
||||
|
||||
if (options.validator) {
|
||||
if(!Object.values(Validators).includes(options.validator)) {
|
||||
throw new Error(`Invalid validator value ${options.validator}`);
|
||||
}
|
||||
FhirOptions.Validator = Validators[options.validator as keyof typeof Validators];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ import { ErrorCode as ec } from '../src/error';
|
|||
import Log, { LogLevels } from '../src/logger';
|
||||
import { isOpensslAvailable } from '../src/utils';
|
||||
import { CliOptions } from '../src/shc-validator';
|
||||
|
||||
import { jreOrDockerAvailable } from '../src/fhirValidator';
|
||||
const testdataDir = './testdata/';
|
||||
|
||||
|
||||
|
@ -269,7 +269,7 @@ test("Cards: nbf in milliseconds",
|
|||
);
|
||||
|
||||
// one error for exp < nbf, one warning for card being expired
|
||||
test("Cards: exp date before nbf", testCard('test-example-00-b-jws-payload-expanded-pre-expired.json', 'jwspayload', [[ec.EXPIRATION_ERROR],[ec.EXPIRATION_ERROR]]));
|
||||
test("Cards: exp date before nbf", testCard('test-example-00-b-jws-payload-expanded-pre-expired.json', 'jwspayload', [[ec.EXPIRATION_ERROR], [ec.EXPIRATION_ERROR]]));
|
||||
|
||||
// the JWK's x5c value has the correct URL, so we get an extra x5c error due to URL mismatch
|
||||
test("Cards: invalid issuer url (http)",
|
||||
|
@ -387,4 +387,21 @@ test("Cards: unknown VC types", testCard('test-example-00-b-jws-payload-expanded
|
|||
|
||||
test("Cards: mismatch kid/issuer", testCard(['test-example-00-d-jws-issuer-kid-mismatch.txt'], "jws", [[ec.ISSUER_KID_MISMATCH]], { jwkset: 'testdata/issuer.jwks.public.not.smart.json' }));
|
||||
|
||||
test("Cards: immunization status not 'completed'", testCard('test-example-00-a-fhirBundle-status-not-completed.json', 'fhirbundle', [[ec.FHIR_SCHEMA_ERROR, ec.FHIR_SCHEMA_ERROR]]));
|
||||
test("Cards: immunization status not 'completed'", testCard('test-example-00-a-fhirBundle-status-not-completed.json', 'fhirbundle', [[ec.FHIR_SCHEMA_ERROR, ec.FHIR_SCHEMA_ERROR]]));
|
||||
|
||||
|
||||
// Tests using the HL7 FHIR Validator
|
||||
// Since these tests require a Java runtime (JRE) or Docker to be installed, they are conditionally executed.
|
||||
// These tests can also take a longer as they have to spin up a Docker image
|
||||
describe('FHIR validator tests', () => {
|
||||
|
||||
const testif = (condition: boolean) => condition ? it : it.skip;
|
||||
const canRunFhirValidator = jreOrDockerAvailable();
|
||||
|
||||
// shc-validator -p ./testdata/test-example-00-a-fhirBundle-profile-usa.json -t fhirbundle -l debug -V fhirvalidator
|
||||
testif(canRunFhirValidator)("Cards: fhir validator test", testCard(['test-example-00-a-fhirBundle-profile-usa.json'], 'fhirbundle',
|
||||
[8, 1], { validator: 'fhirvalidator' }), 1000 * 60 * 5 /*5 minutes*/);
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
import execa from 'execa';
|
||||
import fs from 'fs';
|
||||
import { runCommandSync } from '../src/command';
|
||||
import { ErrorCode as ec } from '../src/error';
|
||||
import { LogItem } from '../src/logger';
|
||||
import { CliOptions } from '../src/shc-validator';
|
||||
|
@ -16,17 +16,6 @@ interface LogEntry {
|
|||
log: LogItem[]
|
||||
}
|
||||
|
||||
function runCommand(command: string) {
|
||||
try {
|
||||
return execa.commandSync(command);
|
||||
|
||||
} catch (error) {
|
||||
// if exitCode !== 0 this error will be thrown
|
||||
// this error object is similar to the result object that would be returned if successful.
|
||||
// we'll return it an sort out the errors there.
|
||||
return error as execa.ExecaSyncError;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Puts the standard output into an array of line,
|
||||
|
@ -81,7 +70,7 @@ function testLogFile(logPath: string, deleteLog = true): LogEntry[] {
|
|||
}
|
||||
|
||||
function testCliCommand(command: string): number {
|
||||
const commandResult = runCommand(command);
|
||||
const commandResult = runCommandSync(command);
|
||||
const out = parseStdout(commandResult.stdout);
|
||||
console.log(out.join('\n'));
|
||||
return commandResult.exitCode;
|
||||
|
@ -133,7 +122,7 @@ test("Logs: valid 00-e health card single log file", () => {
|
|||
const expectedEntries = 1;
|
||||
const expectedLogItems = 8 + (OPENSSL_AVAILABLE ? 0 : 1);
|
||||
|
||||
runCommand('node . --path testdata/example-00-e-file.smart-health-card --type healthcard --loglevel info --logout ' + logFile);
|
||||
runCommandSync(`node . --path testdata/example-00-e-file.smart-health-card --type healthcard --loglevel info --logout ${logFile}`);
|
||||
|
||||
const logs: LogEntry[] = testLogFile(logFile);
|
||||
|
||||
|
@ -148,8 +137,8 @@ test("Logs: valid 00-e health card append log file", () => {
|
|||
const expectedEntries = 2;
|
||||
const expectedLogItems = [8 + (OPENSSL_AVAILABLE ? 0 : 1), 8 + (OPENSSL_AVAILABLE ? 0 : 1)];
|
||||
|
||||
runCommand('node . --path testdata/example-00-e-file.smart-health-card --type healthcard --loglevel info --logout ' + logFile);
|
||||
runCommand('node . --path testdata/example-00-e-file.smart-health-card --type healthcard --loglevel info --logout ' + logFile);
|
||||
runCommandSync(`node . --path testdata/example-00-e-file.smart-health-card --type healthcard --loglevel info --logout ${logFile}`);
|
||||
runCommandSync(`node . --path testdata/example-00-e-file.smart-health-card --type healthcard --loglevel info --logout ${logFile}`);
|
||||
|
||||
const logs: LogEntry[] = testLogFile(logFile);
|
||||
|
||||
|
@ -161,20 +150,20 @@ test("Logs: valid 00-e health card append log file", () => {
|
|||
|
||||
test("Logs: valid 00-e health card bad log path", () => {
|
||||
const logFile = '../foo/log.txt';
|
||||
const commandResult = runCommand('node . --path testdata/example-00-e-file.smart-health-card --type healthcard --loglevel info --logout ' + logFile);
|
||||
const commandResult = runCommandSync(`node . --path testdata/example-00-e-file.smart-health-card --type healthcard --loglevel info --logout ${logFile}`);
|
||||
expect(commandResult.exitCode).toBe(ec.LOG_PATH_NOT_FOUND);
|
||||
});
|
||||
|
||||
test("Logs: valid 00-e health card fhir bundle log file", () => {
|
||||
const logFile = 'fhirout.json.log'; // .log to be gitignored
|
||||
const commandResult = runCommand('node . --path testdata/example-00-e-file.smart-health-card --type healthcard --loglevel info --fhirout ' + logFile);
|
||||
runCommandSync(`node . --path testdata/example-00-e-file.smart-health-card --type healthcard --loglevel info --logout ${logFile}`);
|
||||
// try parsing FHIR output log as a fhir bundle
|
||||
expect(testCliCommand(`node . --path ${logFile} --type fhirbundle`)).toBe(0);
|
||||
});
|
||||
|
||||
test("Logs: valid 00-e health card bad log path", () => {
|
||||
const logFile = '../foo/log.txt';
|
||||
const commandResult = runCommand('node . --path testdata/example-00-e-file.smart-health-card --type healthcard --loglevel info --fhirout ' + logFile);
|
||||
const commandResult = runCommandSync(`node . --path testdata/example-00-e-file.smart-health-card --type healthcard --loglevel info --logout ${logFile}`);
|
||||
expect(commandResult.exitCode).toBe(ec.LOG_PATH_NOT_FOUND);
|
||||
});
|
||||
|
||||
|
|
Загрузка…
Ссылка в новой задаче