All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.sap.psr.vulas.java.WarAnalyzer Maven / Gradle / Ivy

There is a newer version: 3.1.15
Show newest version
/**
 * This file is part of Eclipse Steady.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * SPDX-License-Identifier: Apache-2.0
 *
 * Copyright (c) 2018 SAP SE or an SAP affiliate company. All rights reserved.
 */
package com.sap.psr.vulas.java;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.jar.JarEntry;

import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.configuration.PropertiesConfiguration;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.sap.psr.vulas.ConstructId;
import com.sap.psr.vulas.FileAnalysisException;
import com.sap.psr.vulas.FileAnalyzer;
import com.sap.psr.vulas.core.util.CoreConfiguration;
import com.sap.psr.vulas.monitor.ClassVisitor;
import com.sap.psr.vulas.shared.connectivity.Service;
import com.sap.psr.vulas.shared.util.FileSearch;
import com.sap.psr.vulas.shared.util.VulasConfiguration;

import javassist.CannotCompileException;
import javassist.CtClass;
import javassist.NotFoundException;


/**
 * Analyzes a single Web app archive (WAR) as to identify (and potentially instrument) all its classes (in directory WEB-INF/classes),
 * as well as its JARs (in directory WEB-INF/lib).
 */
public class WarAnalyzer extends JarAnalyzer {

	private static final Log log = LogFactory.getLog(WarAnalyzer.class);
	
	private static final String INCL_SPACE = "vulas.core.instr.static.inclSpace";
	private static final String INCL_BACKEND_URL = "vulas.core.instr.static.inclBackendUrl";

	//	private static final ClassPool CLASSPOOL = ClassPool.getDefault();
	//	/**
	//	 * Adds a given URL to the classpath of the class pool. This allows maintaining dependencies needed for the compilation of instrumented classes.
	//	 * @param _url
	//	 * @throws NotFoundException
	//	 */
	//	public static void insertClasspath(String _url) throws NotFoundException { CLASSPOOL.insertClassPath(_url); }
	//	private static ClassPool getClassPool() { return WarAnalyzer.CLASSPOOL; }

	//	private static boolean OVERWRITE_ENABLED = false;
	//	public static void enableOverwrite(boolean _b) { WarAnalyzer.OVERWRITE_ENABLED = _b; }
	//	public static boolean isOverwriteEnabled() { return WarAnalyzer.OVERWRITE_ENABLED; }

	private MapinstrumentedClasses = new HashMap();
	private Path tmpDir = null; // To where the WAR is extracted
	private Path inclDir = null;
	
	private ArchiveAnalysisManager mgr = null;
	private Set nestedAnalyzers = null;

	/** The Vulas archive in incl/ will be rewritten as to contain a proper vulas configuration file. The original will
	 * then need to be skipped, i.e., not included in the instrumented WAR. */
	private Set ignoredIncludes = new HashSet();

	/** {@inheritDoc} */
	@Override
	public String[] getSupportedFileExtensions() { return new String[] { "war" }; }

	/** {@inheritDoc} */
	@Override
	public void analyze(final File _file) throws FileAnalysisException {
		try {
			super.analyze(_file);

			// Extract the WAR
			if(this.workDir!=null)
				this.tmpDir = Paths.get(this.workDir.toString(), "war_analysis");
			else {
				try {
					this.tmpDir = java.nio.file.Files.createTempDirectory("war_analysis_");
				} catch (IOException e) {
					throw new IllegalStateException("Unable to create temp directory", e);
				}
			}
			this.jarWriter.extract(this.tmpDir);

			// Add WEB-INF/classes to the classpath
			try {
				JarAnalyzer.insertClasspath(Paths.get(this.tmpDir.toString(), "WEB-INF", "classes").toString());
			} catch (Exception e) {
				// No problem at all if instrumentation is not requested.
				// If instrumentation is requested, however, some classes may not compile
				WarAnalyzer.log.error(this.toString() + ": Error while updating the classpath: " + e.getMessage());
			}			
		} catch (IllegalStateException e) {
			log.error("IllegalStateException when analyzing file [" + _file + "]: " + e.getMessage());
			throw new FileAnalysisException("Error when analyzing file [" + _file + "]: " + e.getMessage(), e);
		} catch (IOException e) {
			log.error("IOException when analyzing file [" + _file + "]: " + e.getMessage());
			throw new FileAnalysisException("Error when analyzing file [" + _file + "]: " + e.getMessage(), e);
		} catch (Exception e) {
			log.error("Exception when analyzing file [" + _file + "]: " + e.getMessage());
			throw new FileAnalysisException("Error when analyzing file [" + _file + "]: " + e.getMessage(), e);
		}
	}

