diff --git a/ql/src/semmle/go/dependencies/Dependencies.qll b/ql/src/semmle/go/dependencies/Dependencies.qll new file mode 100644 index 00000000000..4972b0682e2 --- /dev/null +++ b/ql/src/semmle/go/dependencies/Dependencies.qll @@ -0,0 +1,78 @@ +/** + * Provides classes for modeling go.mod dependencies. + */ + +import go +private import semmle.go.dependencies.DependencyCustomizations + +/** + * An abstract representation of a dependency. + */ +abstract class Dependency extends Locatable { + /** + * Holds if this dependency has package path `path` and version `v`. + * + * If the version cannot be determined, `v` is bound to the string + * `"unknown"`. + */ + abstract predicate info(string path, string v); + + /** Gets the package path of this dependency. */ + string getDepPath() { this.info(result, _) } + + /** Gets the version of this dependency. */ + string getDepVer() { this.info(_, result) } + + /** + * An import of this dependency. + */ + ImportSpec getAnImport() { result.getPath() = this.getDepPath() } +} + +/** + * A dependency from a go.mod file. + */ +class GoModDependency extends Dependency, GoModRequireLine { + override predicate info(string path, string v) { + this.replacementInfo(path, v) + or + not this.replacementInfo(_, _) and + this.originalInfo(path, v) + } + + /** + * Holds if there is a replace line that replaces this dependency with a dependency to `path`, + * version `v`. + */ + predicate replacementInfo(string path, string v) { + exists(GoModReplaceLine replace | + replace.getFile() = this.getFile() and + replace.getOriginalPath() = this.getPath() + | + path = replace.getReplacementPath() and + ( + v = replace.getReplacementVer() + or + not exists(replace.getReplacementVer()) and + v = "unknown" + ) + ) + } + + /** + * Get a version that was excluded for this dependency. + */ + string getAnExcludedVer() { + exists(GoModExcludeLine exclude | + exclude.getFile() = this.getFile() and + exclude.getPath() = this.getPath() + | + result = exclude.getVer() + ) + } + + /** + * Holds if this require line originally states dependency `path` had version `ver`. + */ + predicate originalInfo(string path, string v) { path = this.getPath() and v = this.getVer() } +} diff --git a/ql/src/semmle/go/dependencies/DependencyCustomizations.qll b/ql/src/semmle/go/dependencies/DependencyCustomizations.qll new file mode 100644 index 00000000000..8b137891791 --- /dev/null +++ b/ql/src/semmle/go/dependencies/DependencyCustomizations.qll @@ -0,0 +1 @@ + diff --git a/ql/src/semmle/go/dependencies/SemVer.qll b/ql/src/semmle/go/dependencies/SemVer.qll new file mode 100644 index 00000000000..a9c6161dd22 --- /dev/null +++ b/ql/src/semmle/go/dependencies/SemVer.qll @@ -0,0 +1,96 @@ +import semmle.go.dependencies.Dependencies + +/** + * A SemVer-formatted version string in a dependency. + * + * Pre-release information and build metadata is not yet supported. + */ +class DependencySemVer extends string { + Dependency dep; + string normalized; + + DependencySemVer() { + this = dep.getDepVer() and + normalized = normalizeSemver(this) + } + + /** + * Holds if this version may be before `last`. + */ + bindingset[last] + predicate maybeBefore(string last) { normalized < normalizeSemver(last) } + + /** + * Holds if this version may be after `first`. + */ + bindingset[first] + predicate maybeAfter(string first) { normalizeSemver(first) < normalized } + + /** + * Holds if this version may be between `first` (inclusive) and `last` (exclusive). + */ + bindingset[first, last] + predicate maybeBetween(string first, string last) { + normalizeSemver(first) <= normalized and + normalized < normalizeSemver(last) + } + + /** + * Holds if this version is equivalent to `other`. + */ + bindingset[other] + predicate is(string other) { normalized = normalizeSemver(other) } + + /** + * Gets the dependency that uses this string. + */ + Dependency getDependency() { result = dep } +} + +bindingset[str] +private string leftPad(string str) { result = ("000" + str).suffix(str.length()) } + +/** + * Normalizes a SemVer string such that the lexicographical ordering + * of two normalized strings is consistent with the SemVer ordering. + * + * Pre-release information and build metadata is not yet supported. + */ +bindingset[orig] +private string normalizeSemver(string orig) { + exists(string pattern, string major, string minor, string patch | + pattern = "v?(\\d+)\\.(\\d+)\\.(\\d+)(\\D.*)?" and + major = orig.regexpCapture(pattern, 1) and + minor = orig.regexpCapture(pattern, 2) and + patch = orig.regexpCapture(pattern, 3) + | + result = leftPad(major) + "." + leftPad(minor) + "." + leftPad(patch) + ) +} + +/** + * A version string in a dependency that has a SemVer, but also contains a git commit SHA. + * + * This class is useful for interacting with go.mod versions, which use SemVer, but can also contain + * SHAs if no useful tags are found, or when a user wishes to specify a commit SHA. + * + * Pre-release information and build metadata is not yet supported. + */ +class DependencySemShaVer extends DependencySemVer { + string sha; + + DependencySemShaVer() { sha = this.regexpCapture(".*-([0-9a-f]+)", 1) } + + /** + * Gets the commit SHA associated with this version. + */ + string getSha() { result = sha } + + bindingset[other] + override predicate is(string other) { + this.getSha() = other.(DependencySemShaVer).getSha() + or + not other instanceof DependencySemShaVer and + super.is(other) + } +}