Ü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:

  1. Maven: am modernsten und weitesten verbreitet, Abhängigkeiten definiert in pom.xml und Lizenzen größtenteils automatisch abrufbar
  2. 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)
  3. 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)
Autor
Florian Weigand

Florian ist Gründer der BitFlow GmbH und berät Investoren zur Auswahl richtiger Tech-Unternehmen

Autor
Florian Weigand

Florian ist Gründer der BitFlow GmbH und berät Investoren zur Auswahl richtiger Tech-Unternehmen

Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.
UP