	/**
	 * Sets the directory containing JARs to be included in the rewritten WAR.
	 * All JARs contained therein will be modified as well in order to set the application scope in the Vulas configuration file.
	 *
	 * @param _p a {@link java.nio.file.Path} object.
	 */
	public void setIncludeDir(Path _p) {
		this.inclDir = _p;

		// If the WAR is to be instrumented, rewrite all JARs in the inclDir in order to include the current artifact coordinates in the Vulas configuration file
		if(this.inclDir!=null && this.instrument) {
			final FileSearch vis = new FileSearch(new String[] {"jar"});
			final Set libs = vis.search(this.inclDir);
			JarWriter writer = null;
			for(Path lib: libs) {
				try {
					writer = new JarWriter(lib);
					if(writer.hasEntry("vulas-core.properties") && !writer.isRewrittenByVulas()) {

						// Rewrite the configuration file
						writer.setClassifier("and-config");
						writer.register("vulas-core.properties", this);
						writer.rewrite(this.inclDir);

						// If we reach this point, the rewriting worked and the original file can be ignored later on
						this.ignoredIncludes.add(lib);
						//Files.move(..., lib, java.nio.file.StandardCopyOption.REPLACE_EXISTING);
					}
				} catch (IOException ioe) {
					WarAnalyzer.log.error("Error when rewriting the JARs: " + ioe.getMessage());
				} catch (JarAnalysisException jae) {
					WarAnalyzer.log.error("Error when rewriting the JARs: " + jae.getMessage());
				}
			}
		}
	}

	/**
	 * {@inheritDoc}
	 *
	 * Determines whether the instrumented JAR is renamed or not. If yes, the new file name follows the following format:
	 * - If app context is provided: [originalJarName]-vulas-[appGroupId]-[appArtifactId]-[appVersion].jar
	 * - Otherwise: [originalJarName]-vulas.jar
	 */
	public void setRename(boolean _b) { this.rename = _b; }

