Skip to content

Commit e9d6a0c

Browse files
Support rust binary parsing
Support parsing rust binaries and getting: 1. Symbols 2. Packages and Dependencies Reference: aboutcode-org/scancode.io#1435 Signed-off-by: Ayan Sinha Mahapatra <ayansmahapatra@gmail.com>
1 parent 0c67387 commit e9d6a0c

30 files changed

Lines changed: 318153 additions & 50 deletions

.readthedocs.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ version: 2
77

88
# Build in latest ubuntu/python
99
build:
10-
os: ubuntu-22.04
10+
os: ubuntu-24.04
1111
tools:
12-
python: "3.11"
12+
python: "3.12"
1313

1414
# Build PDF & ePub
1515
formats:

AUTHORS.rst

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
The following organizations or individuals have contributed to this repo:
22

3-
-
3+
- Ayan Sinha Mahapatra @AyanSinhaMahapatra
4+
- nexB Inc.
5+

CHANGELOG.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ Changelog
22
=========
33

44

5-
v0.0.0
5+
v0.1.0
66
------
77

88
*xxxx-xx-xx* -- Initial release.

requirements-dev.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
scancode-toolkit==32.3.0

requirements.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# from blint v2.3.2
2+
# https://github.com/owasp-dep-scan/blint/blob/1e1250a4bf6c25eccba8970bd877901ee56070c7/poetry.lock
3+
lief==0.15.1
4+
symbolic==10.2.1

setup.cfg

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
[metadata]
2-
name = skeleton
3-
license = Apache-2.0
2+
name = rust-inspector
3+
license = Apache-2.0 AND MIT
44

55
# description must be on ONE line https://github.com/pypa/setuptools/issues/1390
6-
description = skeleton
6+
description = rust-inspector is a scancode plugin to extract symbols and dependencies found in Rust binaries.
77
long_description = file:README.rst
88
long_description_content_type = text/x-rst
9-
url = https://github.com/aboutcode-org/skeleton
9+
url = https://github.com/nexB/rust-inspector
1010

1111
author = nexB. Inc. and others
1212
author_email = info@aboutcode.org
@@ -38,17 +38,27 @@ zip_safe = false
3838

3939
setup_requires = setuptools_scm[toml] >= 4
4040

41-
python_requires = >=3.8
41+
python_requires = >=3.9
4242

4343
install_requires =
44+
commoncode
45+
plugincode
46+
typecode
47+
lief==0.15.1
48+
symbolic==10.2.1
4449

50+
[options.entry_points]
51+
52+
scancode_scan =
53+
rust_symbol = rust_inspector.plugin:RustSymbolScannerPlugin
4554

4655
[options.packages.find]
4756
where = src
4857

4958

5059
[options.extras_require]
5160
testing =
61+
scancode-toolkit
5262
pytest >= 6, != 7.0.0
5363
pytest-xdist >= 2
5464
aboutcode-toolkit >= 7.0.2
@@ -65,4 +75,3 @@ docs =
6575
sphinx-autobuild
6676
sphinx-rtd-dark-mode>=1.3.0
6777
sphinx-copybutton
68-

src/README.rst

Lines changed: 0 additions & 2 deletions
This file was deleted.

src/rust_inspector/__init__.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# -*- coding: utf-8 -*-
2+
#
3+
# Copyright (c) nexB Inc. and others. All rights reserved.
4+
# ScanCode is a trademark of nexB Inc.
5+
# SPDX-License-Identifier: Apache-2.0
6+
# See http://www.apache.org/licenses/LICENSE-2.0 for the license text.
7+
# See https://github.com/nexB/rust-inspector for support or download.
8+
# See https://aboutcode.org for more information about nexB OSS projects.
9+
#
10+

