Skip to content
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
4 changes: 2 additions & 2 deletions Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
<key>CFBundleDevelopmentRegion</key>
<string>English</string>
<key>CFBundleIdentifier</key>
<string>io.owasp-dep-scan.blint</string>
<string>io.owasp-depscan.blint</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>blint</string>
<key>CFBundleVersion</key>
<string>2.0.0</string>
<string>2.0.1</string>
</dict>
</plist>
7 changes: 4 additions & 3 deletions blint/android.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import tempfile

from blint.binary import parse, parse_dex
from blint.config import SYMBOL_DELIMITER
from blint.cyclonedx.spec import (
Component,
Property,
Expand Down Expand Up @@ -271,7 +272,7 @@ def parse_so_file(app_file, app_temp_dir, sof):
properties=[
Property(name="internal:srcFile", value=rel_path),
Property(name="internal:appFile", value=app_file),
Property(name="internal:functions", value=", ".join(set(functions))),
Property(name="internal:functions", value=SYMBOL_DELIMITER.join(set(functions))),
],
)
component.bom_ref = RefType(purl)
Expand Down Expand Up @@ -355,7 +356,7 @@ def create_dex_component(app_file, dex_metadata, group, name, rel_path, version)
Property(name="internal:appFile", value=app_file),
Property(
name="internal:functions",
value=", ".join(
value=SYMBOL_DELIMITER.join(
{
f"""{m.name}({','.join([_clean_type(p.underlying_array_type) for p in m.prototype.parameters_type])}):{_clean_type(m.prototype.return_type.underlying_array_type)}"""
for m in dex_metadata.get("methods")
Expand All @@ -364,7 +365,7 @@ def create_dex_component(app_file, dex_metadata, group, name, rel_path, version)
),
Property(
name="internal:classes",
value=", ".join(
value=SYMBOL_DELIMITER.join(
set(sorted([_clean_type(c.fullname) for c in dex_metadata.get("classes")]))
),
),
Expand Down
19 changes: 15 additions & 4 deletions blint/binary.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,11 @@ def extract_note_data(idx, note):
if "ID Hash" in note_str:
build_id = note_str.rsplit("ID Hash:", maxsplit=1)[-1].strip()
description = note.description
description_str = " ".join(map(integer_to_hex_str, description[:16]))
if len(description) > 16:
description_str = " ".join(map(integer_to_hex_str, description[:64]))
if len(description) > 64:
description_str += " ..."
if note.type == lief.ELF.Note.TYPE.GNU_BUILD_ID:
build_id = description_str.replace(" ", "")
type_str = note.type
type_str = str(type_str).rsplit(".", maxsplit=1)[-1]
note_details = ""
Expand All @@ -101,7 +103,7 @@ def extract_note_data(idx, note):
version = note_details.version
abi = str(note_details.abi)
version_str = f"{version[0]}.{version[1]}.{version[2]}"
if not version_str and type_str == "BUILD_ID" and build_id:
if not version_str and build_id:
version_str = build_id
return {
"index": idx,
Expand Down Expand Up @@ -213,6 +215,15 @@ def parse_strings(parsed_obj):
return strings_list


def ignorable_symbol(symbol_name: str | None) -> bool:
if not symbol_name:
return True
for pref in ("$f64.", "__"):
if symbol_name.startswith(pref):
return True
return False


def parse_symbols(symbols):
"""
Parse symbols from a list of symbols.
Expand All @@ -238,7 +249,7 @@ def parse_symbols(symbols):
symbol_name = symbol.demangled_name
if isinstance(symbol_name, lief.lief_errors):
symbol_name = symbol.name
if symbol_name:
if not ignorable_symbol(symbol_name):
exe_type = guess_exe_type(symbol_name)
symbols_list.append(
{
Expand Down
9 changes: 4 additions & 5 deletions blint/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
import os
import sys

from blint.sbom import generate
from blint.logger import LOG
from blint.analysis import AnalysisRunner, report
from blint.logger import LOG
from blint.sbom import generate
from blint.utils import gen_file_list

BLINT_LOGO = """
Expand Down Expand Up @@ -148,9 +148,6 @@ def handle_args():
# Create reports directory
reports_dir = args.reports_dir

if not os.path.exists(reports_dir):
os.makedirs(reports_dir)

for src in src_dirs:
if not os.path.exists(src):
LOG.error(f"{src} is an invalid file or directory!")
Expand All @@ -171,6 +168,8 @@ def main():
generate(src_dirs, sbom_output, args.deep_mode)
# Default case
else:
if not os.path.exists(reports_dir):
os.makedirs(reports_dir)
files = gen_file_list(src_dirs)
analyzer = AnalysisRunner()
findings, reviews, fuzzables = analyzer.start(
Expand Down
2 changes: 2 additions & 0 deletions blint/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -1245,3 +1245,5 @@
)
],
}

SYMBOL_DELIMITER = "~~"
89 changes: 78 additions & 11 deletions blint/sbom.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from blint.android import collect_app_metadata
from blint.binary import parse
from blint.config import SYMBOL_DELIMITER
from blint.cyclonedx.spec import (
BomFormat,
Component,
Expand Down Expand Up @@ -36,7 +37,7 @@ def default_parent(src_dirs: list[str]) -> Component:
"""
if not src_dirs:
raise ValueError("No source directories provided")
name = src_dirs[0]
name = os.path.basename(src_dirs[0])
purl = f"pkg:generic/{name}@latest"
component = Component(type=Type.application, name=name, version="latest", purl=purl)
component.bom_ref = RefType(purl)
Expand Down Expand Up @@ -116,7 +117,7 @@ def generate(src_dirs: list[str], output_file: str, deep_mode: bool) -> bool:
start=True,
)
for exe in exe_files:
progress.update(task, description=f"Processing [bold]{exe}[/bold]")
progress.update(task, description=f"Processing [bold]{exe}[/bold]", advance=1)
components.extend(process_exe_file(components, deep_mode, dependencies, exe, sbom))
if android_files:
task = progress.add_task(
Expand All @@ -125,13 +126,13 @@ def generate(src_dirs: list[str], output_file: str, deep_mode: bool) -> bool:
start=True,
)
for f in android_files:
progress.update(task, description=f"Processing [bold]{f}[/bold]")
progress.update(task, description=f"Processing [bold]{f}[/bold]", advance=1)
components.extend(process_android_file(components, deep_mode, dependencies, f, sbom))
return create_sbom(components, dependencies, output_file, sbom)
return create_sbom(components, dependencies, output_file, sbom, deep_mode)


def create_sbom(
components: list[Component], dependencies: list[dict], output_file: str, sbom: CycloneDX
components: list[Component], dependencies: list[dict], output_file: str, sbom: CycloneDX, deep_mode: bool
) -> bool:
"""
Creates a Software Bill of Materials (SBOM) with the provided components,
Expand All @@ -142,6 +143,7 @@ def create_sbom(
dependencies (list): A list of dependencies.
output_file (str): The path to the output file.
sbom: The SBOM object representing the SBOM.
deep_mode (bool): Flag indicating whether to perform deep analysis.

Returns:
bool: True if the SBOM generation is successful, False otherwise.
Expand Down Expand Up @@ -170,7 +172,7 @@ def create_sbom(
with open(output_file, mode="w", encoding="utf-8") as fp:
fp.write(
sbom.model_dump_json(
indent=2,
indent=None if deep_mode else 2,
exclude_none=True,
exclude_defaults=True,
warnings=False,
Expand All @@ -180,6 +182,49 @@ def create_sbom(
return True


def components_from_symbols_version(symbols_version: list[dict]) -> list[Component]:
"""
Creates a list of Component objects from symbols version.
This style of detection is quite imprecise since the version is just a min specifier.

Args:
symbols_version (list[dict]): A list of symbols version.

Returns:
list[Component]: list of components
"""
lib_components: list[Component] = []
for symbol in symbols_version:
group = ""
name = symbol["name"]
version = "latest"
if "_" in name:
tmp_a = name.split("_")
if len(tmp_a) == 2:
version = tmp_a[-1]
name = tmp_a[0].lower()
if name.startswith("glib"):
name = name.removeprefix("g")
group = "gnu"
purl = f"pkg:generic/{group}/{name}@{version}" if group else f"pkg:generic/{name}@{version}"
if symbol.get("hash"):
purl = f"{purl}?hash={symbol.get('hash')}"
comp = Component(
type=Type.library,
group=group,
name=name,
version=version,
purl=purl,
evidence=create_component_evidence(symbol["name"], 0.5),
properties=[
Property(name="internal:symbol_version", value=symbol["name"])
]
)
comp.bom_ref = RefType(purl)
lib_components.append(comp)
return lib_components


def process_exe_file(
components: list[Component],
deep_mode: bool,
Expand All @@ -205,6 +250,7 @@ def process_exe_file(
parent_component: Component = default_parent([exe])
metadata: Dict[str, Any] = parse(exe)
parent_component.properties = []
lib_components: list[Component] = []
for prop in (
"binary_type",
"magic",
Expand Down Expand Up @@ -235,26 +281,47 @@ def process_exe_file(
value = str(metadata.get(prop))
if isinstance(metadata.get(prop), bool):
value = value.lower()
parent_component.properties.append(Property(name=f"internal:{prop}", value=value))
if value:
parent_component.properties.append(Property(name=f"internal:{prop}", value=value))
if metadata.get("notes"):
for note in metadata.get("notes"):
if note.get("version"):
parent_component.properties.append(
Property(name=f"internal:{note.get('type')}", value=note.get('version')))
if deep_mode:
symbols_version: list[dict] = metadata.get("symbols_version", [])
# Attempt to detect library components from the symbols version block
# If this is unsuccessful then store the information as a property
lib_components += components_from_symbols_version(symbols_version)
if not lib_components:
parent_component.properties += [
Property(
name="internal:symbols_version",
value=", ".join([f["name"] for f in symbols_version]),
)
]
parent_component.properties += [
Property(
name="internal:functions",
value="~~".join([f["name"] for f in metadata.get("functions", [])]),
value=SYMBOL_DELIMITER.join(
[f["name"] for f in metadata.get("functions", []) if not f["name"].startswith("__")]),
),
Property(
name="internal:symtab_symbols",
value="~~".join([f["name"] for f in metadata.get("symtab_symbols", [])]),
value=SYMBOL_DELIMITER.join([f["name"] for f in metadata.get("symtab_symbols", [])]),
),
Property(
name="internal:imports",
value="~~".join([f["name"] for f in metadata.get("imports", [])]),
value=SYMBOL_DELIMITER.join([f["name"] for f in metadata.get("imports", [])]),
),
Property(
name="internal:dynamic_symbols",
value=SYMBOL_DELIMITER.join([f["name"] for f in metadata.get("dynamic_symbols", [])]),
),
]
if not sbom.metadata.component.components:
sbom.metadata.component.components = []
sbom.metadata.component.components.append(parent_component)
lib_components: list[Component] = []
if metadata.get("libraries"):
for entry in metadata.get("libraries"):
comp = create_library_component(entry, exe)
Expand Down
8 changes: 4 additions & 4 deletions file_version_info.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ VSVersionInfo(
ffi=FixedFileInfo(
# filevers and prodvers should be always a tuple with four items: (1, 2, 3, 4)
# Set not needed items to zero 0. Must always contain 4 elements.
filevers=(2,0,0,0),
prodvers=(2,0,0,0),
filevers=(2,0,1,0),
prodvers=(2,0,1,0),
# Contains a bitmask that specifies the valid bits 'flags'r
mask=0x3f,
# Contains a bitmask that specifies the Boolean attributes of the file.
Expand All @@ -32,12 +32,12 @@ VSVersionInfo(
u'040904B0',
[StringStruct(u'CompanyName', u'OWASP Foundation'),
StringStruct(u'FileDescription', u'blint - The Binary Linter'),
StringStruct(u'FileVersion', u'2.0.0.0'),
StringStruct(u'FileVersion', u'2.0.1.0'),
StringStruct(u'InternalName', u'blint'),
StringStruct(u'LegalCopyright', u'© OWASP Foundation. All rights reserved.'),
StringStruct(u'OriginalFilename', u'blint.exe'),
StringStruct(u'ProductName', u'blint'),
StringStruct(u'ProductVersion', u'2.0.0.0')])
StringStruct(u'ProductVersion', u'2.0.1.0')])
]),
VarFileInfo([VarStruct(u'Translation', [1033, 1200])])
]
Expand Down
1 change: 0 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "blint"
version = "2.0.0"
version = "2.0.1"
description = "Linter and SBOM generator for binary files."
authors = ["Prabhu Subramanian <[email protected]>", "Caroline Russell <[email protected]>"]
license = "Apache-2.0"
Expand Down