	/**
	 * {@inheritDoc}
	 *
	 * See here: http://docs.oracle.com/javase/7/docs/technotes/guides/jar/jar.html
	 */
	@Override
	protected void createInstrumentedArchive() throws JarAnalysisException {

		// Additional manifest file entries
		this.jarWriter.addManifestEntry("VULAS-classInstrStats", "[" + this.classCount + " total, " + this.instrControl.countClassesInstrumentedAlready() + " existed, " + this.instrControl.countClassesInstrumentedSuccess() + " ok, " + this.instrControl.countClassesInstrumentedFailure() + " err]");
		this.jarWriter.addManifestEntry("VULAS-constructStats", "[" + constructs.size() + " constructs]");
		if(JarAnalyzer.getAppContext()!=null)
			this.jarWriter.addManifestEntry("VULAS-appContext", JarAnalyzer.getAppContext().getMvnGroup() + ":" +
					JarAnalyzer.getAppContext().getArtifact() + ":" +
					JarAnalyzer.getAppContext().getVersion());

		// Register this WarAnalyzer for callbacks
		this.jarWriter.register("^WEB-INF/classes/.*.class$", this);
		this.jarWriter.register("^WEB-INF/lib/.*.jar$", this);

		// Additional files to be added to the WAR
		if(this.inclDir!=null && this.inclDir.toFile().exists()) {

			// Include all JARs in inclDir in WEB-INF/lib
			final FileSearch vis = new FileSearch(new String[] {"jar"});
			final Set libs = vis.search(this.inclDir);
			WarAnalyzer.log.info(this + ": Dir [" + this.inclDir + "] includes another [" + libs.size() + "] JARs, to be included in WEB-INF/lib");

			// Add all files in incl (except the one(s) rewritten to contain a proper vulas-cfg.properties)
			//this.jarWriter.addFiles("WEB-INF/lib", libs, true);
			for(Path l: libs) {
				if(this.ignoredIncludes.contains(l))
					continue;
				else
					this.jarWriter.addFile("WEB-INF/lib", l, true);
			}

			// Add configuration files
			this.jarWriter.addFile("WEB-INF/classes/", Paths.get(this.inclDir.toString(), "vulas-cfg.properties"), true);
			this.jarWriter.addFile("WEB-INF/classes/", Paths.get(this.inclDir.toString(), "vulas-cfg.xml"), true);
			this.jarWriter.addFile("WEB-INF/classes/", Paths.get(this.inclDir.toString(), "log4j.properties"), false);
		}		

		// Rename
		if(this.rename)
			this.jarWriter.setClassifier("vulas-instr");

		// Rewrite
		this.jarWriter.rewrite(this.workDir);

		// Stats
		this.instrControl.logStatistics();
	}	

	/** {@inheritDoc} */
	@Override
	public boolean hasChilds() {
		return this.getChilds(true)!=null && !this.getChilds(true).isEmpty();
	}
	
	/** {@inheritDoc} */
	@Override
	public Set getChilds(boolean _recursive) {
		if(this.mgr==null) {
			this.mgr = new ArchiveAnalysisManager(4, -1, this.instrument, JarAnalyzer.getAppContext());
	
			// Create a lib_mod folder for instrumented JARs of the WAR
			if(this.instrument) {
				final Path lib_mod = Paths.get(this.tmpDir.toString(), "WEB-INF", "lib_mod");
				if(!lib_mod.toFile().exists()) {
					try {
						Files.createDirectory(lib_mod);
					} catch (IOException e) {
						log.error("Cannot create directory [" + lib_mod.toAbsolutePath() + "]: " + e.getMessage(), e);
					}
				}
	
				if(lib_mod.toFile().exists()) {
					mgr.setWorkDir(lib_mod);
				}
				else {
					log.warn("Instrumentation disabled");
					mgr.setInstrument(false);
				}
			}
	
			// Find and analyze all JARs
			final Set jars = new FileSearch(new String[] {"jar"}).search(Paths.get(this.tmpDir.toString(), "WEB-INF", "lib"));
			mgr.startAnalysis(jars, this);
			this.nestedAnalyzers = new HashSet();
			this.nestedAnalyzers.addAll(mgr.getAnalyzers());
		}
		return this.nestedAnalyzers;
	}