src/rust_inspector/binary.py

Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
# -*- coding: utf-8 -*-
2+
#
3+
# Copyright (c) nexB Inc. and others. All rights reserved.
4+
# ScanCode is a trademark of nexB Inc.
5+
# SPDX-License-Identifier: Apache-2.0
6+
# See http://www.apache.org/licenses/LICENSE-2.0 for the license text.
7+
# See https://github.com/nexB/rust-inspector for support or download.
8+
# See https://aboutcode.org for more information about nexB OSS projects.
9+
#
10+
11+
import os
12+
import lief
13+
import json
14+
import zlib
15+
16+
from typecode import contenttype
17+
from typecode.contenttype import get_type
18+
19+
from rust_inspector.blint_binary import parse_symbols
20+
from rust_inspector.config import SPLIT_CHARACTERS_RUST
21+
from rust_inspector.config import STANDARD_SYMBOLS_RUST
22+
23+
24+
def is_macho(location):
25+
"""
26+
Return True if the file at ``location`` is in macOS/Darwin's Mach-O format, otherwise False.
27+
"""
28+
t = get_type(location)
29+
return t.filetype_file.lower().startswith("mach-o") or t.mimetype_file.lower().startswith(
30+
"application/x-mach-binary"
31+
)
32+
33+
34+
def is_executable_binary(location):
35+
"""
36+
Return True if the file at ``location`` is an executable binary.
37+
"""
38+
if not os.path.exists(location):
39+
return False
40+
41+
if not os.path.isfile(location):
42+
return False
43+
44+
typ = contenttype.Type(location)
45+
46+
if not (typ.is_elf or typ.is_winexe or is_macho(location=location)):
47+
return False
48+
49+
return True
50+
51+
52+
def parse_rust_binary(location):
53+
"""
54+
Get a parsed lief._lief.ELF.Binary object from parsing the Rust binary
55+
present at `location`.
56+
"""
57+
return lief.parse(location)
58+
59+
60+
def get_rust_packages_data(location):
61+
"""
62+
Given a rust binary present at location, parse and return packages
63+
data present in the binary.
64+
65+
This get packages data only if it is present in the rust binary, i.e.
66+
if the rust binary was built using `cargo-auditable` which adds a section
67+
to the rust binary with data on packages and dependencies.
68+
See https://github.com/rust-secure-code/cargo-auditable for more info.
69+
70+
Code for parsing rust bianries to get package data is from
71+
https://github.com/rust-secure-code/cargo-auditable/blob/master/PARSING.md
72+
"""
73+
if not is_executable_binary(location):
74+
return
75+
76+
parsed_binary = parse_rust_binary(location)
77+
78+
section_deps = None
79+
for section in parsed_binary.sections:
80+
if section.name == ".dep-v0":
81+
section_deps = section
82+
break
83+
84+
decompressed_data = zlib.decompress(section_deps.content)
85+
packages_data = json.loads(decompressed_data)
86+
87+
return packages_data
88+
89+
90+
91+
def might_have_rust_symbols(string_with_symbols):
92+
93+
if not string_with_symbols:
94+
return False
95+
96+
if not len(string_with_symbols) > 1:
97+
return False
98+
99+
# Commonly encountered strings which are not rust symbols
100+
ignore_strings = ["GCC_except_table", "anonymous", "SAFESEH"]
101+
if string_with_symbols in ignore_strings:
102+
return False
103+
104+
# We have sometimes strings which are all uppercase alphabets, these are
105+
# usually not symbols?
106+
# Examples: `__CALLSITE::META`, `tracing_core::dispatcher::NO_SUBSCRIBER`,
107+
# `encoding_rs::SHIFT_JIS_INIT::hd4a3d8ee69a4c88a` etc
108+
string_without_underscore = string_with_symbols.replace("_", "")
109+
is_all_uppercase = string_without_underscore.isupper() and string_without_underscore.isalpha()
110+
if is_all_uppercase:
111+
return False
112+
113+
# fully numberic strings are not rust symbols
114+
if string_with_symbols.isnumeric():
115+
return False
116+
117+
# TODO: also filter checksum like values (entropy/randomness?)
118+
# TODO: also filter Uppercase Alphabets + Numbers
119+
# Example: encoding_rs::UTF_16LE_INIT::h47f34b513cc70084
120+
121+
return True
122+
123+
def remove_standard_symbols(rust_symbols):
124+
return [
125+
symbol
126+
for symbol in rust_symbols
127+
if symbol not in STANDARD_SYMBOLS_RUST
128+
]
129+
130+
131+
def split_strings_by_char(split_strings, split_char):
132+
133+
final_split_strings = []
134+
for split_str in split_strings:
135+
if split_char in split_str:
136+
split_strings = split_str.split(split_char)
137+
final_split_strings.extend(split_strings)
138+
else:
139+
final_split_strings.append(split_str)
140+
141+
return [
142+
split_string
143+
for split_string in final_split_strings
144+
if split_string
145+
]
146+
147+
148+
def split_strings_into_rust_symbols(strings_to_split, split_by_chars=SPLIT_CHARACTERS_RUST):
149+
150+
split_strings = []
151+
split_strings_log = []
152+
for split_char in split_by_chars:
153+
if not split_strings:
154+
split_strings = strings_to_split
155+
156+
split_strings = split_strings_by_char(split_strings, split_char)
157+
split_strings_log.append(split_strings)
158+
159+
return split_strings
160+
161+
162+
def cleanup_symbols(split_symbols, include_stdlib=False, unique=True, sort_symbols=False):
163+
164+
rust_symbols = []
165+
for split_string in split_symbols:
166+
if might_have_rust_symbols(split_string):
167+
rust_symbols.append(split_string)
168+
169+
if not include_stdlib:
170+
rust_symbols = remove_standard_symbols(rust_symbols)
171+
172+
if unique:
173+
rust_symbols = list(set(rust_symbols))
174+
175+
if sort_symbols:
176+
rust_symbols = sorted(rust_symbols)
177+
178+
return rust_symbols
179+
180+
181+
def extract_strings_with_symbols(symbols_data, include_stdlib=False, unique=True, sort_symbols=False):
182+
183+
strings_with_symbols = []
184+
185+
ignore_types = ["NOTYPE", "TLS"]
186+
187+
for symbol_data in symbols_data:
188+
189+
if not symbol_data.get("name"):
190+
continue
191+
192+
if symbol_data.get("type") in ignore_types:
193+
continue
194+
195+
# These are usually like the following:
196+
# - memcpy@GLIBC_2.14
197+
# - UI_method_set_writer@OPENSSL_3.0.0
198+
# So these doesn't have source symbols
199+
if symbol_data.get("is_imported"):
200+
continue
201+
202+
# These are usually like the following:
203+
# `getrandom@GLIBC_2.25`, `__umodti3`, `_ITM_registerTMCloneTable`
204+
# So these doesn't have source symbols
205+
if symbol_data.get("binding") == 'WEAK':
206+
continue
207+
208+
# file/module names are also source symbols as they
209+
# are imported in source code files
210+
if symbol_data.get("type") == "FILE":
211+
file_string = symbol_data.get("name")
212+
file_segments = file_string.split('.')
213+
if not file_segments:
214+
continue
215+
216+
# These are usually like following:
217+
# cyclonedx_bom.4f845c900a9ac4e1-cgu.11
218+
# only the first part are symbols
219+
filename = file_segments[0]
220+
strings_with_symbols.append(filename)
221+
222+
# Symbols which are Objects and Functions by type are collections of
223+
# rust symbols and misc strings
224+
if symbol_data.get("type") in ["OBJECT", "FUNC"]:
225+
file_string = symbol_data.get("name")
226+
strings_with_symbols.append(file_string)
227+
228+
split_symbols = split_strings_into_rust_symbols(strings_to_split=strings_with_symbols)
229+
rust_symbols = cleanup_symbols(
230+
split_symbols=split_symbols,
231+
include_stdlib=include_stdlib,
232+
unique=unique,
233+
sort_symbols=sort_symbols,
234+
)
235+
236+
return rust_symbols
237+
238+
239+
def collect_and_parse_rust_symbols(location, include_stdlib=False, sort_symbols=True, **kwargs):
240+
"""
241+
Return a mapping of Rust symbols of interest for the Rust binary file at ``location``.
242+
Return an empty mapping if there is no symbols or if this is not a binary.
243+
Raise exceptions on errors.
244+
"""
245+
if not is_executable_binary(location):
246+
return
247+
248+
rust_data = parse_rust_binary(location=location)
249+
return collect_and_parse_rust_symbols_from_data(
250+
rust_data=rust_data,
251+
include_stdlib=include_stdlib,
252+
unique=True,
253+
sort_symbols=sort_symbols,
254+
)
255+
256+
257+
def collect_and_parse_rust_symbols_from_data(rust_data, include_stdlib=False, unique=True, sort_symbols=False, **kwargs):
258+
"""
259+
Return a mapping of Rust symbols of interest for the mapping of Rust binary of ``rust_data``.
260+
Return an empty mapping if there is no symbols or if this is not a binary.
261+
Raise exceptions on errors.
262+
"""
263+
if not rust_data:
264+
return {}
265+
266+
# Extract and demangle symbol strings from rust binary parsed object
267+
# These are collections of symbol and module strings with stdlib objects
268+
extracted_symbols = parse_symbols(rust_data.symtab_symbols)
269+
270+
# Cleanup and get individual symbols which could be rust symbols
271+
symbol_strings = extract_strings_with_symbols(
272+
symbols_data=extracted_symbols,
273+
include_stdlib=include_stdlib,
274+
unique=unique,
275+
sort_symbols=sort_symbols,
276+
)
277+
278+
return dict(rust_symbols=symbol_strings)
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
about_resource: blint_binary.py
2+
name: blint
3+
author: Prabhu Subramanian, Caroline Russell and others
4+
author_email: prabhu@appthreat.com, caroline@appthreat.dev
5+
homepage_url: https://github.com/owasp-dep-scan/blint
6+
download_url: https://github.com/owasp-dep-scan/blint/blob/1e1250a4bf6c25eccba8970bd877901ee56070c7/blint/lib/binary.py
7+
license_expression: mit
8+
copyright: Copyright (c) OWASP Foundation
9+
package_url: pkg:pypi/blint@2.3.2
10+
notes: only a subset of functions from binary.py is used, after minor modifications

0 commit comments

Comments
 (0)