Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Java support #17

Merged
merged 7 commits into from
Jul 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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")
]