	/**
	 * {@inheritDoc}
	 *
	 * Identifies all {@link ConstructId}s of all methods and constructors contained in the WAR file.
	 * Returns true if the WAR has classes in WEB-INF/classes, false otherwise. Note that classes in libraries contained in WEB-INF/lib are ignored.
	 */
	@Override
	public synchronized Set getConstructIds() {
		if(this.constructs==null) {			
			this.constructs = new TreeSet();
			// Loop all *.class files in WEB-INF/classes
			final Set class_names = new HashSet();
			String class_name = null;
			final Set class_files = new FileSearch(new String[] {"class"}).search(Paths.get(this.tmpDir.toString(), "WEB-INF", "classes"));
			for(Path p: class_files) {
				class_name = p.toString();
				class_name = class_name.substring(0, class_name.length()-6); // ".class"
				class_name = class_name.substring(class_name.indexOf("WEB-INF")+16); // "WEB-INF/classes/"
				class_name = class_name.replace(File.separatorChar, '.');
				class_names.add(class_name);				
				WarAnalyzer.log.debug("Found [" + class_name + "]");
			}

			// Visit all classes using Javassist (and instrument as many as possible - if requested)
			CtClass ctclass = null;
			ClassVisitor cv = null;

			for(String cn: class_names) {
				try {
					ctclass = JarAnalyzer.getClassPool().get(cn);

					// Ignore interfaces (no executable code) and enums (rarely containing executable code, perhaps to be included later on)
					if(ctclass.isInterface()) {
						this.interfaceCount++;
					}
					else {

						if(ctclass.isEnum())
							this.enumCount++;
						else 
							this.classCount++;

						// Create ClassVisitor for the current Java class
						cv = new ClassVisitor(ctclass);
						this.constructs.addAll(cv.getConstructs());

						// Instrument (if requested and not blacklisted)
						if(this.instrument && !this.instrControl.isBlacklistedClass(cn)) {
							cv.setOriginalArchiveDigest(this.getSHA1());
							cv.setAppContext(WarAnalyzer.getAppContext());
							if(cv.isInstrumented())
								this.instrControl.updateInstrumentationStatistics(cv.getJavaId(), null);
							else {
								try {
									cv.visitMethods(true);
									cv.visitConstructors(true);
									cv.finalizeInstrumentation();
									this.instrumentedClasses.put(cv.getJavaId(), cv);
									this.instrControl.updateInstrumentationStatistics(cv.getJavaId(), new Boolean(true));
								} catch (IOException ioe) {
									WarAnalyzer.log.error("I/O exception while instrumenting class [" + cv.getJavaId().getQualifiedName() + "]: " + ioe.getMessage());
									this.instrControl.updateInstrumentationStatistics(cv.getJavaId(), new Boolean(false));
								} catch (CannotCompileException cce) {
									WarAnalyzer.log.warn("Cannot compile instrumented class [" + cv.getJavaId().getQualifiedName() + "]: " + cce.getMessage());
									this.instrControl.updateInstrumentationStatistics(cv.getJavaId(), new Boolean(false));
								} catch (Exception e) {
									WarAnalyzer.log.error(e.getClass().getName() + " occured while instrumenting class [" + cv.getJavaId().getQualifiedName() + "]: " + e.getMessage());
									this.instrControl.updateInstrumentationStatistics(cv.getJavaId(), new Boolean(false));
								}
							}
						}						
					}
					if(!this.instrument){
						//only detach if no static instrumentation (otherwise it will fail because the class was modified)
						// in case the instrumentation is performed the detach is done in ClassVisitor.finalizeInstrumentation
						ctclass.detach();
					}
				} catch (NotFoundException nfe) {
					WarAnalyzer.log.error("Error while analyzing class [" + cv.getJavaId().getQualifiedName() + "]: " + nfe.getMessage());
					continue;
				} catch (RuntimeException re) {
					WarAnalyzer.log.error("Error while analyzing class [" + ctclass.getName() + "]: " + re.getMessage());
					continue;
				}
			}
			if(this.instrument)
				WarAnalyzer.log.info(this.toString() + ": classes comprised/already-instr/instr/not-instr [" + this.classCount + "/" + this.instrControl.countClassesInstrumentedAlready() + "/" + this.instrControl.countClassesInstrumentedSuccess() + "/" + this.instrControl.countClassesInstrumentedFailure() + "], constructs comprised [" + constructs.size() + "]");
			else
				WarAnalyzer.log.info(this.toString() + ": constructs comprised [" + constructs.size() + "], classes [" + this.classCount + "], enums [" + enumCount + "], interfaces (ignored) [" + interfaceCount + "]");
		}
		return this.constructs;
	}

