snyk-nuget-plugin
Version:
Snyk CLI NuGet plugin
345 lines • 18.7 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildDepGraphFromFiles = buildDepGraphFromFiles;
exports.buildDepTreeFromFiles = buildDepTreeFromFiles;
const fs = require("fs");
const path = require("path");
const csProjParser = require("./parsers/csproj-parser");
const debugModule = require("debug");
const depsParser = require("dotnet-deps-parser");
const dotnetCoreV3Parser = require("./parsers/dotnet-core-v3-parser");
const dotnetCoreParser = require("./parsers/dotnet-core-parser");
const dotnetCoreV2Parser = require("./parsers/dotnet-core-v2-parser");
const dotnetFrameworkParser = require("./parsers/dotnet-framework-parser");
const projectJsonParser = require("./parsers/project-json-parser");
const packagesConfigParser = require("./parsers/packages-config-parser");
const errors_1 = require("../errors");
const types_1 = require("./types");
const dotnet = require("./cli/dotnet");
const nugetFrameworksParser = require("./csharp/nugetframeworks_parser");
const runtimeAssemblyV2 = require("./runtime-assembly-v2");
const runtimeAssembly = require("./runtime-assembly");
const runtime_assembly_v2_1 = require("./runtime-assembly-v2");
const debug = debugModule('snyk');
const PROJECTSDK = 'Microsoft.NET.Sdk';
const PARSERS = {
'dotnet-core': {
depParser: dotnetCoreParser,
fileContentParser: JSON,
},
'dotnet-core-v2': {
depParser: dotnetCoreV2Parser,
fileContentParser: JSON,
},
'dotnet-core-v3': {
depParser: dotnetCoreV3Parser,
fileContentParser: JSON,
},
'packages.config': {
depParser: dotnetFrameworkParser,
fileContentParser: packagesConfigParser,
},
'project.json': {
depParser: dotnetFrameworkParser,
fileContentParser: projectJsonParser,
},
};
function getPackagesFolder(packagesFolder, projectRootFolder) {
if (packagesFolder) {
return path.resolve(process.cwd(), packagesFolder);
}
return path.resolve(projectRootFolder, 'packages');
}
function getRootName(root, projectRootFolder, projectNamePrefix) {
const defaultRootName = path.basename(root || projectRootFolder || '');
if (projectNamePrefix) {
return projectNamePrefix + defaultRootName;
}
return defaultRootName;
}
function getFileContents(fileContentPath) {
try {
debug(`Parsing content of ${fileContentPath}`);
return fs.readFileSync(fileContentPath, 'utf-8');
}
catch (error) {
throw new errors_1.FileNotProcessableError(error);
}
}
function tryToGetFileByName(dir, filename) {
const depsFilePath = path.join(dir, filename);
try {
const depsFile = fs.readFileSync(depsFilePath);
if (depsFile)
return depsFile;
}
catch (_) {
// Due to race conditions, fs docs suggests to not use .stat or .access to check if a file exists
// but instead we should to try and read it.
// https://nodejs.org/api/fs.html#fsstatpath-options-callback
}
return null;
}
// `dotnet` can publish the .deps file to a variety of places inside the publish folder, depending on what you're
// including and targeting. Instead of trying different directories, just scan them all. In most cases, the file
// will be in the root directory. (See https://github.com/Azure/azure-functions-vs-build-sdk/issues/518)
function findDepsFileInPublishDir(dir, filename) {
let renamedFile = null;
// Try to get the file via full path.
const namedFile = tryToGetFileByName(dir, filename);
if (namedFile)
return namedFile;
for (const item of fs.readdirSync(dir)) {
const itemPath = path.join(dir, item);
// The file is usually <project>.deps.json, but in edge cases, `dotnet` names it for you.
if (itemPath.endsWith('deps.json')) {
renamedFile = fs.readFileSync(itemPath);
}
if (!fs.statSync(itemPath).isDirectory()) {
continue;
}
// Otherwise, look in a nested dir for the same thing.
const foundFile = findDepsFileInPublishDir(itemPath, filename);
if (!foundFile) {
continue;
}
return foundFile;
}
return renamedFile || null;
}
async function getResultsWithPublish(decidedTargetFrameworks, projectPath, safeRoot, projectNameFromManifestFile, nugetFrameworksParserLocation, useFixForImprovedDotnetFalsePositives, resolvedProjectName, projectAssets) {
const parser = PARSERS['dotnet-core-v2'];
// Loop through all TargetFrameworks supplied and generate a dependency graph for each.
const results = [];
for (const decidedTargetFramework of decidedTargetFrameworks) {
// Run `dotnet publish` to create a self-contained publishable binary with included .dlls for assembly version inspection.
const publishDir = await dotnet.publish(
// Attempt to feed it the full path to the project file itself, as multiple could exist. If that fails, don't break the flow, just send the folder as previously
projectPath || safeRoot, decidedTargetFramework);
// Then inspect the dependency graph for the runtimepackage's assembly versions.
const filename = `${projectNameFromManifestFile}.deps.json`;
const depsFile = findDepsFileInPublishDir(publishDir, filename);
if (!depsFile) {
throw new errors_1.CliCommandError(`unable to locate ${filename} anywhere inside ${publishDir}, file is needed for runtime resolution to occur, aborting`);
}
const publishedProjectDeps = JSON.parse(depsFile.toString('utf-8'));
// Parse the TargetFramework using Nuget.Frameworks itself, instead of trying to reinvent the wheel, thus ensuring
// we have maximum context to use later when building the depGraph.
const response = await dotnet.run(nugetFrameworksParserLocation, [
decidedTargetFramework,
]);
const targetFrameworkInfo = JSON.parse(response);
if (targetFrameworkInfo.IsUnsupported) {
throw new errors_1.InvalidManifestError(`dotnet was not able to parse the target framework ${decidedTargetFramework}, it was reported unsupported by the dotnet runtime`);
}
let assemblyVersions = {};
if (!decidedTargetFramework.includes('netstandard')) {
assemblyVersions =
runtimeAssembly.generateRuntimeAssemblies(publishedProjectDeps);
// Specifically targeting .NET Standard frameworks will not provide any specific runtime assembly information in
// the published artifacts files, and can thus not be read more precisely than the .deps file will tell us up-front.
// This probably makes sense when looking at https://dotnet.microsoft.com/en-us/platform/dotnet-standard#versions.
// As such, we don't generate any runtime assemblies and generate the dependency graph without it.
if (useFixForImprovedDotnetFalsePositives) {
let projectFolder = '';
// Get the project folder path
if (projectPath) {
projectFolder = path.dirname(projectPath);
}
// An important failure point here will be a reference to a version of the dotnet SDK that is
// not installed in the environment. Ex: global.json specifies 6.0.100, but the only version install in the env is 8.0.100
// https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet#options-for-displaying-environment-information-and-available-commands
await dotnet.execute(['--version'], projectFolder);
assemblyVersions = await runtimeAssemblyV2.generateRuntimeAssemblies(projectFolder || safeRoot, assemblyVersions);
}
}
const depGraph = parser.depParser.parse(resolvedProjectName, projectAssets, publishedProjectDeps, assemblyVersions, useFixForImprovedDotnetFalsePositives);
results.push({
dependencyGraph: depGraph,
targetFramework: decidedTargetFramework,
});
}
return results;
}
async function getResultsWithoutPublish(decidedTargetFrameworks, projectPath, safeRoot, nugetFrameworksParserLocation, resolvedProjectName, projectAssets) {
const parser = PARSERS['dotnet-core-v3'];
const projectFolder = projectPath ? path.dirname(projectPath) : safeRoot;
const { sdkVersion, sdkPath } = await (0, runtime_assembly_v2_1.extractSdkInfo)(projectFolder);
const localRuntimes = await dotnet.execute(['--list-runtimes'], projectFolder);
const runtimeVersion = (0, runtime_assembly_v2_1.findLatestMatchingVersion)(localRuntimes, sdkVersion);
const overridesAssemblies = {};
try {
const overridesPath = `${path.dirname(sdkPath)}${runtime_assembly_v2_1.PACKS_PATH}${runtimeVersion}/${runtime_assembly_v2_1.PACKAGE_OVERRIDES_FILE}`;
const overridesText = fs.readFileSync(overridesPath, 'utf-8');
for (const pkg of overridesText.split('\n')) {
if (pkg) {
const [name, version] = pkg.split('|');
// Trim any carriage return
overridesAssemblies[name] = version.trim();
}
}
}
catch (err) {
throw new errors_1.FileNotProcessableError(`Failed to read PackageOverrides.txt, error: ${err}`);
}
// Loop through all TargetFrameworks supplied and generate a dependency graph for each.
const results = [];
for (const decidedTargetFramework of decidedTargetFrameworks) {
// Parse the TargetFramework using Nuget.Frameworks itself, instead of trying to reinvent the wheel, thus ensuring
// we have maximum context to use later when building the depGraph.
const response = await dotnet.run(nugetFrameworksParserLocation, [
decidedTargetFramework,
]);
const targetFrameworkInfo = JSON.parse(response);
if (targetFrameworkInfo.IsUnsupported) {
throw new errors_1.InvalidManifestError(`dotnet was not able to parse the target framework ${decidedTargetFramework}, it was reported unsupported by the dotnet runtime`);
}
const overrides = {
overridesAssemblies,
overrideVersion: targetFrameworkInfo.Version.split('.')
.slice(0, -1)
.join('.'),
};
let targetFramework = decidedTargetFramework;
if (targetFrameworkInfo.Framework === '.NETStandard') {
targetFramework = targetFrameworkInfo.DotNetFrameworkName;
}
const depGraph = parser.depParser.parse(resolvedProjectName, targetFramework, projectAssets, overrides);
results.push({
dependencyGraph: depGraph,
targetFramework: decidedTargetFramework,
});
}
return results;
}
async function buildDepGraphFromFiles(root, targetFile, manifestType, useProjectNameFromAssetsFile, useFixForImprovedDotnetFalsePositives, useImprovedDotnetWithoutPublish, projectNamePrefix, targetFramework) {
const safeRoot = root || '.';
const safeTargetFile = targetFile || '.';
const fileContentPath = path.resolve(safeRoot, safeTargetFile);
const fileContent = getFileContents(fileContentPath);
const parser = PARSERS['dotnet-core-v2'];
const projectAssets = await parser.fileContentParser.parse(fileContent);
if (!projectAssets.project?.frameworks) {
throw new errors_1.FileNotProcessableError(`unable to detect any target framework in manifest file ${safeTargetFile}, a valid one is needed to continue down this path.`);
}
// Scan all 'frameworks' detected in the project.assets.json file, and use the targetAlias if detected and
// otherwise the raw key name, as it's not guaranteed that all framework objects contains a targetAlias.
const targetFrameworks = Object.entries(projectAssets.project.frameworks).map(([key, value]) => ('targetAlias' in value ? value.targetAlias : key));
if (targetFrameworks.length <= 0) {
throw new errors_1.FileNotProcessableError(`unable to detect a target framework in ${safeTargetFile}, a valid one is needed to continue down this path.`);
}
if (targetFramework && !targetFrameworks.includes(targetFramework)) {
console.warn(`\x1b[33m⚠ WARNING\x1b[0m: Supplied targetframework \x1b[1m${targetFramework}\x1b[0m was not detected in the supplied
manifest file. Available targetFrameworks detected was \x1b[1m${targetFrameworks.join(',')}\x1b[0m.
Will attempt to build dependency graph anyway, but the operation might fail.`);
}
let resolvedProjectName = getRootName(root, safeRoot, projectNamePrefix);
const projectNameFromManifestFile = projectAssets?.project?.restore?.projectName;
if (manifestType === types_1.ManifestType.DOTNET_CORE &&
useProjectNameFromAssetsFile) {
if (projectNameFromManifestFile) {
resolvedProjectName = projectNameFromManifestFile;
}
else {
debug(`project.assets.json file doesn't contain a value for 'projectName'. Using default value: ${resolvedProjectName}`);
}
}
// If a specific targetFramework has been requested, only query that, otherwise try to do them all
const decidedTargetFrameworks = targetFramework
? [targetFramework]
: targetFrameworks.filter((framework) => {
// Passing a const value as the project sdk. Why? The targetFile it's project.assets.json, which gets generated
// only for the sdk style projects. The assets file won't get generated for projects which rely on packages.config.
// The reason behind deciding to call this method is because maybe in the future we want to not support some specific
// target frameworks.
if (useImprovedDotnetWithoutPublish) {
if (!depsParser.isSupportedByV3GraphGeneration(framework, PROJECTSDK)) {
return false;
}
return true;
}
if (!depsParser.isSupportedByV2GraphGeneration(framework)) {
console.warn(`\x1b[33m⚠ WARNING\x1b[0m: The runtime resolution flag is currently only supported for the following TargetFrameworks: .NET versions 6 and higher, all versions of .NET Core and all versions of .NET Standard. Detected a TargetFramework: \x1b[1m${framework}\x1b[0m, which will be skipped.`);
return false;
}
return true;
});
if (decidedTargetFrameworks.length == 0) {
throw new errors_1.InvalidManifestError(`Was not able to find any supported TargetFrameworks to scan, aborting`);
}
// Ensure `dotnet` is installed on the system or fail trying.
await dotnet.validate();
// Write a .NET Framework Parser to a temporary directory for validating TargetFrameworks.
const nugetFrameworksParserLocation = nugetFrameworksParser.generate();
await dotnet.restore(nugetFrameworksParserLocation);
// Access the path of this project, to ensure we get the right .csproj file, in case of multiples present
const projectPath = projectAssets.project.restore.projectPath;
if (!projectPath) {
console.warn(`\x1b[33m⚠ WARNING\x1b[0m: Could not detect any projectPath in the project assets file, if your solution contains multiple projects in the same folder, this operation might fail.`);
}
if (useImprovedDotnetWithoutPublish) {
return getResultsWithoutPublish(decidedTargetFrameworks, projectPath, safeRoot, nugetFrameworksParserLocation, resolvedProjectName, projectAssets);
}
return getResultsWithPublish(decidedTargetFrameworks, projectPath, safeRoot, projectNameFromManifestFile, nugetFrameworksParserLocation, useFixForImprovedDotnetFalsePositives, resolvedProjectName, projectAssets);
}
async function buildDepTreeFromFiles(root, targetFile, packagesFolderPath, manifestType, useProjectNameFromAssetsFile, projectNamePrefix) {
const safeRoot = root || '.';
const safeTargetFile = targetFile || '.';
const fileContentPath = path.resolve(safeRoot, safeTargetFile);
const fileContent = getFileContents(fileContentPath);
const projectRootFolder = path.resolve(fileContentPath, '../../');
const packagesFolder = getPackagesFolder(packagesFolderPath, projectRootFolder);
const tree = {
dependencies: {},
meta: {},
name: getRootName(root, projectRootFolder, projectNamePrefix),
packageFormatVersion: 'nuget:0.0.0',
version: '0.0.0',
};
let targetFrameworks;
try {
if (manifestType === types_1.ManifestType.DOTNET_CORE) {
targetFrameworks =
csProjParser.getTargetFrameworksFromProjFile(projectRootFolder);
}
else {
// .csproj is in the same directory as packages.config or project.json
const fileContentParentDirectory = path.resolve(fileContentPath, '../');
targetFrameworks = csProjParser.getTargetFrameworksFromProjFile(fileContentParentDirectory);
// finally, for the .NETFramework project, try to assume the framework using dotnet-deps-parser
if (targetFrameworks.length <= 0) {
// currently only process packages.config files
if (manifestType === types_1.ManifestType.PACKAGES_CONFIG) {
const minimumTargetFramework = await packagesConfigParser.getMinimumTargetFramework(fileContent);
if (minimumTargetFramework) {
targetFrameworks = [minimumTargetFramework];
}
}
}
}
}
catch (error) {
return Promise.reject(error);
}
// Only supports the first targetFramework we find.
// Use the newer `buildDepGraphFromFiles` for better support for multiple target frameworks.
const targetFramework = targetFrameworks.length > 0 ? targetFrameworks[0].original : undefined;
tree.meta = {
targetFramework: targetFramework,
};
const parser = PARSERS[manifestType];
const manifest = await parser.fileContentParser.parse(fileContent, tree);
if (manifestType === types_1.ManifestType.DOTNET_CORE &&
useProjectNameFromAssetsFile) {
const projectName = manifest?.project?.restore?.projectName;
if (projectName) {
tree.name = projectName;
}
else {
debug("project.assets.json file doesn't contain a value for 'projectName'. Using default value: " +
tree.name);
}
}
return parser.depParser.parse(tree, manifest, targetFramework, packagesFolder);
}
//# sourceMappingURL=index.js.map