/** * Provides classes and predicates for working with Maven POM files and their content. */ overlay[local?] module; import XML /** * Normalize an absolute path, replacing all ".." and "." components. */ bindingset[path] private string normalize(string path) { result = path.regexpReplaceAll("/\\.(/|$)", "/").regexpReplaceAll("/[^/]*/\\.\\.(/|$)", "/") } /** * An XML element that provides convenience access methods * to retrieve child XML elements named "groupId", "artifactId" * and "version", typically contained in Maven POM XML files. */ class ProtoPom extends XmlElement { /** Gets a child XML element named "groupId". */ Group getGroup() { result = this.getAChild() } /** Gets a child XML element named "artifactId". */ Artifact getArtifact() { result = this.getAChild() } /** Gets a child XML element named "version". */ Version getVersion() { result = this.getAChild() } /** * Gets a string representing the version, or an empty string if no `version` * tag was provided. */ string getVersionString() { if exists(this.getVersion().getValue()) then result = this.getVersion().getValue() else result = "" } /** Gets a Maven coordinate of the form `groupId:artifactId`. */ string getShortCoordinate() { result = this.getGroup().getValue() + ":" + this.getArtifact().getValue() } } /** * An XML element named "project", with convenience access methods * to retrieve child XML elements named "version", "name" and "dependencies", * typically found at the top-level of Maven POM XML files. * * Access to child XML elements named "groupId" and "artifactId" is provided * via inherited methods from the super-class. */ class Pom extends ProtoPom { Pom() { this.getName() = "project" and // Ignore "dependency-reduced-pom" files - these are generated by the // Maven Shade Plugin, and duplicate existing POM files. this.getFile().getStem() != "dependency-reduced-pom" } override Group getGroup() { // For a project element, the group may be defined in the parent tags instead if not exists(super.getGroup()) then exists(Parent p | p = this.getAChild() and result = p.getAChild()) else result = super.getGroup() } /** Gets a Maven coordinate of the form `groupId:artifactId:version`. */ string getCoordinate() { result = this.getGroup().getValue() + ":" + this.getArtifact().getValue() + ":" + this.getVersion().getValue() } /** Gets a child XML element named "name". */ Named getNamed() { result = this.getAChild() } /** Gets a child XML element named "dependencies". */ Dependencies getDependencies() { result = this.getAChild() } /** Gets a child XML element named "dependencyManagement". */ DependencyManagement getDependencyManagement() { result = this.getAChild() } /** Gets a Dependency element for this POM. */ Dependency getADependency() { result = this.getAChild().(Dependencies).getADependency() } /** * Gets a property defined in the `` section of this POM. */ PomProperty getALocalProperty() { result = this.getAChild().(PomProperties).getAProperty() } /** * Gets a property value defined for this project, either in a local `` section, or * in the `` section of an ancestor POM. */ PomProperty getAProperty() { result = this.getALocalProperty() or result = this.getParentPom().getAProperty() and not this.getALocalProperty().getName() = result.getName() } /** * Gets a property value defined for this project with the given name, either in a local * `` section, or in the `` section of an ancestor POM. */ PomProperty getProperty(string name) { result.getName() = name and result = this.getAProperty() } /** * Gets a "project property" - for example, the groupId, name or packaging. */ PomElement getProjectProperty() { ( // It must either be a child of the POM, or a child of the parent node of the POM result = this.getAChild() or result = this.getParentPom().getAChild() and // The parent project property is not shadowed by a local project property not exists(PomElement p | p = this.getAChild() and p.getName() = result.getName()) ) and // Can't be a property if it has children of its own not exists(result.getAChild()) } /** * Resolve the given placeholder (if possible) in the static context of this POM. Resolution * occurs by considering the properties defined by this project or an ancestor project. */ string resolvePlaceholder(string name) { if name.matches("project.%") then exists(PomElement p | p = this.getProjectProperty() and "project." + p.getName() = name and result = p.getValue() ) else exists(PomProperty prop | prop = this.getAProperty() and prop.getName() = name and result = prop.getValue() ) } /** * Gets all the dependencies that are exported by this POM. An exported dependency is one that * is transitively available, i.e. one with scope "compile". */ Dependency getAnExportedDependency() { result = this.getADependency() and result.getScope() = "compile" } /** * Gets a POM dependency that is exported by this POM. An exported dependency is one that * is transitively available, i.e. one with scope "compile". */ Pom getAnExportedPom() { result = this.getAnExportedDependency().getPom() } /** * Gets the `` element of this POM, if any. */ Parent getParentElement() { result = this.getAChild() } /** * Gets the POM referred to by the `` element of this POM, if any. */ Pom getParentPom() { result = this.getParentElement().getPom() } /** * Gets the version specified for dependency `dep` in a `dependencyManagement` * section in this POM or one of its ancestors, or an empty string if no version * is specified. */ string getVersionStringForDependency(Dependency dep) { if exists(this.getDependencyManagement().getDependency(dep)) then result = this.getDependencyManagement().getDependency(dep).getVersionString() else if exists(this.getParentPom()) then result = this.getParentPom().getVersionStringForDependency(dep) else result = "" } /** * Gets the folder considered to be the source directory for this POM, if present in the analyzed * snapshot. * * If the `` property is set, the value will be used relative to the directory * containing this POM. */ Folder getSourceDirectory() { exists(string relativePath | if exists(this.getProperty("sourceDirectory")) then // A custom source directory has been specified. relativePath = this.getProperty("sourceDirectory").getValue() else // The Maven default source directory. relativePath = "src" | // Resolve the relative path against the base directory for this POM result.getAbsolutePath() = normalize(this.getFile().getParentContainer().getAbsolutePath() + "/" + relativePath) ) } /** * Gets a `RefType` contained in the source directory. */ RefType getASourceRefType() { result.getFile().getParentContainer*() = this.getSourceDirectory() } } /** * An XML element named "dependency", as found in Maven POM XML files. * * Access to child XML elements named "groupId" and "artifactId" is provided * via inherited methods from the super-class. */ class Dependency extends ProtoPom { Dependency() { this.getName() = "dependency" } /** * Gets an XML element with the same Maven short coordinate * (of the form `groupId:artifactId`) as this element. */ Pom getPom() { result.getShortCoordinate() = this.getShortCoordinate() } /** * Gets the jar file that Maven likely resolved this dependency to (if any). * See `MavenRepo.getAnArtifact(ProtoPom)` for how this match is determined. */ File getJar() { exists(MavenRepo mr | result = mr.getAnArtifact(this)) } /** * Gets the scope of this dependency. If the `scope` tag is present, this will * be the string contents of that tag, otherwise it defaults to "compile". */ string getScope() { if exists(this.getAChild().(Scope)) then exists(Scope s | s = this.getAChild() and result = s.getValue()) else result = "compile" } override string getVersionString() { if exists(this.getVersion()) then result = super.getVersionString() else if exists(Pom p | this = p.getADependency()) then exists(Pom p | this = p.getADependency() | result = p.getVersionStringForDependency(this)) else result = "" } } /** * A Maven dependency element that represents an actual dependency from a given POM project. */ class PomDependency extends Dependency { PomDependency() { exists(Pom source | // This dependency must be a dependency of a POM - dependency tags can also appear in the // dependencyManagement section, where they do not directly contribute to the dependencies of // the containing POM. source.getADependency() = this and // Consider dependencies that can be used at compile time. ( this.getScope() = "compile" or // Provided dependencies are like compile time dependencies except (a) they are not packaged // when creating the jar and (b) they are not transitive. this.getScope() = "provided" // We ignore "test" dependencies because they can be runtime or compile time dependencies ) ) } } /** * An XML element that provides access to its value string * in the context of Maven POM XML files. */ class PomElement extends XmlElement { /** * Gets the value associated with this element. If the value contains a placeholder only, it will be resolved. */ string getValue() { exists(string s | s = this.allCharactersString() and if s.matches("${%") then // Resolve the placeholder in the parent POM result = this.getParent*().(Pom).resolvePlaceholder(s.substring(2, s.length() - 1)) else result = s ) } } /** An XML element named "groupId", as found in Maven POM XML files. */ class Group extends PomElement { Group() { this.getName() = "groupId" } } /** An XML element named "artifactId", as found in Maven POM XML files. */ class Artifact extends PomElement { Artifact() { this.getName() = "artifactId" } } /** An XML element named "parent", as found in Maven POM XML files. */ class Parent extends ProtoPom { Parent() { this.getName() = "parent" } Pom getPom() { result.getShortCoordinate() = this.getShortCoordinate() } } /** An XML element named "version", as found in Maven POM XML files. */ class Version extends PomElement { Version() { this.getName() = "version" } } /** An XML element named "name", as found in Maven POM XML files. */ class Named extends PomElement { Named() { this.getName() = "name" } } /** An XML element named "scope", as found in Maven POM XML files. */ class Scope extends PomElement { Scope() { this.getName() = "scope" } } /** An XML element named "dependencies", as found in Maven POM XML files. */ class Dependencies extends PomElement { Dependencies() { this.getName() = "dependencies" } Dependency getADependency() { result = this.getAChild() } } /** An XML element named "dependencyManagement", as found in Maven POM XML files. */ class DependencyManagement extends PomElement { DependencyManagement() { this.getName() = "dependencyManagement" } Dependencies getDependencies() { result = this.getAChild() } Dependency getADependency() { result = this.getDependencies().getADependency() } /** * Gets a dependency declared in this `dependencyManagement` element that has * the same (short) coordinates as `dep`. */ Dependency getDependency(Dependency dep) { result = this.getADependency() and result.getShortCoordinate() = dep.getShortCoordinate() } } /** * An XML element named "properties", as found in Maven POM XML files. */ class PomProperties extends PomElement { PomProperties() { this.getName() = "properties" } PomProperty getAProperty() { result = this.getAChild() } } /** * An XML element that is the child of a PomProperties element, as found in Maven POM XML files. * Represents a single property. */ class PomProperty extends PomElement { PomProperty() { this.getParent() instanceof PomProperties } } /** * An XML element representing any kind of repository declared inside of a Maven POM XML file. */ class DeclaredRepository extends PomElement { DeclaredRepository() { this.getName() = ["repository", "snapshotRepository", "pluginRepository"] } /** * Gets the url for this repository. If the `url` tag is present, this will * be the string contents of that tag. */ string getRepositoryUrl() { result = this.getAChild("url").(PomElement).getValue() } /** * Holds if this repository is disabled in both the `releases` and `snapshots` policies. */ predicate isDisabled() { forex(PomElement policy | policy = this.getAChild(["releases", "snapshots"]) | policy.getAChild("enabled").(PomElement).getValue() = "false" ) } } /** * A folder that represents a local Maven repository using the standard layout. Any folder called * "repository" with a parent name ".m2" is considered to be a Maven repository. */ class MavenRepo extends Folder { MavenRepo() { this.getBaseName() = "repository" and this.getParentContainer().getBaseName() = ".m2" } /** * Gets a Jar file contained within this repository. */ File getAJarFile() { result = this.getAChildContainer*() and result.getExtension() = "jar" } /** * Gets any jar artifacts in this repository that match the POM project definition. This is an * over approximation. For soft qualifiers (e.g. 1.0) precise matches are returned in preference * to artifact-only matches. For hard qualifiers (e.g. [1.0]) only precise matches are returned. * For all other qualifiers, all matches are returned regardless of version. */ MavenRepoJar getAnArtifact(ProtoPom pom) { result = this.getAJarFile() and if exists(MavenRepoJar mrj | mrj.preciseMatch(pom)) or versionHardMatch(pom) then // Either a hard match qualifier, or soft and there is at least one precise match result.preciseMatch(pom) else result.artifactMatches(pom) } } /** * If this POM has a version string representing a "hard" match */ private predicate versionHardMatch(ProtoPom pom) { pom.getVersionString().regexpMatch("^\\[[^,\\[]*\\]$") } /** * A jar file inside a Maven repository. * * See: https://cwiki.apache.org/confluence/display/MAVENOLD/Repository+Layout+-+Final */ class MavenRepoJar extends File { MavenRepoJar() { exists(MavenRepo mr | mr.getAJarFile() = this) } /** * Gets the `groupId` of this jar. */ string getGroupId() { exists(MavenRepo mr | mr.getAJarFile() = this | // Assuming the standard layout, the first part of the directory structure from the Maven // repository will be the groupId converted to a path by replacing "." with "/". result = this.getParentContainer() .getParentContainer() .getParentContainer() .getAbsolutePath() .suffix(mr.getAbsolutePath().length() + 1) .replaceAll("/", ".") ) } /** * Gets the `artifactId` of this jar. */ string getArtifactId() { result = this.getParentContainer().getParentContainer().getBaseName() } /** * Gets the artifact version string of this jar. */ string getVersion() { result = this.getParentContainer().getBaseName() } /** * Holds if this jar is an artifact for the given POM or dependency, regardless of which version it is. */ predicate artifactMatches(ProtoPom pom) { pom.getGroup().getValue() = this.getGroupId() and pom.getArtifact().getValue() = this.getArtifactId() } /** * Holds if this jar is both an artifact for the POM, and has a version string that matches the POM * version string. Only soft and hard version matches are supported. */ predicate preciseMatch(ProtoPom pom) { this.artifactMatches(pom) and if versionHardMatch(pom) then ("[" + this.getVersion() + "]").matches(pom.getVersionString() + "%") else this.getVersion().matches(pom.getVersionString() + "%") } }