	/**
	 * {@inheritDoc}
	 *
	 * In case the archive is rewritten, this method is used to rewrite certain {@link JarEntry}s
	 * (rather than taking the file from the original archive).
	 * The callback registration takes place in {@link #createInstrumentedArchive()}.
	 */
	@Override
	public InputStream getInputStream(String _regex, JarEntry _entry) {
		InputStream is = null;

		// Called during rewrite of classes
		if(_regex.equals("^WEB-INF/classes/.*.class$")) {
			JavaId jid = null;
			// Create JavaId from entry name
			try {
				String class_name = _entry.getName();
				class_name = class_name.substring(0, class_name.length()-6); // ".class"
				class_name = class_name.substring(16); // "WEB-INF/classes/"
				class_name = class_name.replace('/', '.');
				jid = JavaId.parseClassQName(class_name);
			} catch (Exception e) {
				WarAnalyzer.log.error("Cannot parse Java Id from Jar Entry [" + _entry.getName() + "]: " + e.getMessage());
				jid = null;
			}

			// Create input stream
			if(jid!=null && this.instrumentedClasses.get(jid)!=null) {
				//new_entry.setSize(this.instrumentedClasses.get(jid).getBytecode().length);
				is = new ByteArrayInputStream(this.instrumentedClasses.get(jid).getBytecode());
			}
		}
		// Called during rewrite of WAR
		else if(_regex.equals("^WEB-INF/lib/.*.jar$")) {
			JarAnalyzer ja = null;
			try {
				final String[] path_elements = _entry.getName().split("/");
				final Path p = Paths.get(path_elements[0], path_elements[1], path_elements[2]); // Assumming: WEB-INF/lib/xyz.jar
				ja = mgr.getAnalyzerForSubpath(p);
				if(ja!=null) {
					final File f = ja.getInstrumentedArchive();
					is = new FileInputStream(f);
				} else {
					WarAnalyzer.log.warn("Cannot find JarAnalyzer for path [" + p + "]");
				}
			} catch (Exception e) {
				WarAnalyzer.log.error("Cannot find rewritten JAR file from [" + ja + "]: " + e.getMessage());
			}	
		}
		// Called during the rewrite of the JARs in inclDir
		else if(_regex.equals("vulas-core.properties")) {
			Path tmp_file = null;
			try {
				final PropertiesConfiguration cfg = new PropertiesConfiguration("vulas-core.properties");
				cfg.setProperty(CoreConfiguration.APP_CTX_GROUP, JarAnalyzer.getAppContext().getMvnGroup());
				cfg.setProperty(CoreConfiguration.APP_CTX_ARTIF, JarAnalyzer.getAppContext().getArtifact());
				cfg.setProperty(CoreConfiguration.APP_CTX_VERSI, JarAnalyzer.getAppContext().getVersion());
				
				// Include space
				if(VulasConfiguration.getGlobal().getConfiguration().getBoolean(INCL_SPACE, true) && VulasConfiguration.getGlobal().getConfiguration().containsKey(CoreConfiguration.SPACE_TOKEN))
					cfg.setProperty(CoreConfiguration.SPACE_TOKEN, VulasConfiguration.getGlobal().getConfiguration().getString(CoreConfiguration.SPACE_TOKEN));
				
				// Include backend URL
				if(VulasConfiguration.getGlobal().getConfiguration().getBoolean(INCL_BACKEND_URL, true) && VulasConfiguration.getGlobal().getConfiguration().containsKey(VulasConfiguration.getServiceUrlKey(Service.BACKEND)))
					cfg.setProperty(VulasConfiguration.getServiceUrlKey(Service.BACKEND), VulasConfiguration.getGlobal().getConfiguration().getString(VulasConfiguration.getServiceUrlKey(Service.BACKEND)));
				
				tmp_file = Files.createTempFile("vulas-core-", ".properties");
				cfg.save(tmp_file.toFile());
				is = new FileInputStream(tmp_file.toFile());						
			}
			catch(ConfigurationException ce) {
				WarAnalyzer.log.error("Error when loading configuration from 'vulas-core.properties': " + ce.getMessage());
			}
			catch(IOException ioe) {
				WarAnalyzer.log.error("Error when creating/reading temporary configuration file [" + tmp_file + "]: " + ioe.getMessage());
			}
		}

		return is;		
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy