Überprüfung von Java-Abhängigkeiten: Maven, Gradle, Ant, Jar-Dateien
In diesem Artikel beleuchten wir, wie Java Abhängigkeiten Überprüfung werden können, insbesondere, wenn die Abhängigkeiten über Maven, Gradle, Ant oder als plain Jar-Datein hinzugefügt werden.
Grundlagen
Im Java-Umfeld gibt es eine Vielzahl an Möglichkeiten, Abhängigkeiten zu verwalten. Im Gegensatz zu etwa npm gibt es kein Standard-Tool, aber v.a. für Enterprise-Projekte wird meist Maven verwendet.
Da viele Java-Projekte historisch gewachsen sind, kann es sein, dass Abhängigkeiten nicht konsistent verwaltet werden, sondern mehrere Tools im Einsatz sind. Um sicherzustellen, alle Abhängigkeiten zu überprüfen, sollten deshalb alle untersucht werden.
Im Folgenden besprechen wir Beispiel-Code, der den Einsatz verwandter Tools erkennt und uns möglichst viel Arbeit bei der Auflistung und Prüfung der Lizenzen abnimmt.
Dabei untersuchen wir die folgenden Arten, Abhängigkeiten in Java zu verwalten:
- Maven: am modernsten und weitesten verbreitet, Abhängigkeiten definiert in
pom.xml
und Lizenzen größtenteils automatisch abrufbar - Gradle: Build-Automatisierung, Definition in mehreren Programmiersprachen möglich, oft Groovy, Abhängigkeiten größtenteils in
.gradle
Dateien definiert und in Maven-Repository auffindbar bzw. als.jar
Dateien vorhanden (siehe Ant) - Ant/Jar-Dateien: älteres Tool für Build-Automatisierung, Abhängigkeiten sind größtenteils nirgends definiert, sondern als
.jar
Dateien vorhanden; am schwierigsten die Lizenz zu prüfen, da Dateien oft nicht so benannt sind, dass sie automatisch zugeordnet werden können.
Alternativ könnte natürlich auch externer Source-Code eingecheckt sein und beim Build mit berücksichtigt werden. Es sollte daher immer mit den Entwicklern gesprochen werden, wie genau Code dritter eingebunden ist.
Maven
Für Projekte, die Maven nutzen, können wir mit mvn site
eine Übersicht der Abhängigkeiten inklusive Lizenzen als HTML-Datei generieren.
Das folgende Bash-Skript sucht dabei zuerst Projekte, die Maven nutzen anhand ihrer pom.xml
Dateien.
Für jedes der Projekte wird daraufhin in einem Docker-Container mvn site
aufgerufen, welches die gewünschten dependencies.html
erzeugt.
# Dependencies: https://github.com/sharkdp/fd, Docker
echo "> Projects using Maven:"
fd pom.xml | sort
echo
echo "> Running \`mvn site\` for each Maven project"
mkdir -p licenses
docker volume create --name maven-repo 1>/dev/null
root=`pwd`
for f in $(fd pom.xml | sort); do
p=$(dirname $f);
echo
echo "$p"
echo "------------------------------------------"
skip=false
if [[ -d "licenses/$p" && "licenses/$p" -nt "$p" ]]; then
echo "licenes/$p is newer than $p -> skipping \`mvn site\`"
skip=true
fi
cd "$p"
if [ "$skip" != true ]; then
docker run -it --rm -v maven-repo:/root/.m2 -v "$(pwd)":/usr/src/mymaven -w /usr/src/mymaven maven mvn site
mkdir -p "$root/licenses/$p"
cp -r target/site "$root/licenses/$p"
fi
echo "Licenses: $(pwd)/target/site/dependencies.html"
echo ".jar files under version control despite Maven:"
fd '.jar' | sort
cd "$root"
done
Um die IDs der Abhängigkeiten direkt aus den XML-Dateien aufzulisten (1. String, 2. JSON), können folgende Befehle verwendet werden:
# Dependencies: https://github.com/TomWright/dasel, https://github.com/jqlang/jq
dasel -f pom.xml -r xml 'project.dependencies.dependency.all().join(.,groupId,artifactId,string( ))'
dasel -f pom.xml -r xml -w json | jq '.project.dependencies[] | map([.groupId, .artifactId] | join("."))'
Gradle
Für Projekte, die Gradle nutzen, suchen wir nach .gradle
Dateien, in diesen Dateien nach Abhängigkeits-Definitionen via library
und anhand dieser bei mvnrepository.com nach der Lizenz.
Da es sich bei der Zuordnung der Lizenzen über ihre Namen um eine Heuristik handelt, sollten die Zuordnung für jede Abhängigkeit manuell verifiziert werden.
Um dies zu erleichtern, berechnen wir zudem die Edit-Distance (wie unterschiedlich ist der Name lokal und der online) und färben Abhängigkeiten entsprechend gelb oder rot.
Da all dies nur schwer verständlich in einem Bash-Skript umsetzbar wäre, nutzen wir Google zx, welches sich gut eignet, um einfacher Shell-Befehle in JavaScript zu nutzen.
Zuerst wird eine Funktion definiert, um die Levenshtein-Distance (Edit-Distance) zweier Strings zu berechnen.
Um bei mehreren Aufrufen zu viele Anfragen an mvnrepository.com zu vermeiden, nutzen wir eine Datei maven.json
als Cache in dem wir die Antworten speichern. Diese kann bei Bedarf gelöscht werden, um die Anfragen erneut auszuführen.
Wir übergeben den Namen der Abhängigkeit als Query-String und nutzen pup, um die HTML-Antwort zu parsen und uns anhand von Selektoren die relevanten Daten wie u.a. die Lizenz auszugeben.
Die Ausführung beginnt mit der Auflistung aller .gradle
Dateien, der darin definierten Abhängigkeiten library
und deren version
.
Da in Gradle-Dateien oftmals mittels versionRef
die verssion
einer anderen library
referenziert wird, speichern wir uns diese Referenzen und lösen sie auf.
Als Ausgabe speichern wir in gradle-paths.json
die Zuordnung, welche Abhängigkeit in welcher .gradle
Datei definiert wurde. Die Abhängigkeiten werden daraufhin online gesucht und die Ergebnisse inklusive Edit-Distance auf der Konsole ausgegeben und in einer Datei gradle-licenses.csv
gespeichert.
Als Zusammenfassung folgt noch die Zuordnung von Lizenz nach Liste der Abhängigkeiten mit der entsprechenden Lizenz.
#!/usr/bin/env zx
// Dependencies: https://github.com/google/zx, https://github.com/sharkdp/fd, https://github.com/ericchiang/pup, https://github.com/ggreer/the_silver_searcher
$.verbose = false
// Headers in blue
const h = s => echo(chalk.blue(`> ${s}`))
// Edit distance: how different are two strings?
const levenshteinDistance = (str1 = '', str2 = '') => {
const track = Array(str2.length + 1).fill(null).map(() =>
Array(str1.length + 1).fill(null));
for (let i = 0; i <= str1.length; i += 1) {
track[0][i] = i;
}
for (let j = 0; j <= str2.length; j += 1) {
track[j][0] = j;
}
for (let j = 1; j <= str2.length; j += 1) {
for (let i = 1; i <= str1.length; i += 1) {
const indicator = str1[i - 1] === str2[j - 1] ? 0 : 1;
track[j][i] = Math.min(
track[j][i - 1] + 1, // deletion
track[j - 1][i] + 1, // insertion
track[j - 1][i - 1] + indicator, // substitution
);
}
}
return track[str2.length][str1.length];
}
// avoid making too many requests to the mvnrepository.com
const cache = fs.readJsonSync('./maven.json', {throws: false}) || {}
// console.log('cache:', cache)
const query = async q => {
if (q in cache) return cache[q]
const req = $`curl -s "https://mvnrepository.com/search?q=${q}" -H 'user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36' --compressed`
const fst = await req.pipe($`pup '.im:nth-of-type(2)'`)
if (!fst.stdout) return null
const pup = async (s, p='text{}') => (await $`echo ${fst} | pup ${s + ' ' + p}`).toString().trim()
const title = await pup('.im-title a:first-of-type')
const groupId = await pup('.im-subtitle a:first-of-type')
const artifactId = await pup('.im-subtitle a:nth-of-type(2)')
const artifactUrl = 'https://mvnrepository.com' + await pup('.im-subtitle a:nth-of-type(2)', 'attr{href}')
const license = await pup('.im-subtitle span')
const versions = (await $`curl -s "${artifactUrl}" -H 'user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36' --compressed`.pipe($`pup 'table.versions tr > td:first-of-type > div text{}'`)).toString().trim().replaceAll('\n.', '.').split('\n')
const r = {title, groupId, artifactId, license, artifactUrl, versions}
// console.log(r)
cache[q] = r
fs.outputJsonSync('./maven.json', cache, {spaces: 2})
return r
}
h('Projects using Gradle:')
const projects = await $`fd '.gradle$' | sort`
echo(projects)
echo ``
const gradle_files = await $`ag -l -G '.*\\.gradle' 'library\\(|version\\('`
h('Files containing library versions:')
echo(gradle_files)
echo ``
h('Library versions:')
echo(await $`ag -H --color -G '.*\\.gradle' 'library\\(|version\\('`)
const m = {} // dep -> paths
for (const f of gradle_files.stdout.trim().split('\n')) {
echo ``
echo(chalk.green(f))
const libs = (await $`ag --nonumbers 'library\\(|version\\(' ${f}`).stdout.trim().split('\n').map(x => x.trim()).filter(x => !x.startsWith('//'))
console.log(libs)
for (const lib of libs) {
const m1 = lib.match(/version\('(.+)', '(.+)'\)/)
const m2 = lib.match(/library\((.+)\)\.version\('(.+)'\)/)
const m3 = lib.match(/library\((.+)\)\.versionRef\('(.+)'\)/)
const parseList = s => s.split(', ').map(x => x.replaceAll("'", ''))
const save = (xs, v) => {
const x = xs.join('/')
m[x] ??= {}
m[x].version = v
m[x].paths ??= []
if (!m[x].paths.includes(f)) m[x].paths.push(f)
}
if (m1 && m1.length == 3) {
console.log('version', m1[1], m1[2])
save([m1[1]], m1[2])
} else if (m2 && m2.length == 3) {
console.log('library.version', parseList(m2[1]), m2[2])
save(parseList(m2[1]), m2[2])
} else if (m3 && m3.length == 3) {
console.log('library.versionRef', parseList(m3[1]), m3[2])
save(parseList(m3[1]), m[m3[2]].version)
}
}
}
console.log(m)
fs.outputJsonSync('./gradle-paths.json', m, {spaces: 2})
echo ``
const l = {} // license -> deps
const csv = []
h('Checking licenses of grouped libraries:')
for (const q in m) {
echo ``
console.log(q, m[q].version, m[q].paths)
const qs = q.split('/').reverse()[0]
echo `https://mvnrepository.com/search?q=${qs}`
const maven = await query(qs)
console.log(maven || chalk.red('No results!'))
m[q].maven = maven
const license = maven?.license || 'Unknown'
l[license] ??= []
l[license].push(q)
const ed = maven ? Math.round(levenshteinDistance(q, maven.artifactId)/Math.max(q.length, maven.artifactId.length)*100) : -1
m[q].editDistance = ed
console.log((ed == 0 ? chalk.yellow : chalk.red)('Edit distance:', m[q].editDistance))
csv.push([license, ed, q, m[q].version, m[q].paths, JSON.stringify(maven)])
echo ``
}
fs.outputFileSync('./gradle-licenses.csv', csv.map(r => r.map(String).map(c => c.replaceAll('"', '""')).map(c => `"${c}"`).join(',')).join('\n'))
echo ``
h('license -> dependencies:')
const lo = Object.keys(l).sort().reduce(
(obj, key) => {
obj[key] = l[key];
return obj;
},
{}
);
console.dir(lo, {depth: null, maxArrayLength: null})
// console.log(m)
Ant/Jar-Dateien
Für Projekte die Ant nutzen und Abhängigkeiten einfach als .jar
Dateien vorliegen haben, müssen wir möglichst viele nützliche Information aus den entsprechenden Dateien beziehen, um sie möglichst gut Abhängigkeiten auf mvnrepository.com zuordnen zu können.
Der Aufbau der Skripts ist ähnlich zu dem für Gradle, mit dem Unterschied, dass wir die Abhängigkeiten nicht in .gradle
Dateien definiert haben, sondern nach .jar
Dateien suchen und versuchen den Namen und die Version aus dem Dateinamen abzuleiten.
In ant-skipPaths.json
können wir Pfade definieren, die nicht überprüft werden sollen (z.B. transitive Abhängigkeiten).
Mit jar -d -f
listen wir alle Pfade innerhalb der .jar
Datei auf und überprüfen, ob der Pfad, der online gefunden wurde, vorkommt. Falls nicht, deutet dies oft darauf hin, dass die falsche Abhängigkeit zugeordnet wurde.
#!/usr/bin/env zx
// Dependencies: https://github.com/google/zx, https://github.com/sharkdp/fd, https://github.com/ericchiang/pup, https://github.com/ggreer/the_silver_searcher
$.verbose = false
// same as for gradle: h, levenshteinDistance, cache, query
h('Projects using Ant:')
const projects = await $`fd build.xml | sort`
echo(projects)
echo ``
const m = {} // dep -> paths
const cwd = process.cwd()
h('Collecting .jar files of each project:')
for (const f of projects.stdout.trim().split('\n')) {
const p = path.dirname(f)
cd(p)
echo ``
echo(chalk.green(p))
const jars = await $`fd '\.jar$' | sort`
for (const j of jars.stdout.trim().split('\n')) {
if (!j) continue
echo('└──', j)
const q = path.basename(j).replace(/\.jar$/, '').replace(/[_-]?(\d\.?)+.*/, '')
// echo(`clean name: ${q}`)
m[q] ??= {}
m[q].paths ??= []
m[q].paths.push(path.join(p, j))
}
cd(cwd)
}
fs.outputJsonSync('./ant-paths.json', m, {spaces: 2})
echo ``
const skipPaths = fs.readJsonSync('./ant-skipPaths.json', { throws: false }) || []
console.log('skipPaths:', skipPaths)
const l = {} // license -> deps
const csv = []
h('Checking licenses of grouped .jar files:')
for (const q in m) {
echo ``
console.log(q, m[q].paths)
echo `https://mvnrepository.com/search?q=${q}`
const maven = await query(q)
console.log(maven || chalk.red('No results!'))
m[q].maven = maven
const license = maven?.license || 'Unknown'
l[license] ??= []
l[license].push(q)
const ed = maven ? Math.round(levenshteinDistance(q, maven.artifactId)/Math.max(q.length, maven.artifactId.length)*100) : -1
m[q].editDistance = ed
console.log((ed == 0 ? chalk.yellow : chalk.red)('Edit distance:', m[q].editDistance))
let jar_info = ''
if (!argv.skipJarInfo) {
// echo `---`
const p = m[q].paths[0]
// # jar tf ${j} | grep -v META-INF | grep -v '.class$'
jar_info = (await $`jar -d -f ${p} | grep -v 'No module descriptor found.' | grep -v '^$'`).stdout
if (maven && !jar_info.includes(maven.groupId)) echo(chalk.red(`jar does not include ${maven.groupId}`))
if (!argv.fullJarInfo) {
const uniq = a => a.reduce((a,b) => { if(a.indexOf(b) < 0) a.push(b); return a }, [])
jar_info = uniq(jar_info.split('\n').map(r => r.startsWith('contains') ? r.split('.').slice(0,3).join('.') : r)).join('\n')
}
echo(jar_info)
m[q].jar_info = jar_info
}
if (!m[q].paths.some(x => skipPaths.some(y => x.startsWith(y))))
csv.push([license, ed, q, m[q].paths, JSON.stringify(maven), jar_info])
echo ``
}
fs.outputFileSync('./ant-licenses.csv', csv.map(r => r.map(String).map(c => c.replaceAll('"', '""')).map(c => `"${c}"`).join(',')).join('\n'))
echo ``
h('license -> dependencies:')
const lo = Object.keys(l).sort().reduce(
(obj, key) => {
obj[key] = l[key];
return obj;
},
{}
);
console.dir(lo, {depth: null, maxArrayLength: null})
// console.log(m)