Skip to content

Commit

Permalink
Merge pull request #17 from LLNL/CYT-334-Java-support
Browse files Browse the repository at this point in the history
Add Java support
  • Loading branch information
nightlark authored Jul 27, 2023
2 parents ed45007 + 135b2ad commit f22223b
Show file tree
Hide file tree
Showing 5 changed files with 179 additions and 1 deletion.
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ dependencies = [
"defusedxml",
"spdx-tools>=0.7.1,==0.7.*",
"pluggy",
"click"
"click",
"javatools>=1.6.0"
]
dynamic = ["version"]

Expand Down
80 changes: 80 additions & 0 deletions surfactant/infoextractors/java_file.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
from typing import Any, Dict

import javatools.jarinfo

import surfactant.plugin
from surfactant.sbomtypes import SBOM, Software

# TODO: Add documentation about how to install javatools
# swig and libssl-dev needs to be installed on Ubuntu
# https://gitlab.com/m2crypto/m2crypto/-/blob/master/INSTALL.rst


def supports_file(filetype: str) -> bool:
return filetype in ("JAVACLASS", "JAR", "WAR", "EAR")


@surfactant.plugin.hookimpl
def extract_file_info(sbom: SBOM, software: Software, filename: str, filetype: str) -> object:
if not supports_file(filetype):
return None
return extract_java_info(filename, filetype)


# Map from internal major number to Java SE version
# https://docs.oracle.com/javase/specs/jvms/se20/html/jvms-4.html#jvms-4.1-200-B.2
_JAVA_VERSION_MAPPING = {
45: "1.1",
46: "1.2",
47: "1.3",
48: "1.4",
49: "5.0",
50: "6",
51: "7",
52: "8",
53: "9",
54: "10",
55: "11",
56: "12",
57: "13",
58: "14",
59: "15",
60: "16",
61: "17",
62: "18",
63: "19",
64: "20",
}


def handle_java_class(info: Dict[str, Any], class_info: javatools.JavaClassInfo):
# This shouldn't happen but just in-case it does don't overwrite information
if class_info.get_this() in info["javaClasses"]:
return
info["javaClasses"][class_info.get_this()] = {}
add_to = info["javaClasses"][class_info.get_this()]
(major_version, _) = class_info.get_version()
if major_version in _JAVA_VERSION_MAPPING:
add_to["javaMinSEVersion"] = _JAVA_VERSION_MAPPING[major_version]
add_to["javaExports"] = [*class_info.get_provides()]
# I've seen this fail for some reason; catch errors on it and just ignore
# them if it fails
try:
add_to["javaImports"] = [*class_info.get_requires()]
except IndexError:
# Should this be set to "Unknown" or similar?
add_to["javaImports"] = []


def extract_java_info(filename: str, filetype: str) -> object:
info = {"javaClasses": {}}
if filetype in ("JAR", "EAR", "WAR"):
with javatools.jarinfo.JarInfo(filename) as jarinfo:
for class_ in jarinfo.get_classes():
handle_java_class(info, jarinfo.get_classinfo(class_))
elif filetype == "JAVACLASS":
with open(filename, "rb") as f:
class_info = javatools.JavaClassInfo()
class_info.unpack(javatools.unpack(f))
handle_java_class(info, class_info)
return info
4 changes: 4 additions & 0 deletions surfactant/plugin/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@ def _register_plugins(pm: pluggy.PluginManager) -> None:
a_out_file,
coff_file,
elf_file,
java_file,
ole_file,
pe_file,
)
from surfactant.output import csv_writer, cytrics_writer, spdx_writer
from surfactant.relationships import (
dotnet_relationship,
elf_relationship,
java_relationship,
pe_relationship,
)

Expand All @@ -31,10 +33,12 @@ def _register_plugins(pm: pluggy.PluginManager) -> None:
a_out_file,
coff_file,
elf_file,
java_file,
pe_file,
ole_file,
dotnet_relationship,
elf_relationship,
java_relationship,
pe_relationship,
csv_writer,
cytrics_writer,
Expand Down
50 changes: 50 additions & 0 deletions surfactant/relationships/java_relationship.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
from typing import List, Optional

import surfactant.plugin
from surfactant.sbomtypes import SBOM, Relationship, Software


def has_required_fields(metadata) -> bool:
return "javaClasses" in metadata


class _ExportDict:
created = False
supplied_by = {}

@classmethod
def create_export_dict(cls, sbom: SBOM):
if cls.created:
return
for software_entry in sbom.software:
for metadata in software_entry.metadata:
if "javaClasses" in metadata:
for class_info in metadata["javaClasses"].values():
for export in class_info["javaExports"]:
cls.supplied_by[export] = software_entry.UUID
cls.created = True

@classmethod
def get_supplier(cls, import_name: str) -> Optional[str]:
if import_name in cls.supplied_by:
return cls.supplied_by[import_name]
return None


@surfactant.plugin.hookimpl
def establish_relationships(
sbom: SBOM, software: Software, metadata
) -> Optional[List[Relationship]]:
if not has_required_fields(metadata):
return None
_ExportDict.create_export_dict(sbom)
relationships = []
dependant_uuid = software.UUID
for class_info in metadata["javaClasses"].values():
for import_ in class_info["javaImports"]:
if supplier_uuid := _ExportDict.get_supplier(import_):
if supplier_uuid != dependant_uuid:
rel = Relationship(dependant_uuid, supplier_uuid, "Uses")
if rel not in relationships:
relationships.append(rel)
return relationships
43 changes: 43 additions & 0 deletions tests/relationships/test_java.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Copyright 2023 Lawrence Livermore National Security, LLC
# See the top-level LICENSE file for details.
#
# SPDX-License-Identifier: MIT

from surfactant.plugin.manager import get_plugin_manager
from surfactant.sbomtypes import SBOM, Relationship, Software

sbom = SBOM(
software=[
Software(
UUID="supplier",
fileName=["supplier"],
installPath=["supplier"],
metadata=[{"javaClasses": {"dummy": {"javaExports": ["someFunc():void"]}}}],
),
Software(
UUID="consumer",
fileName=["consumer"],
installPath=["consumer"],
metadata=[
{
"javaClasses": {
"dummy": {
"javaExports": [],
"javaImports": ["someFunc():void"],
},
},
},
],
),
],
relationships=[],
)


def test_java_relationship():
javaPlugin = get_plugin_manager().get_plugin("surfactant.relationships.java_relationship")
sw = sbom.software[1]
md = sw.metadata[0]
assert javaPlugin.establish_relationships(sbom, sw, md) == [
Relationship("consumer", "supplier", "Uses")
]

0 comments on commit f22223b

Please sign in to comment.