High severityNVD Advisory· Published Sep 1, 2020· Updated Sep 11, 2023
fury-adapter-swagger allows arbitrary file read from system
CVE-2016-1000249
Description
fury-adapter-swagger from version 0.2.0 until version 0.9.7 has a weakness that allows an attacker to read arbitrary files off of the system. This can be used to read sensitive data, or to cause a denial of service condition by attempting to read something like /dev/zero.
Proof of
Concept:
---
swagger: '2.0'
info:
title: Read local files
version: '1.0'
paths:
/foo:
get:
responses:
200:
description: Some description
examples:
text/html:
example:
$ref: '/etc/passwd'
Recommendation
Upgrade to version 0.9.7 or later.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
fury-adapter-swaggernpm | >= 0.2.0, < 0.9.7 | 0.9.7 |
Patches
2777e2d68f035fix(dereferencing): Prevent dereferencing external assets (#89)
5 files changed · +128 −2
CHANGELOG.md+6 −0 modified@@ -1,3 +1,9 @@ +# 0.9.7 - 2016-10-25 + +## Bug Fixes + +- Prevents dereferencing external assets such as local files. + # 0.9.5 - 2016-09-01 ## Bug Fixes
package.json+1 −1 modified@@ -1,6 +1,6 @@ { "name": "fury-adapter-swagger", - "version": "0.9.6", + "version": "0.9.7", "description": "Swagger 2.0 parser for Fury.js", "main": "./lib/adapter.js", "tonicExampleFilename": "tonic-example.js",
src/parser.js+6 −1 modified@@ -90,7 +90,12 @@ export default class Parser { // Next, we dereference and validate the loaded Swagger object. Any schema // violations get converted into annotations with source maps. - swaggerParser.validate(loaded, (err) => { + const swaggerOptions = { + '$refs': { + external: false, + }, + }; + swaggerParser.validate(loaded, swaggerOptions, (err) => { const swagger = swaggerParser.api; this.swagger = swaggerParser.api;
test/fixtures/external-dereferencing.json+101 −0 added@@ -0,0 +1,101 @@ +{ + "element": "parseResult", + "meta": {}, + "attributes": {}, + "content": [ + { + "element": "category", + "meta": { + "classes": [ + "api" + ], + "title": "Dereferencing a local file" + }, + "attributes": {}, + "content": [ + { + "element": "resource", + "meta": {}, + "attributes": { + "href": "/" + }, + "content": [ + { + "element": "transition", + "meta": {}, + "attributes": {}, + "content": [ + { + "element": "httpTransaction", + "meta": {}, + "attributes": {}, + "content": [ + { + "element": "httpRequest", + "meta": {}, + "attributes": { + "method": "GET" + }, + "content": [] + }, + { + "element": "httpResponse", + "meta": {}, + "attributes": { + "headers": { + "element": "httpHeaders", + "meta": {}, + "attributes": {}, + "content": [ + { + "element": "member", + "meta": {}, + "attributes": {}, + "content": { + "key": { + "element": "string", + "meta": {}, + "attributes": {}, + "content": "Content-Type" + }, + "value": { + "element": "string", + "meta": {}, + "attributes": {}, + "content": "application/json" + } + } + } + ] + }, + "statusCode": "200" + }, + "content": [ + { + "element": "copy", + "meta": {}, + "attributes": {}, + "content": "dereference package.json" + }, + { + "element": "asset", + "meta": { + "classes": [ + "messageBody" + ] + }, + "attributes": {}, + "content": "{\n \"example\": {\n \"$ref\": \"package.json\"\n }\n}" + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] +} \ No newline at end of file
test/fixtures/external-dereferencing.yaml+14 −0 added@@ -0,0 +1,14 @@ +swagger: "2.0" +info: + title: Dereferencing a local file + version: v2 +paths: + /: + get: + responses: + 200: + description: dereference package.json + examples: + application/json: + example: + $ref: 'package.json'
f4407e3a5323Refactor code
8 files changed · +924 −798
Changelog.md+11 −1 modified@@ -1,6 +1,16 @@ +# Unreleased + +- Code refactoring + + - Parsing is now handled by a class to more easily share state between various methods. This lays the framework for more refactoring and code cleanup. + - The parser shared state tracks the path of the component currently being parsed, which makes it easier to split pieces of the parser into separate methods. + - YAML AST lookup paths are now arrays of strings rather than strings of period-separated components, which made escaping very difficult. + - Rather than guessing the YAML type, the AST lookup now checks and handles it accordingly. Paths like `'foo.bar.0.baz'` would fail previously and now work (but are passed as `['foo', 'bar', '0', 'baz']`). + - Underscore is now replaced with Lodash for consistency with other modules. + # 0.6.0 - 2015-12-14 -- Invalid YAML now returns error + - Invalid YAML now returns error # 0.5.3 - 2015-12-11
package.json+1 −1 modified@@ -19,8 +19,8 @@ "dependencies": { "babel-runtime": "^5.8.20", "js-yaml": "^3.4.2", + "lodash": "^3.10.1", "swagger-parser": "^3.3.0", - "underscore": "^1.8.3", "yaml-js": "^0.1.3" }, "devDependencies": {
src/adapter.js+5 −791 modified@@ -1,34 +1,5 @@ -import _ from 'underscore'; -import buildUriTemplate from './uri-template'; -import SwaggerParser from 'swagger-parser'; -import yaml from 'js-yaml'; -import yamlAst from 'yaml-js'; - -// These describe the type of annotations that are produced by this parser -// and assigns a unique code to each one. Downstream applications can use this -// code to group similar types of annotations together. -const ANNOTATIONS = { - CANNOT_PARSE: { - type: 'error', - code: 1, - fragment: 'yaml-parser', - }, - AST_UNAVAILABLE: { - type: 'warning', - code: 2, - fragment: 'yaml-parser', - }, - DATA_LOST: { - type: 'warning', - code: 3, - fragment: 'refract-not-supported', - }, - VALIDATION_ERROR: { - type: 'warning', - code: 4, - fragment: 'swagger-validation', - }, -}; +import _ from 'lodash'; +import Parser from './parser'; export const name = 'swagger'; @@ -44,769 +15,12 @@ export function detect(source) { : source.swagger === '2.0'); } -// Test whether a key is a special Swagger extension. -function isExtension(value, key) { - return key.indexOf('x-') === 0; -} - -// Test whether tags can be treated as resource groups, and if so it sets a -// group name for each resource (used later to create groups). -function useResourceGroups(api) { - const tags = []; - - if (api.paths) { - _.each(api.paths, (path) => { - let tag = null; - - if (path) { - _.each(path, (operation) => { - if (operation.tags && operation.tags.length) { - if (operation.tags.length > 1) { - // Too many tags... each resource can only be in one group! - return false; - } - - if (tag === null) { - tag = operation.tags[0]; - } else if (tag !== operation.tags[0]) { - // Non-matching tags... can't have a resource in multiple groups! - return false; - } - } - }); - } - - if (tag) { - path['x-group-name'] = tag; - tags.push(tag); - } - }); - } - - return tags.length > 0; -} - -// Look up a position in the original source based on a JSON path, for -// example 'paths./test.get.responses.200' -function getPosition(ast, path) { - const pieces = _.isArray(path) ? path.splice(0) : path.split('.'); - let end; - let node = ast; - let piece = pieces.shift(); - let start; - - while (piece) { - let newNode = null; - let index = null; - - // If a piece ends with an array index, then we need to make sure we fetch - // that specific item from the value array. - const match = piece.match(/(.*)\[([0-9])+\]$/); - if (match) { - piece = match[1]; - index = parseInt(match[2], 10); - } - - for (const subNode of node.value) { - if (subNode[0] && subNode[0].value === piece) { - if (pieces.length) { - newNode = subNode[1]; - if (index !== null) { - newNode = newNode.value[index]; - } - } else { - // This is the last item! - if (index !== null) { - newNode = subNode[1].value[index]; - start = newNode.start_mark.pointer; - end = newNode.end_mark.pointer; - } else { - newNode = subNode[0]; - start = subNode[0].start_mark.pointer; - end = subNode[1].end_mark.pointer; - } - } - break; - } else if (subNode[0] && subNode[0].value === '$ref') { - if (subNode[1].value.indexOf('#') === 0) { - // This is an internal reference! First, we reset the node to the - // root of the document, shift the ref item off the pieces stack - // and then add the referenced path to the pieces. - const refPaths = subNode[1].value.substr(2).split('/'); - newNode = ast; - Array.prototype.unshift.apply(pieces, refPaths.concat([piece])); - break; - } else { - console.log(`External reference ${subNode[1].value} not supported for source maps!`); - } - } - } - - if (newNode) { - node = newNode; - } else { - return null; - } - - piece = pieces.shift(); - } - - return {start, end}; -} - -// Make a new source map for the given element -function makeSourceMap(SourceMap, ast, element, path) { - const position = getPosition(ast, path); - if (position) { - element.attributes.set('sourceMap', [ - new SourceMap([[position.start, position.end - position.start]]), - ]); - } -} - -// Make a new annotation for the given path and message -function makeAnnotation(Annotation, Link, SourceMap, ast, result, info, path, message) { - const annotation = new Annotation(message); - annotation.classes.push(info.type); - annotation.code = info.code; - result.content.push(annotation); - - if (info.fragment) { - const link = new Link(); - link.relation = 'origin'; - link.href = `http://docs.apiary.io/validations/swagger#${info.fragment}`; - annotation.links.push(link); - } - - if (ast && path) { - const position = getPosition(ast, path); - if (position && !isNaN(position.start) && !isNaN(position.end)) { - annotation.attributes.set('sourceMap', [ - new SourceMap([[position.start, position.end - position.start]]), - ]); - } - } -} - -function convertParameterToElement(minim, generateSourceMap, setupSourceMap, parameter, path, setAttributes = false) { - const StringElement = minim.getElementClass('string'); - const NumberElement = minim.getElementClass('number'); - const BooleanElement = minim.getElementClass('boolean'); - const ArrayElement = minim.getElementClass('array'); - - let element; - - // Convert from Swagger types to Minim elements - if (parameter.type === 'string') { - element = new StringElement(''); - } else if (parameter.type === 'integer' || parameter.type === 'number') { - element = new NumberElement(); - } else if (parameter.type === 'boolean') { - element = new BooleanElement(); - } else if (parameter.type === 'array') { - element = new ArrayElement(); - - if (parameter.items) { - element.content = [convertParameterToElement(minim, generateSourceMap, - setupSourceMap, parameter.items, `${path}.items`, true)]; - } - } else { - // Default to a string in case we get a type we haven't seen - element = new StringElement(''); - } - - if (generateSourceMap) { - setupSourceMap(element, path); - } - - if (setAttributes) { - if (parameter.description) { - element.description = parameter.description; - - if (generateSourceMap) { - setupSourceMap(element.meta.get('description'), `${path}.description`); - } - } - - if (parameter.required) { - element.attributes.set('typeAttributes', ['required']); - } - - if (parameter.default !== undefined) { - element.attributes.set('default', parameter.default); - } - } - - return element; -} - -function convertParameterToMember(minim, generateSourceMap, setupSourceMap, parameter, path) { - const MemberElement = minim.getElementClass('member'); - const memberValue = convertParameterToElement(minim, generateSourceMap, - setupSourceMap, parameter, path); - - // TODO: Update when Minim has better support for elements as values - // should be: new MemberType(parameter.name, memberValue); - const member = new MemberElement(parameter.name); - member.content.value = memberValue; - - if (generateSourceMap) { - setupSourceMap(member, path); - } - - if (parameter.description) { - member.description = parameter.description; - - if (generateSourceMap) { - setupSourceMap(member.meta.get('description'), `${path}.description`); - } - } - - if (parameter.required) { - member.attributes.set('typeAttributes', ['required']); - } - - // If there is a default, it is set on the member value instead of the member - // element itself because the default value applies to the value. - if (parameter.default) { - memberValue.attributes.set('default', parameter.default); - } - - return member; -} - -function createAssetFromJsonSchema(minim, jsonSchema) { - const Asset = minim.getElementClass('asset'); - const schemaAsset = new Asset(JSON.stringify(jsonSchema)); - schemaAsset.classes.push('messageBodySchema'); - schemaAsset.attributes.set('contentType', 'application/schema+json'); - - return schemaAsset; -} - -function createTransaction(minim, transition, method) { - const HttpTransaction = minim.getElementClass('httpTransaction'); - const HttpRequest = minim.getElementClass('httpRequest'); - const HttpResponse = minim.getElementClass('httpResponse'); - const transaction = new HttpTransaction(); - transaction.content = [new HttpRequest(), new HttpResponse()]; - - if (transition) { - transition.content.push(transaction); - } - - if (method) { - transaction.request.attributes.set('method', method.toUpperCase()); - } - - return transaction; -} - /* * Parse Swagger 2.0 into Refract elements */ -export function parse({minim, source, generateSourceMap}, done) { - // TODO: Will refactor this once API Description namespace is stable - // Leaving as large block of code until then - const Annotation = minim.getElementClass('annotation'); - const Asset = minim.getElementClass('asset'); - const Copy = minim.getElementClass('copy'); - const Category = minim.getElementClass('category'); - const DataStructure = minim.getElementClass('dataStructure'); - const HrefVariables = minim.getElementClass('hrefVariables'); - const HttpHeaders = minim.getElementClass('httpHeaders'); - const Link = minim.getElementClass('link'); - const MemberElement = minim.getElementClass('member'); - const ObjectElement = minim.getElementClass('object'); - const ParseResult = minim.getElementClass('parseResult'); - const Resource = minim.getElementClass('resource'); - const SourceMap = minim.getElementClass('sourceMap'); - const Transition = minim.getElementClass('transition'); - - const parser = new SwaggerParser(); - const parseResult = new ParseResult(); - - let loaded; - try { - loaded = _.isString(source) ? yaml.safeLoad(source) : source; - } catch (err) { - makeAnnotation(Annotation, Link, SourceMap, null, parseResult, - ANNOTATIONS.CANNOT_PARSE, null, (err.reason || 'Problem loading the input')); - - if (err.mark) { - parseResult.first().attributes.set('sourceMap', [ - new SourceMap([[err.mark.position, 1]]), - ]); - } - - return done(new Error(err.message), parseResult); - } - - let ast = null; - if (_.isString(source)) { - // TODO: Could we lazy-load the AST here? Seems like a waste of time if - // we load it but don't wind up using it. - try { - ast = yamlAst.compose(source); - } catch (err) { - makeAnnotation(Annotation, Link, SourceMap, null, parseResult, - ANNOTATIONS.AST_UNAVAILABLE, null, - 'Input AST could not be composed, so source maps will not be available'); - } - } else { - makeAnnotation(Annotation, Link, SourceMap, null, parseResult, - ANNOTATIONS.AST_UNAVAILABLE, null, - 'Source maps are only available with string input'); - } - - // Some sane defaults since these are sometimes left out completely - if (loaded.info === undefined) { - loaded.info = {}; - } - - if (loaded.paths === undefined) { - loaded.paths = {}; - } - - // Parse and validate the Swagger document! - parser.validate(loaded, (err) => { - const swagger = parser.api; - - if (err) { - if (swagger === undefined) { - return done(err, parseResult); - } - - // Non-fatal errors, so let us try and create annotations for them and - // continue with the parsing as best we can. - if (err.details) { - const queue = [err.details]; - while (queue.length) { - for (const item of queue[0]) { - makeAnnotation(Annotation, Link, SourceMap, ast, parseResult, - ANNOTATIONS.VALIDATION_ERROR, item.path, item.message); - - if (item.inner) { - // TODO: I am honestly not sure what the correct behavior is - // here. Some items will have within them a tree of other items, - // some of which might contain more info (but it's unclear). - // Do we treat them as their own error or do something else? - queue.push(item.inner); - } - } - queue.shift(); - } - } - } - - const basePath = (swagger.basePath || '').replace(/[/]+$/, ''); - const setupSourceMap = makeSourceMap.bind(makeSourceMap, SourceMap, ast); - const setupAnnotation = makeAnnotation.bind(makeAnnotation, Annotation, - Link, SourceMap, ast, parseResult); - const paramToElement = convertParameterToMember.bind( - convertParameterToElement, minim, generateSourceMap, setupSourceMap); - - const api = new Category(); - parseResult.push(api); - - // Root API Element - api.classes.push('api'); - - if (swagger.info) { - if (swagger.info.title) { - api.meta.set('title', swagger.info.title); - - if (generateSourceMap && ast) { - setupSourceMap(api.meta.get('title'), 'info.title'); - } - } - - if (swagger.info.description) { - api.content.push(new Copy(swagger.info.description)); - - if (generateSourceMap && ast) { - setupSourceMap(api.content[api.content.length - 1], 'info.description'); - } - } - } - - if (swagger.host) { - let hostname = swagger.host; - - if (swagger.schemes) { - if (swagger.schemes.length > 1) { - setupAnnotation(ANNOTATIONS.DATA_LOST, 'schemes', - 'Only the first scheme will be used to create a hostname'); - } - - hostname = `${swagger.schemes[0]}://${hostname}`; - } - - api.attributes.set('meta', {}); - const meta = api.attributes.get('meta'); - const member = new MemberElement('HOST', hostname); - member.meta.set('classes', ['user']); - - if (generateSourceMap && ast) { - setupSourceMap(member, 'host'); - } - - meta.content.push(member); - } - - if (swagger.securityDefinitions) { - setupAnnotation(ANNOTATIONS.DATA_LOST, 'securityDefinitions', - 'Authentication information is not yet supported'); - } - - if (swagger.security) { - setupAnnotation(ANNOTATIONS.DATA_LOST, 'security', - 'Authentication information is not yet supported'); - } - - if (swagger.externalDocs) { - setupAnnotation(ANNOTATIONS.DATA_LOST, 'externalDocs', - 'External documentation is not yet supported'); - } - - const useGroups = useResourceGroups(swagger); - let group = api; - - // Swagger has a paths object to loop through - // The key is the href - _.each(_.omit(swagger.paths, isExtension), (pathValue, href) => { - const resource = new Resource(); - - if (generateSourceMap && ast) { - setupSourceMap(resource, `paths.${href}`); - } - - // Provide users with a way to add a title to a resource in Swagger - if (pathValue['x-summary']) { - resource.title = pathValue['x-summary']; - } - - // Provide users a way to add a description to a resource in Swagger - if (pathValue['x-description']) { - const resourceDescription = new Copy(pathValue['x-description']); - resource.push(resourceDescription); - } - - if (useGroups) { - const groupName = pathValue['x-group-name']; - - if (groupName) { - group = api.find((el) => el.element === 'category' && el.classes.contains('resourceGroup') && el.title === groupName).first(); - - if (!group) { - group = new Category(); - group.title = groupName; - group.classes.push('resourceGroup'); - - if (swagger.tags && swagger.tags.forEach) { - swagger.tags.forEach((tag) => { - // TODO: Check for external docs here? - if (tag.name === groupName && tag.description) { - group.content.push(new Copy(tag.description)); - } - }); - } - - api.content.push(group); - } - } - } - - group.content.push(resource); - - const pathObjectParameters = pathValue.parameters || []; - - // TODO: Currently this only supports URI parameters for `path` and `query`. - // It should add support for `body` parameters as well. - if (pathObjectParameters.length > 0) { - resource.hrefVariables = new HrefVariables(); - - pathObjectParameters.forEach((parameter, index) => { - if (parameter.in === 'query' || parameter.in === 'path') { - const paramPath = `paths.${href}.parameters[${index}]`; - const member = paramToElement(parameter, paramPath); - if (generateSourceMap && ast) { - setupSourceMap(member, paramPath); - } - resource.hrefVariables.content.push(member); - } else if (parameter.in === 'body') { - setupAnnotation(ANNOTATIONS.DATA_LOST, - `paths.${href}.parameters[${index}]`, - 'Path-level body parameters are not yet supported'); - } - }); - } - - // TODO: Handle parameters on a resource level - // See https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md#path-item-object - const relevantPaths = _.chain(pathValue) - .omit('parameters', '$ref') - .omit(isExtension) - .value(); - - // Each path is an object with methods as properties - _.each(relevantPaths, (methodValue, method) => { - const transition = new Transition(); - resource.content.push(transition); - - if (generateSourceMap && ast) { - setupSourceMap(transition, `paths.${href}.${method}`); - } - - if (methodValue.externalDocs) { - setupAnnotation(ANNOTATIONS.DATA_LOST, - `paths.${href}.${method}.externalDocs`, - 'External documentation is not yet supported'); - } - - const methodValueParameters = methodValue.parameters || []; - - const queryParameters = methodValueParameters.filter((parameter) => { - return parameter.in === 'query'; - }); - - // URI parameters are for query and path variables - const uriParameters = methodValueParameters.filter((parameter) => { - return parameter.in === 'query' || parameter.in === 'path'; - }); - - // Body parameters are ones that define JSON Schema - const bodyParameters = methodValueParameters.filter((parameter) => { - return parameter.in === 'body'; - }); - - // Form parameters are send as encoded form data in the body - const formParameters = methodValueParameters.filter((parameter) => { - return parameter.in === 'formData'; - }); - - const hrefForResource = buildUriTemplate(basePath, href, pathObjectParameters, queryParameters); - resource.attributes.set('href', hrefForResource); - - if (methodValue.summary) { - transition.meta.set('title', methodValue.summary); - - if (generateSourceMap && ast) { - const title = transition.meta.get('title'); - setupSourceMap(title, `paths.${href}.${method}.summary`); - } - } - - if (methodValue.description) { - const description = new Copy(methodValue.description); - transition.push(description); - - if (generateSourceMap && ast) { - setupSourceMap(description, `paths.${href}.${method}.description`); - } - } - - if (methodValue.operationId) { - transition.attributes.set('relation', methodValue.operationId); - } - - // For each uriParameter, create an hrefVariable - if (uriParameters.length > 0) { - transition.hrefVariables = new HrefVariables(); - - uriParameters.forEach((parameter) => { - let paramPath; - if (generateSourceMap && ast) { - const index = methodValueParameters.indexOf(parameter); - paramPath = `paths.${href}.${method}.parameters[${index}]`; - } - transition.hrefVariables.content.push( - paramToElement(parameter, paramPath)); - }); - } - - // Currently, default responses are not supported in API Description format - const relevantResponses = _.chain(methodValue.responses) - .omit('default') - .omit(isExtension) - .value(); - - if (methodValue.responses && methodValue.responses.default) { - setupAnnotation(ANNOTATIONS.DATA_LOST, - `paths.${href}.${method}.responses.default`, - 'Default response is not yet supported'); - } - - if (_.keys(relevantResponses).length === 0) { - if (bodyParameters.length) { - // Create an empty successful response so that the request/response - // pair gets properly generated. In the future we may want to - // refactor the code below as this is a little weird. - relevantResponses.null = {}; - } else { - createTransaction(minim, transition, method); - } - } - - // Transactions are created for each response in the document - _.each(relevantResponses, (responseValue, statusCode) => { - let examples = { - '': undefined, - }; - - if (responseValue.examples) { - examples = responseValue.examples; - } - - examples = _.omit(examples, 'schema'); - - _.each(examples, (responseBody, contentType) => { - const transaction = createTransaction(minim, transition, method); - const request = transaction.request; - const response = transaction.response; - - if (generateSourceMap && ast) { - setupSourceMap(transaction, - `paths.${href}.${method}.responses.${statusCode}`); - setupSourceMap(request, `paths.${href}.${method}`); - - if (statusCode) { - setupSourceMap(response, `paths.${href}.${method}.responses.${statusCode}`); - } - } - - if (responseValue.description) { - const description = new Copy(responseValue.description); - response.content.push(description); - if (generateSourceMap && ast) { - setupSourceMap(description, `paths.${href}.${method}.responses.${statusCode}.description`); - } - } - - const headers = new HttpHeaders(); - - if (contentType) { - headers.push(new MemberElement( - 'Content-Type', contentType - )); - - if (generateSourceMap && ast) { - setupSourceMap(headers.content[headers.content.length - 1], `paths.${href}.${method}.responses.${statusCode}.examples.${contentType}`); - } - - response.headers = headers; - } - - if (responseValue.headers) { - for (const headerName in responseValue.headers) { - if (responseValue.headers.hasOwnProperty(headerName)) { - const header = responseValue.headers[headerName]; - let value = ''; - - // Choose the first available option - if (header.enum) { - value = header.enum[0]; - } - - if (header.default) { - value = header.default; - } - - const member = new MemberElement(headerName, value); - - if (generateSourceMap && ast) { - setupSourceMap(member, `paths.${href}.${method}.responses.${statusCode}.headers.${headerName}`); - } - - if (header.description) { - member.meta.set('description', header.description); - - if (generateSourceMap && ast) { - setupSourceMap(member.meta.get('description'), `paths.${href}.${method}.responses.${statusCode}.headers.${headerName}.description`); - } - } - - headers.push(member); - } - } - - response.headers = headers; - } - - // Body parameters define request schemas - _.each(bodyParameters, (bodyParameter) => { - const schemaAsset = createAssetFromJsonSchema(minim, bodyParameter.schema); - request.content.push(schemaAsset); - }); - - // Using form parameters instead of body? We will convert those to - // data structures. - if (formParameters.length) { - const dataStructure = new DataStructure(); - // A form is essentially an object with key/value members - const dataObject = new ObjectElement(); - - _.each(formParameters, (param) => { - let paramPath; - if (generateSourceMap && ast) { - const index = methodValueParameters.indexOf(param); - paramPath = `paths.${href}.${method}.parameters[${index}]`; - } - dataObject.content.push(paramToElement(param, paramPath)); - }); - - dataStructure.content = dataObject; - request.content.push(dataStructure); - } - - // Responses can have bodies - if (responseBody !== undefined) { - let formattedResponseBody = responseBody; - - if (typeof(responseBody) !== 'string') { - formattedResponseBody = JSON.stringify(responseBody, null, 2); - } - - const bodyAsset = new Asset(formattedResponseBody); - bodyAsset.classes.push('messageBody'); - if (generateSourceMap && ast) { - setupSourceMap(bodyAsset, `paths.${href}.${method}.responses.${statusCode}.examples.${contentType}`); - } - response.content.push(bodyAsset); - } - - // Responses can have schemas in Swagger - const schema = responseValue.schema || (responseValue.examples && responseValue.examples.schema); - if (schema) { - const schemaAsset = createAssetFromJsonSchema(minim, schema); - if (generateSourceMap && ast) { - let schemaPath = `paths.${href}.${method}.responses.${statusCode}`; - if (responseValue.examples && responseValue.examples.schema) { - schemaPath += '.examples.schema'; - } else { - schemaPath += '.schema'; - } - setupSourceMap(schemaAsset, schemaPath); - } - response.content.push(schemaAsset); - } - - // TODO: Decide what to do with request hrefs - // If the URI is templated, we don't want to add it to the request - // if (uriParameters.length === 0) { - // request.attributes.href = href; - // } - - if (statusCode !== 'null') { - response.attributes.set('statusCode', statusCode); - } - }); - }); - }); - }); - - done(null, parseResult); - }); +export function parse(options, done) { + const parser = new Parser(options); + parser.parse(done); } export default {name, mediaTypes, detect, parse};
src/ast.js+81 −0 added@@ -0,0 +1,81 @@ +// A module for dealing with YAML syntax trees and looking up source map +// location information. + +import _ from 'lodash'; +import yamlAst from 'yaml-js'; + +export default class Ast { + constructor(source) { + this.root = yamlAst.compose(source); + } + + // Look up a position in the original source based on a JSON path, for + // example ['paths', '/test', 'get', 'responses', '200']. Also supported + // is using a string ('paths./test.get') but it does not understand any + // escaping. + getPosition(path) { + const pieces = _.isArray(path) ? [].concat(path) : path.split('.'); + let end; + let node = this.root; + let piece = pieces.shift(); + let start; + + if (!node) { + return null; + } + + while (piece !== undefined) { + let newNode = null; + + if (node.tag === 'tag:yaml.org,2002:map') { + // This is a may / object with key:value pairs. + for (const subNode of node.value) { + if (subNode[0] && subNode[0].value === piece) { + newNode = subNode[1]; + + if (!pieces.length) { + // This is the last item! + start = subNode[0].start_mark.pointer; + end = subNode[1].end_mark.pointer; + } + break; + } else if (subNode[0] && subNode[0].value === '$ref') { + if (subNode[1].value.indexOf('#') === 0) { + // This is an internal reference! First, we reset the node to the + // root of the document, shift the ref item off the pieces stack + // and then add the referenced path to the pieces. + const refPaths = subNode[1].value.substr(2).split('/'); + newNode = this.root; + Array.prototype.unshift.apply(pieces, refPaths.concat([piece])); + break; + } else { + // TODO: Communicate this in some other way? + console.log(`External reference ${subNode[1].value} not supported for source maps!`); + } + } + } + } else if (node.tag === 'tag:yaml.org,2002:seq') { + // This is a sequence, i.e. array. Access it by index. + newNode = node.value[piece]; + + if (!pieces.length) { + // This is the last item! + start = newNode.start_mark.pointer; + end = newNode.end_mark.pointer; + } + } else { + // Unknown piece, which will just return no source map. + } + + if (newNode) { + node = newNode; + } else { + return null; + } + + piece = pieces.shift(); + } + + return {start, end}; + } +}
src/parser.js+782 −0 added@@ -0,0 +1,782 @@ +// The main Swagger parsing component that outputs refract. + +import _ from 'lodash'; +import Ast from './ast'; +import buildUriTemplate from './uri-template'; +import SwaggerParser from 'swagger-parser'; +import yaml from 'js-yaml'; + +// These describe the type of annotations that are produced by this parser +// and assigns a unique code to each one. Downstream applications can use this +// code to group similar types of annotations together. +const ANNOTATIONS = { + CANNOT_PARSE: { + type: 'error', + code: 1, + fragment: 'yaml-parser', + }, + AST_UNAVAILABLE: { + type: 'warning', + code: 2, + fragment: 'yaml-parser', + }, + DATA_LOST: { + type: 'warning', + code: 3, + fragment: 'refract-not-supported', + }, + VALIDATION_ERROR: { + type: 'warning', + code: 4, + fragment: 'swagger-validation', + }, +}; + +// Test whether a key is a special Swagger extension. +function isExtension(value, key) { + return key.indexOf('x-') === 0; +} + +// The parser holds state about the current parsing environment and converts +// the input Swagger into Refract elements. The `parse` function is its main +// interface. +export default class Parser { + constructor({minim, source, generateSourceMap}) { + // Parser options + this.minim = minim; + this.source = source; + this.generateSourceMap = generateSourceMap; + + // Loaded, dereferenced Swagger API + this.swagger = null; + // Refract parse result + this.result = null; + // Refract API category + this.api = null; + // State of the current parsing path + this.path = []; + // Current resource group, if any + this.group = null; + } + + parse(done) { + const { + Copy, Category, Member: MemberElement, ParseResult, SourceMap, + } = this.minim.elements; + + const swaggerParser = new SwaggerParser(); + const parseResult = new ParseResult(); + this.result = parseResult; + + let loaded; + try { + loaded = _.isString(this.source) ? yaml.safeLoad(this.source) : this.source; + } catch (err) { + this.makeAnnotation(ANNOTATIONS.CANNOT_PARSE, null, + (err.reason || 'Problem loading the input')); + + if (err.mark) { + parseResult.first().attributes.set('sourceMap', [ + new SourceMap([[err.mark.position, 1]]), + ]); + } + + return done(new Error(err.message), parseResult); + } + + // Some sane defaults since these are sometimes left out completely + if (loaded.info === undefined) { + loaded.info = {}; + } + + if (loaded.paths === undefined) { + loaded.paths = {}; + } + + // Parse and validate the Swagger document! + swaggerParser.validate(loaded, (err) => { + const swagger = swaggerParser.api; + this.swagger = swaggerParser.api; + + if (err) { + if (swagger === undefined) { + return done(err, parseResult); + } + + // Non-fatal errors, so let us try and create annotations for them and + // continue with the parsing as best we can. + if (err.details) { + const queue = [err.details]; + while (queue.length) { + for (const item of queue[0]) { + this.makeAnnotation(ANNOTATIONS.VALIDATION_ERROR, item.path, + item.message); + + if (item.inner) { + // TODO: I am honestly not sure what the correct behavior is + // here. Some items will have within them a tree of other items, + // some of which might contain more info (but it's unclear). + // Do we treat them as their own error or do something else? + queue.push(item.inner); + } + } + queue.shift(); + } + } + } + + const api = new Category(); + this.api = api; + parseResult.push(api); + + // Root API Element + api.classes.push('api'); + + if (swagger.info) { + this.path.push('info'); + + if (swagger.info.title) { + api.meta.set('title', swagger.info.title); + + if (this.generateSourceMap) { + this.makeSourceMap(api.meta.get('title'), this.path.concat(['title'])); + } + } + + if (swagger.info.description) { + api.content.push(new Copy(swagger.info.description)); + + if (this.generateSourceMap) { + this.makeSourceMap(api.content[api.content.length - 1], + this.path.concat(['description'])); + } + } + + this.path.pop(); + } + + if (swagger.host) { + let hostname = swagger.host; + + if (swagger.schemes) { + if (swagger.schemes.length > 1) { + this.makeAnnotation(ANNOTATIONS.DATA_LOST, ['schemes'], + 'Only the first scheme will be used to create a hostname'); + } + + hostname = `${swagger.schemes[0]}://${hostname}`; + } + + api.attributes.set('meta', {}); + const meta = api.attributes.get('meta'); + const member = new MemberElement('HOST', hostname); + member.meta.set('classes', ['user']); + + if (this.generateSourceMap) { + this.makeSourceMap(member, ['host']); + } + + meta.content.push(member); + } + + if (swagger.securityDefinitions) { + this.makeAnnotation(ANNOTATIONS.DATA_LOST, ['securityDefinitions'], + 'Authentication information is not yet supported'); + } + + if (swagger.security) { + this.makeAnnotation(ANNOTATIONS.DATA_LOST, ['security'], + 'Authentication information is not yet supported'); + } + + if (swagger.externalDocs) { + this.makeAnnotation(ANNOTATIONS.DATA_LOST, ['externalDocs'], + 'External documentation is not yet supported'); + } + + this.group = api; + + // Swagger has a paths object to loop through + // The key is the href + _.each(_.omit(swagger.paths, isExtension), (pathValue, href) => { + this.handleSwaggerPath(pathValue, href); + }); + + done(null, parseResult); + }); + } + + // == Internal properties & functions == + + handleSwaggerPath(pathValue, href) { + const { + Asset, Copy, Category, DataStructure, HrefVariables, HttpHeaders, + Member: MemberElement, Object: ObjectElement, Resource, Transition, + } = this.minim.elements; + const resource = new Resource(); + + this.path.push('paths'); + this.path.push(href); + + if (this.generateSourceMap) { + this.makeSourceMap(resource, this.path); + } + + // Provide users with a way to add a title to a resource in Swagger + if (pathValue['x-summary']) { + resource.title = pathValue['x-summary']; + } + + // Provide users a way to add a description to a resource in Swagger + if (pathValue['x-description']) { + const resourceDescription = new Copy(pathValue['x-description']); + resource.push(resourceDescription); + } + + if (this.useResourceGroups()) { + const groupName = pathValue['x-group-name']; + + if (groupName) { + this.group = this.api.find((el) => el.element === 'category' && el.classes.contains('resourceGroup') && el.title === groupName).first(); + + if (!this.group) { + this.group = new Category(); + this.group.title = groupName; + this.group.classes.push('resourceGroup'); + + if (this.swagger.tags && this.swagger.tags.forEach) { + this.swagger.tags.forEach((tag) => { + // TODO: Check for external docs here? + if (tag.name === groupName && tag.description) { + this.group.content.push(new Copy(tag.description)); + } + }); + } + + this.api.content.push(this.group); + } + } + } + + this.group.content.push(resource); + + const pathObjectParameters = pathValue.parameters || []; + + // TODO: Currently this only supports URI parameters for `path` and `query`. + // It should add support for `body` parameters as well. + if (pathObjectParameters.length > 0) { + resource.hrefVariables = new HrefVariables(); + + pathObjectParameters.forEach((parameter, index) => { + this.path.push('parameters'); + this.path.push(index); + + if (parameter.in === 'query' || parameter.in === 'path') { + const member = this.convertParameterToMember(parameter, this.path); + if (this.generateSourceMap) { + this.makeSourceMap(member, this.path); + } + resource.hrefVariables.content.push(member); + } else if (parameter.in === 'body') { + this.makeAnnotation(ANNOTATIONS.DATA_LOST, this.path, + 'Path-level body parameters are not yet supported'); + } + + this.path.pop(); + this.path.pop(); + }); + } + + const relevantMethods = _.chain(pathValue) + .omit('parameters', '$ref') + .omit(isExtension) + .value(); + + // Each path is an object with methods as properties + _.each(relevantMethods, (methodValue, method) => { + const transition = new Transition(); + resource.content.push(transition); + + this.path.push(method); + + if (this.generateSourceMap) { + this.makeSourceMap(transition, this.path); + } + + if (methodValue.externalDocs) { + this.makeAnnotation(ANNOTATIONS.DATA_LOST, + this.path.concat(['externalDocs']), + 'External documentation is not yet supported'); + } + + const methodValueParameters = methodValue.parameters || []; + + const queryParameters = methodValueParameters.filter((parameter) => { + return parameter.in === 'query'; + }); + + // URI parameters are for query and path variables + const uriParameters = methodValueParameters.filter((parameter) => { + return parameter.in === 'query' || parameter.in === 'path'; + }); + + // Body parameters are ones that define JSON Schema + const bodyParameters = methodValueParameters.filter((parameter) => { + return parameter.in === 'body'; + }); + + // Form parameters are send as encoded form data in the body + const formParameters = methodValueParameters.filter((parameter) => { + return parameter.in === 'formData'; + }); + + const basePath = (this.swagger.basePath || '').replace(/[/]+$/, ''); + const hrefForResource = buildUriTemplate(basePath, href, pathObjectParameters, queryParameters); + resource.attributes.set('href', hrefForResource); + + if (methodValue.summary) { + transition.meta.set('title', methodValue.summary); + + if (this.generateSourceMap) { + const title = transition.meta.get('title'); + this.makeSourceMap(title, this.path.concat(['summary'])); + } + } + + if (methodValue.description) { + const description = new Copy(methodValue.description); + transition.push(description); + + if (this.generateSourceMap) { + this.makeSourceMap(description, this.path.concat(['description'])); + } + } + + if (methodValue.operationId) { + transition.attributes.set('relation', methodValue.operationId); + } + + // For each uriParameter, create an hrefVariable + if (uriParameters.length > 0) { + transition.hrefVariables = new HrefVariables(); + + this.path.push('parameters'); + + uriParameters.forEach((parameter) => { + const index = methodValueParameters.indexOf(parameter); + this.path.push(index); + transition.hrefVariables.content.push( + this.convertParameterToMember(parameter, this.path)); + this.path.pop(); + }); + + this.path.pop(); + } + + // Currently, default responses are not supported in API Description format + const relevantResponses = _.chain(methodValue.responses) + .omit('default') + .omit(isExtension) + .value(); + + if (methodValue.responses && methodValue.responses.default) { + this.path.push('responses'); + this.path.push('default'); + this.makeAnnotation(ANNOTATIONS.DATA_LOST, this.path, + 'Default response is not yet supported'); + this.path.pop(); + this.path.pop(); + } + + if (_.keys(relevantResponses).length === 0) { + if (bodyParameters.length) { + // Create an empty successful response so that the request/response + // pair gets properly generated. In the future we may want to + // refactor the code below as this is a little weird. + relevantResponses.null = {}; + } else { + this.createTransaction(transition, method); + } + } + + // Transactions are created for each response in the document + _.each(relevantResponses, (responseValue, statusCode) => { + let examples = { + '': undefined, + }; + + this.path.push('responses'); + this.path.push(statusCode); + + if (responseValue.examples) { + examples = responseValue.examples; + } + + examples = _.omit(examples, 'schema'); + + _.each(examples, (responseBody, contentType) => { + const transaction = this.createTransaction(transition, method); + const request = transaction.request; + const response = transaction.response; + + if (this.generateSourceMap) { + this.makeSourceMap(transaction, this.path); + this.makeSourceMap(request, this.path.slice(0, 3)); + + if (statusCode) { + this.makeSourceMap(response, this.path); + } + } + + if (responseValue.description) { + const description = new Copy(responseValue.description); + response.content.push(description); + if (this.generateSourceMap) { + this.makeSourceMap(description, this.path.concat(['description'])); + } + } + + const headers = new HttpHeaders(); + + if (contentType) { + headers.push(new MemberElement( + 'Content-Type', contentType + )); + + if (this.generateSourceMap) { + this.makeSourceMap(headers.content[headers.content.length - 1], this.path.concat(['examples', contentType])); + } + + response.headers = headers; + } + + if (responseValue.headers) { + response.headers = this.createHeaders(headers, responseValue.headers); + } + + // Body parameters define request schemas + _.each(bodyParameters, (bodyParameter) => { + const schemaAsset = this.createAssetFromJsonSchema( + bodyParameter.schema); + request.content.push(schemaAsset); + }); + + // Using form parameters instead of body? We will convert those to + // data structures. + if (formParameters.length) { + const dataStructure = new DataStructure(); + // A form is essentially an object with key/value members + const dataObject = new ObjectElement(); + + _.each(formParameters, (param) => { + const index = methodValueParameters.indexOf(param); + dataObject.content.push(this.convertParameterToMember(param, this.path.slice(0, 3).concat(['parameters', index]))); + }); + + dataStructure.content = dataObject; + request.content.push(dataStructure); + } + + this.path.push('examples'); + + // Responses can have bodies + if (responseBody !== undefined) { + let formattedResponseBody = responseBody; + + if (typeof(responseBody) !== 'string') { + formattedResponseBody = JSON.stringify(responseBody, null, 2); + } + + const bodyAsset = new Asset(formattedResponseBody); + bodyAsset.classes.push('messageBody'); + if (this.generateSourceMap) { + this.path.push(contentType); + this.makeSourceMap(bodyAsset, this.path); + this.path.pop(); + } + response.content.push(bodyAsset); + } + + // Responses can have schemas in Swagger + const schema = responseValue.schema || (responseValue.examples && responseValue.examples.schema); + if (schema) { + const schemaAsset = this.createAssetFromJsonSchema(schema); + if (this.generateSourceMap) { + let schemaPath = this.path.slice(0, 5); + if (responseValue.examples && responseValue.examples.schema) { + schemaPath = schemaPath.concat(['examples', 'schema']); + } else { + schemaPath = schemaPath.concat(['schema']); + } + this.makeSourceMap(schemaAsset, schemaPath); + } + response.content.push(schemaAsset); + } + + if (statusCode !== 'null') { + response.attributes.set('statusCode', statusCode); + } + + this.path.pop(); + }); + + this.path.pop(); + this.path.pop(); + }); + + this.path.pop(); + this.path.pop(); + }); + + this.path.pop(); + } + + // Takes in an `httpHeaders` element and a list of Swagger headers. Adds + // the Swagger headers to the element and then returns the modified element. + createHeaders(element, headers) { + const {Member: MemberElement} = this.minim.elements; + + this.path.push('headers'); + + for (const headerName in headers) { + if (headers.hasOwnProperty(headerName)) { + const header = headers[headerName]; + let value = ''; + + // Choose the first available option + if (header.enum) { + value = header.enum[0]; + } + + if (header.default) { + value = header.default; + } + + const member = new MemberElement(headerName, value); + + if (this.generateSourceMap) { + this.makeSourceMap(member, this.path.concat([headerName])); + } + + if (header.description) { + member.meta.set('description', header.description); + + if (this.generateSourceMap) { + this.makeSourceMap(member.meta.get('description'), this.path.concat(['description'])); + } + } + + element.push(member); + } + } + + this.path.pop(); + + return element; + } + + // Lazy-loaded input AST is made available when we need it. If it can't be + // loaded, then an annotation is generated with more information about why. + get ast() { + if (this._ast !== undefined) { + return this._ast; + } + + if (_.isString(this.source)) { + try { + this._ast = new Ast(this.source); + } catch (err) { + this._ast = null; + this.makeAnnotation(ANNOTATIONS.AST_UNAVAILABLE, null, + 'Input AST could not be composed, so source maps will not be available'); + } + } else { + this._ast = null; + this.makeAnnotation(ANNOTATIONS.AST_UNAVAILABLE, null, + 'Source maps are only available with string input'); + } + + return this._ast; + } + + // Test whether tags can be treated as resource groups, and if so it sets a + // group name for each resource (used later to create groups). + useResourceGroups() { + const tags = []; + + if (this.swagger.paths) { + _.each(this.swagger.paths, (path) => { + let tag = null; + + if (path) { + _.each(path, (operation) => { + if (operation.tags && operation.tags.length) { + if (operation.tags.length > 1) { + // Too many tags... each resource can only be in one group! + return false; + } + + if (tag === null) { + tag = operation.tags[0]; + } else if (tag !== operation.tags[0]) { + // Non-matching tags... can't have a resource in multiple groups! + return false; + } + } + }); + } + + if (tag) { + path['x-group-name'] = tag; + tags.push(tag); + } + }); + } + + return tags.length > 0; + } + + // Make a new source map for the given element + makeSourceMap(element, path) { + if (this.ast) { + const SourceMap = this.minim.getElementClass('sourceMap'); + const position = this.ast.getPosition(path); + if (position && !isNaN(position.start) && !isNaN(position.end)) { + element.attributes.set('sourceMap', [ + new SourceMap([[position.start, position.end - position.start]]), + ]); + } + } + } + + // Make a new annotation for the given path and message + makeAnnotation(info, path, message) { + const {Annotation, Link} = this.minim.elements; + const annotation = new Annotation(message); + annotation.classes.push(info.type); + annotation.code = info.code; + this.result.content.push(annotation); + + if (info.fragment) { + const link = new Link(); + link.relation = 'origin'; + link.href = `http://docs.apiary.io/validations/swagger#${info.fragment}`; + annotation.links.push(link); + } + + if (path && this.ast) { + this.makeSourceMap(annotation, path); + } + } + + convertParameterToElement(parameter, path, setAttributes = false) { + const { + Array: ArrayElement, Boolean: BooleanElement, Number: NumberElement, + String: StringElement, + } = this.minim.elements; + let element; + + // Convert from Swagger types to Minim elements + if (parameter.type === 'string') { + element = new StringElement(''); + } else if (parameter.type === 'integer' || parameter.type === 'number') { + element = new NumberElement(); + } else if (parameter.type === 'boolean') { + element = new BooleanElement(); + } else if (parameter.type === 'array') { + element = new ArrayElement(); + + if (parameter.items) { + element.content = [this.convertParameterToElement( + parameter.items, (path || []).concat(['items']), true)]; + } + } else { + // Default to a string in case we get a type we haven't seen + element = new StringElement(''); + } + + if (this.generateSourceMap) { + this.makeSourceMap(element, path); + } + + if (setAttributes) { + if (parameter.description) { + element.description = parameter.description; + + if (this.generateSourceMap) { + this.makeSourceMap(element.meta.get('description'), path.concat(['description'])); + } + } + + if (parameter.required) { + element.attributes.set('typeAttributes', ['required']); + } + + if (parameter.default !== undefined) { + element.attributes.set('default', parameter.default); + } + } + + return element; + } + + convertParameterToMember(parameter, path) { + const MemberElement = this.minim.getElementClass('member'); + const memberValue = this.convertParameterToElement(parameter, path); + + // TODO: Update when Minim has better support for elements as values + // should be: new MemberType(parameter.name, memberValue); + const member = new MemberElement(parameter.name); + member.content.value = memberValue; + + if (this.generateSourceMap) { + this.makeSourceMap(member, path); + } + + if (parameter.description) { + member.description = parameter.description; + + if (this.generateSourceMap) { + this.makeSourceMap(member.meta.get('description'), + path.concat(['description'])); + } + } + + if (parameter.required) { + member.attributes.set('typeAttributes', ['required']); + } + + // If there is a default, it is set on the member value instead of the member + // element itself because the default value applies to the value. + if (parameter.default) { + memberValue.attributes.set('default', parameter.default); + } + + return member; + } + + createAssetFromJsonSchema(jsonSchema) { + const Asset = this.minim.getElementClass('asset'); + const schemaAsset = new Asset(JSON.stringify(jsonSchema)); + schemaAsset.classes.push('messageBodySchema'); + schemaAsset.attributes.set('contentType', 'application/schema+json'); + + return schemaAsset; + } + + createTransaction(transition, method) { + const {HttpRequest, HttpResponse, HttpTransaction} = this.minim.elements; + const transaction = new HttpTransaction(); + transaction.content = [new HttpRequest(), new HttpResponse()]; + + if (transition) { + transition.content.push(transaction); + } + + if (method) { + transaction.request.attributes.set('method', method.toUpperCase()); + } + + return transaction; + } +}
src/uri-template.js+1 −1 modified@@ -1,4 +1,4 @@ -import _ from 'underscore'; +import _ from 'lodash'; export default function buildUriTemplate(basePath, href, pathObjectParameters = [], queryParameters = []) { if (queryParameters.length > 0 || pathObjectParameters.length > 0) {
test/fixtures/refract/payload-as-string.json+1 −1 modified@@ -231,4 +231,4 @@ ] } ] -} +} \ No newline at end of file
test/fixtures/refract/petstore.json+42 −3 modified@@ -22,7 +22,20 @@ ] }, "attributes": { - "code": 4 + "code": 4, + "sourceMap": [ + { + "element": "sourceMap", + "meta": {}, + "attributes": {}, + "content": [ + [ + 7245, + 275 + ] + ] + } + ] }, "content": "Data does not match any schemas from 'oneOf'" }, @@ -45,7 +58,20 @@ ] }, "attributes": { - "code": 4 + "code": 4, + "sourceMap": [ + { + "element": "sourceMap", + "meta": {}, + "attributes": {}, + "content": [ + [ + 7245, + 275 + ] + ] + } + ] }, "content": "Data does not match any schemas from 'oneOf'" }, @@ -68,7 +94,20 @@ ] }, "attributes": { - "code": 4 + "code": 4, + "sourceMap": [ + { + "element": "sourceMap", + "meta": {}, + "attributes": {}, + "content": [ + [ + 7245, + 275 + ] + ] + } + ] }, "content": "Missing required property: $ref" },
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/advisories/GHSA-2r7f-4h2c-5x73ghsaADVISORY
- github.com/apiaryio/fury-adapter-swagger/commit/777e2d68f03546a88f3203bbd4725df8b1f662a7ghsaWEB
- github.com/apiaryio/fury-adapter-swagger/commit/f4407e3a5323bc31123d45dbc93b8417002e4d51ghsaWEB
- github.com/apiaryio/fury-adapter-swagger/pull/89ghsaWEB
- security.snyk.io/vuln/npm:fury-adapter-swagger:20161024ghsaWEB
- www.npmjs.com/advisories/305ghsaWEB
News mentions
0No linked articles in our index yet.