"Fossies" - the Fresh Open Source Software Archive

Member "cli-1.1280.1/src/cli/commands/log4shell.ts" (20 Feb 2024, 7517 Bytes) of package /linux/misc/snyk-cli-1.1280.1.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) TypeScript source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file.

    1 import { MethodArgs } from '../args';
    2 import { promises, Stats } from 'fs';
    3 import * as crypto from 'crypto';
    4 import * as AdmZip from 'adm-zip';
    5 import * as ora from 'ora';
    6 import * as semver from 'semver';
    7 import { FileSignatureDetails, vulnerableSignatures } from './log4shell-hashes';
    8 
    9 const readFile = promises.readFile;
   10 const readDir = promises.readdir;
   11 const stat = promises.stat;
   12 const MAX_FILE_SIZE = 2 * 1024 * 1024 * 1024 - 1;
   13 
   14 type ExploitType = 'Log4Shell' | 'DoS' | 'Unknown';
   15 type Signature = {
   16   value: string;
   17   path: string;
   18   exploitType: ExploitType;
   19 };
   20 type FileContent = Buffer;
   21 type FilePath = string;
   22 type Digest = string;
   23 
   24 type File = {
   25   path: FilePath;
   26   content: () => Promise<FileContent>;
   27 };
   28 
   29 class Paths {
   30   paths: Array<File>;
   31 
   32   constructor(paths: Array<File>) {
   33     this.paths = paths;
   34   }
   35 
   36   static empty() {
   37     return new Paths([]);
   38   }
   39 
   40   static fromZip(content: FileContent, path: FilePath) {
   41     try {
   42       const unzippedEntries = new AdmZip(content).getEntries();
   43 
   44       const entries: File[] = unzippedEntries.map((entry) => {
   45         return {
   46           path: path + '/' + entry.entryName,
   47           content: async () => entry.getData(),
   48         };
   49       });
   50 
   51       return new Paths(entries);
   52     } catch (error) {
   53       errors.push(error);
   54 
   55       return this.empty();
   56     }
   57   }
   58 
   59   static async fromDisk(paths: FilePath[]) {
   60     try {
   61       const entries = paths.map((path) => {
   62         return {
   63           path,
   64           content: async () => await readFile(path),
   65         };
   66       });
   67 
   68       return new Paths(entries);
   69     } catch (error) {
   70       errors.push(error);
   71 
   72       return this.empty();
   73     }
   74   }
   75 }
   76 
   77 interface FileHandler {
   78   (filePath: string, stats: Stats): void;
   79 }
   80 
   81 const errors: any[] = [];
   82 
   83 async function startSpinner(): Promise<ora.Ora> {
   84   const spinner: ora.Ora = ora({ isSilent: false, stream: process.stdout });
   85   spinner.text = `Looking for Log4Shell...`;
   86   spinner.start();
   87 
   88   return spinner;
   89 }
   90 
   91 // eslint-disable-next-line @typescript-eslint/no-unused-vars
   92 export default async function log4shell(...args: MethodArgs): Promise<void> {
   93   console.log(
   94     'Please note this command is for already built artifacts. To test source code please use `snyk test`.',
   95   );
   96 
   97   const signatures: Array<Signature> = new Array<Signature>();
   98   const spinner = await startSpinner();
   99 
  100   const paths: FilePath[] = await find('.');
  101 
  102   await parsePaths(await Paths.fromDisk(paths), signatures);
  103 
  104   spinner.stop();
  105 
  106   console.log('\nResults:');
  107 
  108   const issues = filterJndi(signatures);
  109   if (issues.length == 0) {
  110     console.log('No known vulnerable version of Log4J was detected');
  111     return;
  112   }
  113   const rceIssues: Signature[] = [];
  114   const dosIssues: Signature[] = [];
  115 
  116   issues.forEach((issue) => {
  117     issue.path = issue.path.replace(
  118       /(.*org\/apache\/logging\/log4j\/core).*/,
  119       '$1',
  120     );
  121 
  122     if (issue.exploitType === 'Log4Shell') {
  123       rceIssues.push(issue);
  124     }
  125     if (issue.exploitType === 'DoS') {
  126       dosIssues.push(issue);
  127     }
  128   });
  129 
  130   if (rceIssues.length > 0) {
  131     displayIssues(
  132       'A version of Log4J that is vulnerable to Log4Shell was detected:',
  133       rceIssues,
  134     );
  135     displayRemediation('Log4Shell');
  136   }
  137 
  138   if (dosIssues.length > 0) {
  139     displayIssues(
  140       'A version of Log4J that is vulnerable to CVE-2021-45105 (Denial of Service) was detected:',
  141       dosIssues,
  142     );
  143     displayRemediation('DoS');
  144   }
  145 
  146   exitWithError();
  147 }
  148 
  149 async function parsePaths(ctx: Paths, accumulator: Array<Signature>) {
  150   for (const { path, content } of ctx.paths) {
  151     if (!isArchiveOrJndi(path)) {
  152       continue;
  153     }
  154 
  155     const signature = await computeSignature(await content());
  156     const isVulnerable = signature in vulnerableSignatures;
  157 
  158     if (isVulnerable || path.includes('JndiLookup')) {
  159       await append(path, signature, accumulator);
  160       continue;
  161     }
  162 
  163     if (!isVulnerable && isJavaArchive(path)) {
  164       await parsePaths(Paths.fromZip(await content(), path), accumulator);
  165     }
  166   }
  167 }
  168 
  169 async function computeSignature(content: FileContent): Promise<Digest> {
  170   return crypto
  171     .createHash('md5')
  172     .update(content)
  173     .digest('base64')
  174     .replace(/=/g, '');
  175 }
  176 
  177 async function find(path: FilePath): Promise<FilePath[]> {
  178   const result: FilePath[] = [];
  179 
  180   await traverse(path, (filePath: string, stats: Stats) => {
  181     if (!stats.isFile() || stats.size > MAX_FILE_SIZE) {
  182       return;
  183     }
  184     result.push(filePath);
  185   });
  186 
  187   return result;
  188 }
  189 
  190 async function traverse(path: FilePath, handle: FileHandler) {
  191   try {
  192     const stats = await stat(path);
  193 
  194     if (!stats.isDirectory()) {
  195       handle(path, stats);
  196       return;
  197     }
  198 
  199     const entries = await readDir(path);
  200     for (const entry of entries) {
  201       const absolute = path + '/' + entry;
  202       await traverse(absolute, handle);
  203     }
  204   } catch (error) {
  205     errors.push(error);
  206   }
  207 }
  208 
  209 async function computeExploitType(
  210   signatureDetails: FileSignatureDetails,
  211 ): Promise<ExploitType> {
  212   for (const version of signatureDetails.versions) {
  213     const coercedVersion = semver.coerce(version);
  214 
  215     if (coercedVersion === null) {
  216       continue;
  217     }
  218 
  219     if (semver.lt(coercedVersion, '2.16.0')) {
  220       return 'Log4Shell';
  221     }
  222 
  223     if (semver.satisfies(coercedVersion, '2.16.x')) {
  224       return 'DoS';
  225     }
  226   }
  227 
  228   return 'Unknown';
  229 }
  230 
  231 function displayIssues(message: string, signatures: Signature[]) {
  232   console.log(message);
  233   signatures.forEach((signature) => {
  234     console.log(`\t${signature.path}`);
  235   });
  236 }
  237 
  238 function displayRemediation(exploitType: ExploitType) {
  239   switch (exploitType) {
  240     case 'Log4Shell':
  241       console.log(`\nWe highly recommend fixing this vulnerability. If it cannot be fixed by upgrading, see mitigation information here:
  242       \t- https://security.snyk.io/vuln/SNYK-JAVA-ORGAPACHELOGGINGLOG4J-2314720
  243       \t- https://snyk.io/blog/log4shell-remediation-cheat-sheet/\n`);
  244       break;
  245 
  246     case 'DoS':
  247       console.log(`\nWe recommend fixing this vulnerability by upgrading to a later version. To learn more about this vulnerability, see:
  248       \t- https://security.snyk.io/vuln/SNYK-JAVA-ORGAPACHELOGGINGLOG4J-2321524\n`);
  249       break;
  250 
  251     default:
  252       break;
  253   }
  254 }
  255 
  256 function isJavaArchive(path: FilePath) {
  257   return path.endsWith('.jar') || path.endsWith('.war') || path.endsWith('ear');
  258 }
  259 
  260 function isArchiveOrJndi(path: FilePath) {
  261   return (
  262     isJavaArchive(path) ||
  263     path.includes('JndiManager') ||
  264     path.includes('JndiLookup')
  265   );
  266 }
  267 
  268 async function append(
  269   path: FilePath,
  270   signature: Digest,
  271   accumulator: Array<Signature>,
  272 ): Promise<void> {
  273   const exploitType = vulnerableSignatures[signature]
  274     ? await computeExploitType(vulnerableSignatures[signature])
  275     : 'Unknown';
  276 
  277   accumulator.push({
  278     value: signature,
  279     path,
  280     exploitType,
  281   });
  282 }
  283 
  284 function filterJndi(signatures: Array<Signature>) {
  285   return signatures.filter((signature) => {
  286     if (isJavaArchive(signature.path)) {
  287       return true;
  288     }
  289 
  290     if (signature.path.includes('JndiManager')) {
  291       const jndiManagerPathIndex = signature.path.indexOf(
  292         '/net/JndiManager.class',
  293       );
  294       const jndiLookupPath =
  295         signature.path.substr(0, jndiManagerPathIndex) + '/lookup/JndiLookup';
  296 
  297       const isJndiLookupPresent = signatures.find((element) =>
  298         element.path.includes(jndiLookupPath),
  299       );
  300 
  301       return !!isJndiLookupPresent;
  302     }
  303 
  304     return false;
  305   });
  306 }
  307 
  308 function exitWithError() {
  309   const err = new Error() as any;
  310   err.code = 'VULNS';
  311 
  312   throw err;
  313 }