fabric8-analytics-lsp-server
Advanced tools
Comparing version 0.3.2 to 0.4.0
@@ -12,2 +12,3 @@ /* -------------------------------------------------------------------------------------------- | ||
const utils_1 = require("./utils"); | ||
const child_process_1 = require("child_process"); | ||
/* Please note :: There was issue with semverRegex usage in the code. During run time, it extracts | ||
@@ -72,8 +73,7 @@ * version with 'v' prefix, but this is not be behavior of semver in CLI and test environment. | ||
class NaiveGomodParser { | ||
constructor(contents) { | ||
this.dependencies = NaiveGomodParser.parseDependencies(contents); | ||
constructor(contents, goImports) { | ||
this.dependencies = NaiveGomodParser.parseDependencies(contents, goImports); | ||
} | ||
static parseDependencies(contents) { | ||
const gomod = contents.split("\n"); | ||
return gomod.reduce((dependencies, line, index) => { | ||
static parseDependencies(contents, goImports) { | ||
return contents.split("\n").reduce((dependencies, line, index) => { | ||
// Ignore "replace" lines | ||
@@ -99,2 +99,11 @@ if (!line.includes("=>")) { | ||
dependencies.push(new Dependency(entry)); | ||
// Find packages of this module. | ||
goImports.forEach(pckg => { | ||
if (pckg != pkgName && pckg.startsWith(pkgName + "/")) { | ||
const entry = new types_1.KeyValueEntry(pckg, { line: 0, column: 0 }); | ||
entry.value = new types_1.Variant(types_1.ValueType.String, 'v' + version[0]); | ||
entry.value_position = { line: index + 1, column: version.index }; | ||
dependencies.push(new Dependency(entry)); | ||
} | ||
}); | ||
} | ||
@@ -113,7 +122,21 @@ } | ||
class GomodDependencyCollector { | ||
constructor(classes = ["dependencies"]) { | ||
constructor(manifestFile, classes = ["dependencies"]) { | ||
this.manifestFile = manifestFile; | ||
this.classes = classes; | ||
this.manifestFile = manifestFile; | ||
} | ||
async collect(contents) { | ||
let parser = new NaiveGomodParser(contents); | ||
let promiseExec = new Promise((resolve, reject) => { | ||
const vscodeRootpath = this.manifestFile.replace("file://", "").replace("/go.mod", ""); | ||
child_process_1.exec(`go list -f '{{ join .Imports "\\n" }}' ./...`, { cwd: vscodeRootpath, maxBuffer: 1024 * 1200 }, (error, stdout, stderr) => { | ||
if (error) { | ||
reject(`'go list' command failed with error :: ${stderr}`); | ||
} | ||
else { | ||
resolve(new Set(stdout.toString().split("\n"))); | ||
} | ||
}); | ||
}); | ||
const goImports = await promiseExec; | ||
let parser = new NaiveGomodParser(contents, goImports); | ||
return parser.parse(); | ||
@@ -120,0 +143,0 @@ } |
120
consumers.js
@@ -7,4 +7,5 @@ /* -------------------------------------------------------------------------------------------- | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.codeActionsMap = exports.EmptyResultEngine = exports.SecurityEngine = exports.DiagnosticsPipeline = void 0; | ||
exports.codeActionsMap = exports.SecurityEngine = exports.DiagnosticsPipeline = void 0; | ||
const utils_1 = require("./utils"); | ||
const vulnerability_1 = require("./vulnerability"); | ||
const vscode_languageserver_1 = require("vscode-languageserver"); | ||
@@ -31,3 +32,3 @@ ; | ||
class DiagnosticsPipeline { | ||
constructor(classes, dependency, config, diags, uri) { | ||
constructor(classes, dependency, config, diags, vulnerabilityAggregator, uri) { | ||
this.items = classes.map((i) => { return new i(dependency, config); }); | ||
@@ -38,2 +39,3 @@ this.dependency = dependency; | ||
this.uri = uri; | ||
this.vulnerabilityAggregator = vulnerabilityAggregator; | ||
} | ||
@@ -43,7 +45,41 @@ run(data) { | ||
if (item.consume(data)) { | ||
for (let d of item.produce(this.uri)) | ||
this.diagnostics.push(d); | ||
for (let d of item.produce()) { | ||
const aggVulnerability = this.vulnerabilityAggregator.aggregate(d); | ||
const aggDiagnostic = aggVulnerability.getDiagnostic(); | ||
// Add/Update quick action for given aggregated diangnostic | ||
// TODO: this can be done lazily | ||
if (aggVulnerability.recommendedVersion && (aggVulnerability.vulnerabilityCount > 0 || aggVulnerability.exploitCount != null)) { | ||
let codeAction = { | ||
title: `Switch to recommended version ${aggVulnerability.recommendedVersion}`, | ||
diagnostics: [aggDiagnostic], | ||
kind: vscode_languageserver_1.CodeActionKind.QuickFix, | ||
edit: { | ||
changes: {} | ||
} | ||
}; | ||
codeAction.edit.changes[this.uri] = [{ | ||
range: aggDiagnostic.range, | ||
newText: aggVulnerability.recommendedVersion | ||
}]; | ||
// We will have line|start as key instead of message | ||
codeActionsMap[aggDiagnostic.range.start.line + "|" + aggDiagnostic.range.start.character] = codeAction; | ||
} | ||
if (this.vulnerabilityAggregator.isNewVulnerability) { | ||
this.diagnostics.push(aggDiagnostic); | ||
} | ||
else { | ||
// Update the existing diagnostic object based on range values | ||
this.diagnostics.forEach((diag, index) => { | ||
if (diag.range.start.line == aggVulnerability.range.start.line && | ||
diag.range.start.character == aggVulnerability.range.start.character) { | ||
this.diagnostics[index] = aggDiagnostic; | ||
return; | ||
} | ||
}); | ||
} | ||
} | ||
} | ||
} | ||
return this.diagnostics; | ||
// This is not used by any one. | ||
return []; | ||
} | ||
@@ -57,2 +93,4 @@ } | ||
this.config = config; | ||
this.package = null; | ||
this.version = null; | ||
this.changeTo = null; | ||
@@ -71,2 +109,8 @@ this.message = null; | ||
} | ||
if (this.packageBinding != null) { | ||
this.package = bind_object(data, this.packageBinding); | ||
} | ||
if (this.versionBinding != null) { | ||
this.version = bind_object(data, this.versionBinding); | ||
} | ||
if (this.changeToBinding != null) { | ||
@@ -94,24 +138,2 @@ this.changeTo = bind_object(data, this.changeToBinding); | ||
; | ||
/* We've received an empty/unfinished result, display that analysis is pending */ | ||
class EmptyResultEngine extends AnalysisConsumer { | ||
constructor(context, config) { | ||
super(config); | ||
this.context = context; | ||
} | ||
produce() { | ||
if (this.item == {} && (this.item.finished_at === undefined || | ||
this.item.finished_at == null)) { | ||
return [{ | ||
severity: vscode_languageserver_1.DiagnosticSeverity.Information, | ||
range: utils_1.get_range(this.context.version), | ||
message: `Application dependency ${this.context.name.value}-${this.context.version.value} - analysis is pending`, | ||
source: 'Dependency Analytics Plugin [Powered by Snyk]' | ||
}]; | ||
} | ||
else { | ||
return []; | ||
} | ||
} | ||
} | ||
exports.EmptyResultEngine = EmptyResultEngine; | ||
/* Report CVEs in found dependencies */ | ||
@@ -123,2 +145,4 @@ class SecurityEngine extends AnalysisConsumer { | ||
this.binding = { path: ['vulnerability'] }; | ||
this.packageBinding = { path: ['package'] }; | ||
this.versionBinding = { path: ['version'] }; | ||
/* recommendation to use a different version */ | ||
@@ -137,43 +161,5 @@ this.changeToBinding = { path: ['recommended_versions'] }; | ||
} | ||
produce(ctx) { | ||
produce() { | ||
if (this.item.length > 0) { | ||
/* The diagnostic's severity. */ | ||
let diagSeverity; | ||
if (this.vulnerabilityCount == 0 && this.advisoryCount > 0) { | ||
diagSeverity = vscode_languageserver_1.DiagnosticSeverity.Information; | ||
} | ||
else { | ||
diagSeverity = vscode_languageserver_1.DiagnosticSeverity.Error; | ||
} | ||
const recommendedVersion = this.changeTo || "N/A"; | ||
const exploitCount = this.exploitCount || "unavailable"; | ||
const msg = `${this.context.name.value}: ${this.context.version.value} | ||
Known security vulnerability: ${this.vulnerabilityCount} | ||
Security advisory: ${this.advisoryCount} | ||
Exploits: ${exploitCount} | ||
Highest severity: ${this.highestSeverity} | ||
Recommendation: ${recommendedVersion}`; | ||
let diagnostic = { | ||
severity: diagSeverity, | ||
range: utils_1.get_range(this.context.version), | ||
message: msg, | ||
source: '\nDependency Analytics Plugin [Powered by Snyk]', | ||
}; | ||
// TODO: this can be done lazily | ||
if (this.changeTo && (this.vulnerabilityCount > 0 || this.exploitCount != null)) { | ||
let codeAction = { | ||
title: "Switch to recommended version " + this.changeTo, | ||
diagnostics: [diagnostic], | ||
kind: vscode_languageserver_1.CodeActionKind.QuickFix, | ||
edit: { | ||
changes: {} | ||
} | ||
}; | ||
codeAction.edit.changes[ctx] = [{ | ||
range: diagnostic.range, | ||
newText: this.changeTo | ||
}]; | ||
codeActionsMap[diagnostic.message] = codeAction; | ||
} | ||
return [diagnostic]; | ||
return [new vulnerability_1.Vulnerability(this.package, this.version, 1, this.vulnerabilityCount, this.advisoryCount, this.exploitCount, this.highestSeverity, this.changeTo, utils_1.get_range(this.context.version))]; | ||
} | ||
@@ -180,0 +166,0 @@ else { |
{ | ||
"name": "fabric8-analytics-lsp-server", | ||
"description": "LSP Server for Dependency Analytics", | ||
"version": "0.3.2", | ||
"version": "0.4.0", | ||
"author": "Pavel Odvody", | ||
@@ -37,3 +37,4 @@ "contributors": [ | ||
"winston": "3.2.1", | ||
"xml2object": "0.1.2" | ||
"xml2object": "0.1.2", | ||
"compare-versions": "3.6.0" | ||
}, | ||
@@ -51,3 +52,4 @@ "devDependencies": { | ||
"ts-node": "^8.3.0", | ||
"typescript": "^3.6.3" | ||
"typescript": "^3.6.3", | ||
"fake-exec": "^1.1.0" | ||
}, | ||
@@ -54,0 +56,0 @@ "scripts": { |
@@ -12,5 +12,5 @@ /* -------------------------------------------------------------------------------------------- | ||
const consumers_1 = require("./consumers"); | ||
const aggregators_1 = require("./aggregators"); | ||
const node_fetch_1 = require("node-fetch"); | ||
const url = require('url'); | ||
const https = require('https'); | ||
const winston = require('winston'); | ||
@@ -192,6 +192,6 @@ let transport; | ||
/* Runs DiagnosticPileline to consume response and generate Diagnostic[] */ | ||
function runPipeline(response, diagnostics, diagnosticFilePath, dependencyMap, totalCount) { | ||
function runPipeline(response, diagnostics, packageAggregator, diagnosticFilePath, dependencyMap, totalCount) { | ||
response.forEach(r => { | ||
const dependency = dependencyMap.get(r.package + r.version); | ||
let pipeline = new consumers_1.DiagnosticsPipeline(DiagnosticsEngines, dependency, config, diagnostics, diagnosticFilePath); | ||
let pipeline = new consumers_1.DiagnosticsPipeline(DiagnosticsEngines, dependency, config, diagnostics, packageAggregator, diagnosticFilePath); | ||
pipeline.run(r); | ||
@@ -221,7 +221,23 @@ for (const item of pipeline.items) { | ||
connection.sendNotification('caNotification', { 'data': caDefaultMsg }); | ||
const deps = await collector.collect(contents); | ||
let deps = null; | ||
try { | ||
deps = await collector.collect(contents); | ||
} | ||
catch (error) { | ||
// Error can be raised during golang `go list ` command only. | ||
if (ecosystem == "golang") { | ||
console.error("Command execution failed, something wrong with manifest file go.mod\n%s", error); | ||
connection.sendNotification('caError', { 'data': 'Unable to execute `go list` command, run `go mod tidy` to resolve dependencies issues' }); | ||
return; | ||
} | ||
} | ||
let validPackages = deps; | ||
let packageAggregator = null; | ||
if (ecosystem != "golang") { | ||
validPackages = deps.filter(d => regexVersion.test(d.version.value.trim())); | ||
packageAggregator = new aggregators_1.NoopVulnerabilityAggregator(); | ||
} | ||
else { | ||
packageAggregator = new aggregators_1.GolangVulnerabilityAggregator(); | ||
} | ||
const requestPayload = validPackages.map(d => ({ "package": d.name.value, "version": d.version.value })); | ||
@@ -233,3 +249,3 @@ const requestMapper = new Map(validPackages.map(d => [d.name.value + d.version.value, d])); | ||
const start = new Date().getTime(); | ||
const allRequests = slicePayload(requestPayload, batchSize, ecosystem).map(request => fetchVulnerabilities(request).then(response => runPipeline(response, diagnostics, diagnosticFilePath, requestMapper, totalCount))); | ||
const allRequests = slicePayload(requestPayload, batchSize, ecosystem).map(request => fetchVulnerabilities(request).then(response => runPipeline(response, diagnostics, packageAggregator, diagnosticFilePath, requestMapper, totalCount))); | ||
await Promise.allSettled(allRequests); | ||
@@ -250,3 +266,3 @@ const end = new Date().getTime(); | ||
files.on(EventStream.Diagnostics, "^go\\.mod$", (uri, name, contents) => { | ||
sendDiagnostics('golang', uri, contents, new collector_1.GomodDependencyCollector()); | ||
sendDiagnostics('golang', uri, contents, new collector_1.GomodDependencyCollector(uri)); | ||
}); | ||
@@ -273,3 +289,3 @@ let checkDelay; | ||
for (let diagnostic of params.context.diagnostics) { | ||
let codeAction = consumers_1.codeActionsMap[diagnostic.message]; | ||
let codeAction = consumers_1.codeActionsMap[diagnostic.range.start.line + "|" + diagnostic.range.start.character]; | ||
if (codeAction != null) { | ||
@@ -276,0 +292,0 @@ codeActions.push(codeAction); |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
85631
18
898
6
12
+ Addedcompare-versions@3.6.0
+ Addedcompare-versions@3.6.0(transitive)