scanoss 1.41.1__py3-none-any.whl → 1.43.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- scanoss/__init__.py +1 -1
- scanoss/cli.py +17 -5
- scanoss/data/build_date.txt +1 -1
- scanoss/header_filter.py +563 -0
- scanoss/scanner.py +14 -137
- scanoss/scanossapi.py +1 -1
- scanoss/scanossbase.py +1 -1
- scanoss/winnowing.py +71 -19
- {scanoss-1.41.1.dist-info → scanoss-1.43.0.dist-info}/METADATA +1 -1
- {scanoss-1.41.1.dist-info → scanoss-1.43.0.dist-info}/RECORD +14 -13
- {scanoss-1.41.1.dist-info → scanoss-1.43.0.dist-info}/WHEEL +0 -0
- {scanoss-1.41.1.dist-info → scanoss-1.43.0.dist-info}/entry_points.txt +0 -0
- {scanoss-1.41.1.dist-info → scanoss-1.43.0.dist-info}/licenses/LICENSE +0 -0
- {scanoss-1.41.1.dist-info → scanoss-1.43.0.dist-info}/top_level.txt +0 -0
scanoss/__init__.py
CHANGED
scanoss/cli.py
CHANGED
|
@@ -170,7 +170,6 @@ def setup_args() -> None: # noqa: PLR0912, PLR0915
|
|
|
170
170
|
default=DEFAULT_RETRY,
|
|
171
171
|
help='Retry limit for API communication (optional - default 5)',
|
|
172
172
|
)
|
|
173
|
-
p_scan.add_argument('--no-wfp-output', action='store_true', help='Skip WFP file generation')
|
|
174
173
|
p_scan.add_argument('--dependencies', '-D', action='store_true', help='Add Dependency scanning')
|
|
175
174
|
p_scan.add_argument('--dependencies-only', action='store_true', help='Run Dependency scanning only')
|
|
176
175
|
p_scan.add_argument(
|
|
@@ -1096,6 +1095,19 @@ def setup_args() -> None: # noqa: PLR0912, PLR0915
|
|
|
1096
1095
|
p.add_argument('--skip-md5', '-5', type=str, action='append', help='Skip files matching MD5.')
|
|
1097
1096
|
p.add_argument('--strip-hpsm', '-G', type=str, action='append', help='Strip HPSM string from WFP.')
|
|
1098
1097
|
p.add_argument('--strip-snippet', '-N', type=str, action='append', help='Strip Snippet ID string from WFP.')
|
|
1098
|
+
p.add_argument(
|
|
1099
|
+
'--skip-headers',
|
|
1100
|
+
'-skh',
|
|
1101
|
+
action='store_true',
|
|
1102
|
+
help='Skip license headers, comments and imports at the beginning of files.',
|
|
1103
|
+
)
|
|
1104
|
+
p.add_argument(
|
|
1105
|
+
'--skip-headers-limit',
|
|
1106
|
+
'-shl',
|
|
1107
|
+
type=int,
|
|
1108
|
+
default=0,
|
|
1109
|
+
help='Maximum number of lines to skip when filtering headers (default: 0 = no limit).',
|
|
1110
|
+
)
|
|
1099
1111
|
|
|
1100
1112
|
# Global Scan/GRPC options
|
|
1101
1113
|
for p in [
|
|
@@ -1388,6 +1400,8 @@ def wfp(parser, args):
|
|
|
1388
1400
|
strip_hpsm_ids=args.strip_hpsm,
|
|
1389
1401
|
strip_snippet_ids=args.strip_snippet,
|
|
1390
1402
|
scan_settings=scan_settings,
|
|
1403
|
+
skip_headers=args.skip_headers,
|
|
1404
|
+
skip_headers_limit=args.skip_headers_limit,
|
|
1391
1405
|
)
|
|
1392
1406
|
if args.stdin:
|
|
1393
1407
|
contents = sys.stdin.buffer.read()
|
|
@@ -1537,9 +1551,6 @@ def scan(parser, args): # noqa: PLR0912, PLR0915
|
|
|
1537
1551
|
if args.retry < 0:
|
|
1538
1552
|
print_stderr(f'POST retry (--retry) too small: {args.retry}. Reverting to default.')
|
|
1539
1553
|
|
|
1540
|
-
if not os.access(os.getcwd(), os.W_OK): # Make sure the current directory is writable. If not disable saving WFP
|
|
1541
|
-
print_stderr(f'Warning: Current directory is not writable: {os.getcwd()}')
|
|
1542
|
-
args.no_wfp_output = True
|
|
1543
1554
|
if args.ca_cert and not os.path.exists(args.ca_cert):
|
|
1544
1555
|
print_stderr(f'Error: Certificate file does not exist: {args.ca_cert}.')
|
|
1545
1556
|
sys.exit(1)
|
|
@@ -1558,7 +1569,6 @@ def scan(parser, args): # noqa: PLR0912, PLR0915
|
|
|
1558
1569
|
nb_threads=args.threads,
|
|
1559
1570
|
post_size=args.post_size,
|
|
1560
1571
|
timeout=args.timeout,
|
|
1561
|
-
no_wfp_file=args.no_wfp_output,
|
|
1562
1572
|
all_extensions=args.all_extensions,
|
|
1563
1573
|
all_folders=args.all_folders,
|
|
1564
1574
|
hidden_files_folders=args.all_hidden,
|
|
@@ -1583,6 +1593,8 @@ def scan(parser, args): # noqa: PLR0912, PLR0915
|
|
|
1583
1593
|
scan_settings=scan_settings,
|
|
1584
1594
|
req_headers=process_req_headers(args.header),
|
|
1585
1595
|
use_grpc=args.grpc,
|
|
1596
|
+
skip_headers=args.skip_headers,
|
|
1597
|
+
skip_headers_limit=args.skip_headers_limit,
|
|
1586
1598
|
)
|
|
1587
1599
|
if args.wfp:
|
|
1588
1600
|
if not scanner.is_file_or_snippet_scan():
|
scanoss/data/build_date.txt
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
date:
|
|
1
|
+
date: 20260105093002, utime: 1767605402
|
scanoss/header_filter.py
ADDED
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
Copyright (c) 2025, SCANOSS
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in
|
|
14
|
+
all copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
22
|
+
THE SOFTWARE.
|
|
23
|
+
|
|
24
|
+
Line Filter Module - Identifies where real source code implementation begins.
|
|
25
|
+
|
|
26
|
+
This module analyzes source code files and determines which lines are:
|
|
27
|
+
- License headers
|
|
28
|
+
- Documentation comments
|
|
29
|
+
- Imports/includes
|
|
30
|
+
- Blank lines
|
|
31
|
+
|
|
32
|
+
And returns the content from where the real implementation begins.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
import re
|
|
36
|
+
from pathlib import Path
|
|
37
|
+
from typing import Optional, Tuple
|
|
38
|
+
|
|
39
|
+
from .scanossbase import ScanossBase
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class LanguagePatterns:
|
|
43
|
+
"""
|
|
44
|
+
Regex patterns for different programming languages.
|
|
45
|
+
|
|
46
|
+
This class provides a collection of regex patterns for identifying different
|
|
47
|
+
programming constructs, handling imports, comments, and license statements
|
|
48
|
+
across various programming languages. The main purpose of this class is to
|
|
49
|
+
assist in parsing or analysing code written in different languages efficiently.
|
|
50
|
+
|
|
51
|
+
:ivar COMMENT_PATTERNS: A dictionary containing regex patterns to identify
|
|
52
|
+
single-line and multi-line comments in various programming languages.
|
|
53
|
+
:ivar IMPORT_PATTERNS: A dictionary mapping programming languages to their
|
|
54
|
+
respective regex patterns for identifying import statements or package
|
|
55
|
+
includes it.
|
|
56
|
+
:ivar LICENSE_KEYWORDS: A list of keywords commonly found in license texts
|
|
57
|
+
or statements, often used to detect the presence of licensing information.
|
|
58
|
+
"""
|
|
59
|
+
# Comment patterns (single-line and multi-line start/end)
|
|
60
|
+
COMMENT_PATTERNS = {
|
|
61
|
+
# C-style languages: C, C++, Java, JavaScript, TypeScript, Go,
|
|
62
|
+
# Rust, C#, PHP, Kotlin, Scala, Dart, Objective-C
|
|
63
|
+
'c_style': {
|
|
64
|
+
'single_line': r'^\s*//.*$',
|
|
65
|
+
'multi_start': r'^\s*/\*',
|
|
66
|
+
'multi_end': r'\*/\s*$',
|
|
67
|
+
'multi_single': r'^\s*/\*.*\*/\s*$',
|
|
68
|
+
},
|
|
69
|
+
# Python, shell scripts, Ruby, Perl, R, Julia, YAML
|
|
70
|
+
'python_style': {
|
|
71
|
+
'single_line': r'^\s*#.*$',
|
|
72
|
+
'doc_string_start': r'^\s*"""',
|
|
73
|
+
'doc_string_end': r'"""\s*$',
|
|
74
|
+
},
|
|
75
|
+
# Lua, SQL, Haskell
|
|
76
|
+
'lua_style': {
|
|
77
|
+
'single_line': r'^\s*--.*$',
|
|
78
|
+
'multi_start': r'^\s*--\[\[',
|
|
79
|
+
'multi_end': r'\]\]\s*$',
|
|
80
|
+
},
|
|
81
|
+
# HTML, XML
|
|
82
|
+
'html_style': {
|
|
83
|
+
'multi_start': r'^\s*<!--',
|
|
84
|
+
'multi_end': r'-->\s*$',
|
|
85
|
+
'multi_single': r'^\s*<!--.*-->\s*$',
|
|
86
|
+
},
|
|
87
|
+
}
|
|
88
|
+
# Import/include patterns by language
|
|
89
|
+
IMPORT_PATTERNS = {
|
|
90
|
+
'python': [
|
|
91
|
+
r'^\s*import\s+',
|
|
92
|
+
r'^\s*from\s+.*\s+import\s+',
|
|
93
|
+
],
|
|
94
|
+
'javascript': [
|
|
95
|
+
r'^\s*import\s+.*\s+from\s+',
|
|
96
|
+
r'^\s*import\s+["\']',
|
|
97
|
+
r'^\s*import\s+type\s+',
|
|
98
|
+
r'^\s*export\s+\*\s+from\s+',
|
|
99
|
+
r'^\s*export\s+\{.*\}\s+from\s+',
|
|
100
|
+
r'^\s*const\s+.*\s*=\s*require\(',
|
|
101
|
+
r'^\s*var\s+.*\s*=\s*require\(',
|
|
102
|
+
r'^\s*let\s+.*\s*=\s*require\(',
|
|
103
|
+
],
|
|
104
|
+
'typescript': [
|
|
105
|
+
r'^\s*import\s+',
|
|
106
|
+
r'^\s*export\s+.*\s+from\s+',
|
|
107
|
+
r'^\s*import\s+type\s+',
|
|
108
|
+
r'^\s*import\s+\{.*\}\s+from\s+',
|
|
109
|
+
],
|
|
110
|
+
'java': [
|
|
111
|
+
r'^\s*import\s+',
|
|
112
|
+
r'^\s*package\s+',
|
|
113
|
+
],
|
|
114
|
+
'kotlin': [
|
|
115
|
+
r'^\s*import\s+',
|
|
116
|
+
r'^\s*package\s+',
|
|
117
|
+
],
|
|
118
|
+
'scala': [
|
|
119
|
+
r'^\s*import\s+',
|
|
120
|
+
r'^\s*package\s+',
|
|
121
|
+
],
|
|
122
|
+
'go': [
|
|
123
|
+
r'^\s*import\s+\(',
|
|
124
|
+
r'^\s*import\s+"',
|
|
125
|
+
r'^\s*package\s+',
|
|
126
|
+
r'^\s*"[^"]*"\s*$', # Imports inside import () block
|
|
127
|
+
# Imports with alias: name "package"
|
|
128
|
+
r'^\s*[a-zA-Z_][a-zA-Z0-9_]*\s+"[^"]*"\s*$',
|
|
129
|
+
r'^\s*_\s+"[^"]*"\s*$', # _ "package" imports
|
|
130
|
+
],
|
|
131
|
+
'rust': [
|
|
132
|
+
r'^\s*use\s+',
|
|
133
|
+
r'^\s*extern\s+crate\s+',
|
|
134
|
+
r'^\s*mod\s+',
|
|
135
|
+
],
|
|
136
|
+
'cpp': [
|
|
137
|
+
r'^\s*#include\s+',
|
|
138
|
+
r'^\s*#pragma\s+',
|
|
139
|
+
r'^\s*#ifndef\s+.*_H.*', # Header guards: #ifndef FOO_H
|
|
140
|
+
r'^\s*#define\s+.*_H.*', # Header guards: #define FOO_H
|
|
141
|
+
# #endif at end of file (may have comment)
|
|
142
|
+
r'^\s*#endif\s+(//.*)?\s*$',
|
|
143
|
+
],
|
|
144
|
+
'csharp': [
|
|
145
|
+
r'^\s*using\s+',
|
|
146
|
+
r'^\s*namespace\s+',
|
|
147
|
+
],
|
|
148
|
+
'php': [
|
|
149
|
+
r'^\s*use\s+',
|
|
150
|
+
r'^\s*require\s+',
|
|
151
|
+
r'^\s*require_once\s+',
|
|
152
|
+
r'^\s*include\s+',
|
|
153
|
+
r'^\s*include_once\s+',
|
|
154
|
+
r'^\s*namespace\s+',
|
|
155
|
+
],
|
|
156
|
+
'swift': [
|
|
157
|
+
r'^\s*import\s+',
|
|
158
|
+
],
|
|
159
|
+
'ruby': [
|
|
160
|
+
r'^\s*require\s+',
|
|
161
|
+
r'^\s*require_relative\s+',
|
|
162
|
+
r'^\s*load\s+',
|
|
163
|
+
],
|
|
164
|
+
'perl': [
|
|
165
|
+
r'^\s*use\s+',
|
|
166
|
+
r'^\s*require\s+',
|
|
167
|
+
],
|
|
168
|
+
'r': [
|
|
169
|
+
r'^\s*library\(',
|
|
170
|
+
r'^\s*require\(',
|
|
171
|
+
r'^\s*source\(',
|
|
172
|
+
],
|
|
173
|
+
'lua': [
|
|
174
|
+
r'^\s*require\s+',
|
|
175
|
+
r'^\s*local\s+.*\s*=\s*require\(',
|
|
176
|
+
],
|
|
177
|
+
'dart': [
|
|
178
|
+
r'^\s*import\s+',
|
|
179
|
+
r'^\s*export\s+',
|
|
180
|
+
r'^\s*part\s+',
|
|
181
|
+
],
|
|
182
|
+
'haskell': [
|
|
183
|
+
r'^\s*import\s+',
|
|
184
|
+
r'^\s*module\s+',
|
|
185
|
+
],
|
|
186
|
+
'elixir': [
|
|
187
|
+
r'^\s*import\s+',
|
|
188
|
+
r'^\s*alias\s+',
|
|
189
|
+
r'^\s*require\s+',
|
|
190
|
+
r'^\s*use\s+',
|
|
191
|
+
],
|
|
192
|
+
'clojure': [
|
|
193
|
+
r'^\s*\(\s*ns\s+',
|
|
194
|
+
r'^\s*\(\s*require\s+',
|
|
195
|
+
r'^\s*\(\s*import\s+',
|
|
196
|
+
],
|
|
197
|
+
}
|
|
198
|
+
# Keywords that indicate licenses
|
|
199
|
+
LICENSE_KEYWORDS = [
|
|
200
|
+
'copyright', 'license', 'licensed', 'all rights reserved',
|
|
201
|
+
'permission', 'redistribution', 'warranty', 'liability',
|
|
202
|
+
'apache', 'mit', 'gpl', 'bsd', 'mozilla', 'author:',
|
|
203
|
+
'spdx-license', 'contributors', 'licensee'
|
|
204
|
+
]
|
|
205
|
+
|
|
206
|
+
COMPLETE_DOCSTRING_QUOTE_COUNT = 2
|
|
207
|
+
LICENSE_HEADER_MAX_LINES = 50
|
|
208
|
+
# Map of file extensions to programming languages
|
|
209
|
+
EXT_MAP = {
|
|
210
|
+
'.py': 'python',
|
|
211
|
+
'.js': 'javascript',
|
|
212
|
+
'.mjs': 'javascript',
|
|
213
|
+
'.cjs': 'javascript',
|
|
214
|
+
'.ts': 'typescript',
|
|
215
|
+
'.tsx': 'typescript',
|
|
216
|
+
'.jsx': 'javascript',
|
|
217
|
+
'.java': 'java',
|
|
218
|
+
'.kt': 'kotlin',
|
|
219
|
+
'.kts': 'kotlin',
|
|
220
|
+
'.scala': 'scala',
|
|
221
|
+
'.sc': 'scala',
|
|
222
|
+
'.go': 'go',
|
|
223
|
+
'.rs': 'rust',
|
|
224
|
+
'.cpp': 'cpp',
|
|
225
|
+
'.cc': 'cpp',
|
|
226
|
+
'.cxx': 'cpp',
|
|
227
|
+
'.c': 'cpp',
|
|
228
|
+
'.h': 'cpp',
|
|
229
|
+
'.hpp': 'cpp',
|
|
230
|
+
'.hxx': 'cpp',
|
|
231
|
+
'.cs': 'csharp',
|
|
232
|
+
'.php': 'php',
|
|
233
|
+
'.swift': 'swift',
|
|
234
|
+
'.rb': 'ruby',
|
|
235
|
+
'.pl': 'perl',
|
|
236
|
+
'.pm': 'perl',
|
|
237
|
+
'.r': 'r',
|
|
238
|
+
'.R': 'r',
|
|
239
|
+
'.lua': 'lua',
|
|
240
|
+
'.dart': 'dart',
|
|
241
|
+
'.hs': 'haskell',
|
|
242
|
+
'.ex': 'elixir',
|
|
243
|
+
'.exs': 'elixir',
|
|
244
|
+
'.clj': 'clojure',
|
|
245
|
+
'.cljs': 'clojure',
|
|
246
|
+
'.m': 'cpp', # Objective-C
|
|
247
|
+
'.mm': 'cpp', # Objective-C++
|
|
248
|
+
# Shell scripts share Python's # comment style, but lack dedicated
|
|
249
|
+
# import patterns (source/. commands won't be filtered)
|
|
250
|
+
'.sh': 'python',
|
|
251
|
+
'.bash': 'python',
|
|
252
|
+
'.zsh': 'python',
|
|
253
|
+
'.fish': 'python',
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def is_blank_line(stripped_line: str) -> bool:
|
|
258
|
+
"""
|
|
259
|
+
Check if a line is blank.
|
|
260
|
+
|
|
261
|
+
This method determines whether a given string `line` is blank by checking
|
|
262
|
+
if it consists entirely of whitespace or is empty.
|
|
263
|
+
|
|
264
|
+
:param stripped_line: The string to be evaluated.
|
|
265
|
+
:return: True if the string is blank, otherwise False.
|
|
266
|
+
"""
|
|
267
|
+
return len(stripped_line) == 0
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def is_shebang(stripped_line: str) -> bool:
|
|
271
|
+
"""
|
|
272
|
+
Check if the given line is a shebang line.
|
|
273
|
+
|
|
274
|
+
This function determines if the provided string is a shebang line,
|
|
275
|
+
which indicates the path to the interpreter that should execute the
|
|
276
|
+
script.
|
|
277
|
+
|
|
278
|
+
:param stripped_line: The string to check if it's a shebang line.
|
|
279
|
+
:return: True if the given line starts with '#!', otherwise False.
|
|
280
|
+
"""
|
|
281
|
+
return stripped_line.startswith('#!')
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
class HeaderFilter(ScanossBase):
|
|
285
|
+
"""
|
|
286
|
+
Source code file analyser that filters headers, comments, and imports.
|
|
287
|
+
|
|
288
|
+
This class processes code files and returns only the real
|
|
289
|
+
implementation content, omitting licenses, documentation comments,
|
|
290
|
+
and imports.
|
|
291
|
+
"""
|
|
292
|
+
|
|
293
|
+
def __init__(
|
|
294
|
+
self,
|
|
295
|
+
debug: bool = False,
|
|
296
|
+
trace: bool = False,
|
|
297
|
+
quiet: bool = False,
|
|
298
|
+
skip_limit: Optional[int] = None
|
|
299
|
+
):
|
|
300
|
+
"""
|
|
301
|
+
Initialise HeaderFilter
|
|
302
|
+
Parameters
|
|
303
|
+
----------
|
|
304
|
+
skip_limit: int
|
|
305
|
+
Maximum number of lines to skip when analysing a file.
|
|
306
|
+
If set, then stop stripping data after this number of lines.
|
|
307
|
+
(None/0 = unlimited by default)
|
|
308
|
+
"""
|
|
309
|
+
super().__init__(debug, trace, quiet)
|
|
310
|
+
self.patterns = LanguagePatterns()
|
|
311
|
+
self.max_lines = skip_limit
|
|
312
|
+
|
|
313
|
+
def filter(self, file: str, decoded_contents: str) -> int:
|
|
314
|
+
"""
|
|
315
|
+
Main method that filters file content
|
|
316
|
+
Parameters
|
|
317
|
+
----------
|
|
318
|
+
:param file: File path (used to detect extension)
|
|
319
|
+
:param decoded_contents: File contents in utf-8 encoding
|
|
320
|
+
Return
|
|
321
|
+
------
|
|
322
|
+
- line_offset: Number of lines skipped from the beginning
|
|
323
|
+
(0 if no filtering)
|
|
324
|
+
"""
|
|
325
|
+
if not decoded_contents or not file:
|
|
326
|
+
self.print_msg(f'No file or contents provided, skipping line filter for: {file}')
|
|
327
|
+
return 0
|
|
328
|
+
self.print_debug(f'HeaderFilter processing file: {file}')
|
|
329
|
+
# Detect language
|
|
330
|
+
language = self.detect_language(file)
|
|
331
|
+
# If language is not supported, return original content
|
|
332
|
+
if not language:
|
|
333
|
+
self.print_debug(f'Skipping line filter for unsupported language: {file}')
|
|
334
|
+
return 0
|
|
335
|
+
lines = decoded_contents.splitlines(keepends=True)
|
|
336
|
+
num_lines = len(lines)
|
|
337
|
+
if num_lines == 0:
|
|
338
|
+
self.print_msg(f'No lines in file: {file}')
|
|
339
|
+
return 0
|
|
340
|
+
self.print_debug(f'Analysing {num_lines} lines for file: {file}')
|
|
341
|
+
|
|
342
|
+
# Find the first implementation line (optimised - stops at first match)
|
|
343
|
+
implementation_start = self.find_first_implementation_line(lines, language)
|
|
344
|
+
# If no implementation, return empty
|
|
345
|
+
if implementation_start is None:
|
|
346
|
+
self.print_debug(f'No implementation found in file: {file}')
|
|
347
|
+
return 0
|
|
348
|
+
# Calculate how many lines were filtered out (line_offset)
|
|
349
|
+
line_offset = implementation_start - 1
|
|
350
|
+
# Apply max_lines limit if configured
|
|
351
|
+
if self.max_lines is not None and 0 < self.max_lines < line_offset:
|
|
352
|
+
self.print_trace(
|
|
353
|
+
f'Line offset {line_offset} exceeds max_lines {self.max_lines}, '
|
|
354
|
+
f'capping at {self.max_lines} for: {file}'
|
|
355
|
+
)
|
|
356
|
+
line_offset = self.max_lines
|
|
357
|
+
|
|
358
|
+
if line_offset > 0:
|
|
359
|
+
self.print_debug(f'Filtered out {line_offset} lines from beginning of {file} (language: {language})')
|
|
360
|
+
return line_offset
|
|
361
|
+
|
|
362
|
+
def detect_language(self, file_path: str) -> Optional[str]:
|
|
363
|
+
"""
|
|
364
|
+
Detects the programming language based on the provided file extension.
|
|
365
|
+
|
|
366
|
+
This function uses a predefined mapping between file extensions and programming
|
|
367
|
+
languages to determine the language associated with the file. If the file extension
|
|
368
|
+
is found in the mapping, the corresponding language is returned. Otherwise, it
|
|
369
|
+
returns None.
|
|
370
|
+
|
|
371
|
+
:param file_path: Path to the file whose programming language needs to be detected.
|
|
372
|
+
:return: The programming language corresponding to the file extension if mapped,
|
|
373
|
+
otherwise None.
|
|
374
|
+
"""
|
|
375
|
+
path = Path(file_path)
|
|
376
|
+
extension = path.suffix.lower()
|
|
377
|
+
if extension:
|
|
378
|
+
detected_language = EXT_MAP.get(extension)
|
|
379
|
+
if detected_language:
|
|
380
|
+
self.print_debug(f'Detected language "{detected_language}" for extension "{extension}"')
|
|
381
|
+
else:
|
|
382
|
+
self.print_debug(f'No language mapping found for extension "{extension}"')
|
|
383
|
+
else:
|
|
384
|
+
self.print_debug(f'No file extension found, skipping language detection for: {file_path}')
|
|
385
|
+
detected_language = None
|
|
386
|
+
return detected_language
|
|
387
|
+
|
|
388
|
+
def is_license_header(self, line: str) -> bool:
|
|
389
|
+
"""
|
|
390
|
+
Check if the line appears to be part of a license header.
|
|
391
|
+
|
|
392
|
+
This method evaluates a given line of text to determine whether it
|
|
393
|
+
contains keywords that suggest it is part of a license header. It
|
|
394
|
+
performs a case-insensitive check against a predefined set of license
|
|
395
|
+
keywords.
|
|
396
|
+
|
|
397
|
+
:param line: The line of text to check.
|
|
398
|
+
:return: True if the line contains keywords indicating it is part of a
|
|
399
|
+
license header; False otherwise.
|
|
400
|
+
"""
|
|
401
|
+
line_lower = line.lower()
|
|
402
|
+
return any(keyword in line_lower for keyword in self.patterns.LICENSE_KEYWORDS)
|
|
403
|
+
|
|
404
|
+
def get_comment_style(self, language: str) -> str:
|
|
405
|
+
"""
|
|
406
|
+
Return the comment style associated with a given programming language.
|
|
407
|
+
|
|
408
|
+
This method determines the appropriate comment style to use based on the
|
|
409
|
+
specified programming language. Supported languages include those with C-style
|
|
410
|
+
comments, Python-style comments, and Lua-style comments. If the language does
|
|
411
|
+
not match any of the explicitly defined groups, a default of `c_style` is
|
|
412
|
+
returned.
|
|
413
|
+
|
|
414
|
+
:param language: The name of the programming language for which the comment
|
|
415
|
+
style needs to be determined.
|
|
416
|
+
:return: The comment style for the provided programming language. Possible
|
|
417
|
+
values are 'c_style', 'python_style', or 'lua_style'.
|
|
418
|
+
"""
|
|
419
|
+
if language:
|
|
420
|
+
if language in ['cpp', 'java', 'kotlin', 'scala', 'javascript', 'typescript',
|
|
421
|
+
'go', 'rust', 'csharp', 'php', 'swift', 'dart']:
|
|
422
|
+
return 'c_style'
|
|
423
|
+
if language in ['python', 'ruby', 'perl', 'r']:
|
|
424
|
+
return 'python_style'
|
|
425
|
+
if language in ['lua', 'haskell']:
|
|
426
|
+
return 'lua_style'
|
|
427
|
+
self.print_debug(f'No comment style defined for language "{language}", using default: "c_style"')
|
|
428
|
+
return 'c_style' # Default
|
|
429
|
+
|
|
430
|
+
def is_comment(self, line: str, in_multiline: bool, patterns: dict) -> Tuple[bool, bool]: # noqa: PLR0911
|
|
431
|
+
"""
|
|
432
|
+
Check if a line is a comment
|
|
433
|
+
|
|
434
|
+
:param patterns: comment patterns
|
|
435
|
+
:param line: Line to check
|
|
436
|
+
:param in_multiline: Whether we're currently in a multiline comment
|
|
437
|
+
:return: Tuple of (is_comment, still_in_multiline)
|
|
438
|
+
"""
|
|
439
|
+
if not patterns:
|
|
440
|
+
self.print_msg('No comment patterns defined, skipping comment check')
|
|
441
|
+
return False, in_multiline
|
|
442
|
+
# If we're in a multiline comment
|
|
443
|
+
if in_multiline:
|
|
444
|
+
# Check if the comment ends
|
|
445
|
+
if 'multi_end' in patterns and re.search(patterns['multi_end'], line):
|
|
446
|
+
return True, False
|
|
447
|
+
if 'doc_string_end' in patterns and re.search(patterns['doc_string_end'], line):
|
|
448
|
+
return True, False
|
|
449
|
+
return True, True
|
|
450
|
+
# Single-line comment
|
|
451
|
+
if 'single_line' in patterns and re.match(patterns['single_line'], line):
|
|
452
|
+
return True, False
|
|
453
|
+
# Multiline comment complete in one line
|
|
454
|
+
if 'multi_single' in patterns and re.match(patterns['multi_single'], line):
|
|
455
|
+
return True, False
|
|
456
|
+
# Start of multiline comment (C-style)
|
|
457
|
+
if 'multi_start' in patterns and re.search(patterns['multi_start'], line):
|
|
458
|
+
# If it also ends on the same line
|
|
459
|
+
if 'multi_end' in patterns and re.search(patterns['multi_end'], line):
|
|
460
|
+
return True, False
|
|
461
|
+
return True, True
|
|
462
|
+
# Start of docstring (Python)
|
|
463
|
+
if 'doc_string_start' in patterns and '"""' in line:
|
|
464
|
+
# Count how many quotes there are
|
|
465
|
+
count = line.count('"""')
|
|
466
|
+
if count == COMPLETE_DOCSTRING_QUOTE_COUNT: # Complete docstring in one line
|
|
467
|
+
return True, False
|
|
468
|
+
if count == 1: # Start of a multiline docstring
|
|
469
|
+
return True, True
|
|
470
|
+
# Default response: not a comment
|
|
471
|
+
return False, in_multiline
|
|
472
|
+
|
|
473
|
+
def is_import(self, line: str, patterns: dict) -> bool:
|
|
474
|
+
"""
|
|
475
|
+
Check if a line of code is an import or include statement for a given programming language.
|
|
476
|
+
|
|
477
|
+
This function determines whether a specific line of code matches any
|
|
478
|
+
import/include patterns defined for the provided programming language.
|
|
479
|
+
It relies on predefined regular expression patterns.
|
|
480
|
+
|
|
481
|
+
:param patterns: import patterns for the given language.
|
|
482
|
+
:param line: A single line of code to check.
|
|
483
|
+
:return: True if the line matches any import/include pattern for the given language,
|
|
484
|
+
otherwise False.
|
|
485
|
+
"""
|
|
486
|
+
if not patterns:
|
|
487
|
+
self.print_debug('No import patterns defined, skipping import check')
|
|
488
|
+
return any(re.match(pattern, line) for pattern in patterns)
|
|
489
|
+
|
|
490
|
+
def find_first_implementation_line(self, lines: list[str], language: str) -> Optional[int]: # noqa: PLR0912
|
|
491
|
+
"""
|
|
492
|
+
Find the line number where the implementation begins (optimised version).
|
|
493
|
+
Returns as soon as the first implementation line is found.
|
|
494
|
+
|
|
495
|
+
:param lines: List of code lines
|
|
496
|
+
:param language: Programming language
|
|
497
|
+
:return: Line number (1-indexed) where implementation starts, or None if not found
|
|
498
|
+
"""
|
|
499
|
+
if not lines or not language:
|
|
500
|
+
self.print_debug('No lines or language provided, skipping implementation line detection')
|
|
501
|
+
return None
|
|
502
|
+
in_multiline_comment = False
|
|
503
|
+
in_license_section = False
|
|
504
|
+
in_import_block = False # To handle import blocks in Go
|
|
505
|
+
consecutive_imports_count = 0
|
|
506
|
+
# Get comment & import patterns for the language
|
|
507
|
+
comment_patterns = self.patterns.COMMENT_PATTERNS[self.get_comment_style(language)]
|
|
508
|
+
import_patterns = self.patterns.IMPORT_PATTERNS[language]
|
|
509
|
+
# Iterate through lines trying to find the first implementation line
|
|
510
|
+
for i, line in enumerate(lines):
|
|
511
|
+
line_number = i + 1
|
|
512
|
+
stripped = line.strip()
|
|
513
|
+
# Shebang (only first line) or blank line
|
|
514
|
+
if (i == 0 and is_shebang(stripped)) or is_blank_line(stripped):
|
|
515
|
+
continue
|
|
516
|
+
# Check if it's a comment
|
|
517
|
+
is_a_comment, in_multiline_comment = self.is_comment(line, in_multiline_comment, comment_patterns)
|
|
518
|
+
if is_a_comment:
|
|
519
|
+
# Check if it's part of the license header
|
|
520
|
+
if self.is_license_header(line):
|
|
521
|
+
if not in_license_section:
|
|
522
|
+
self.print_trace(f'Line {line_number}: Detected license header section')
|
|
523
|
+
in_license_section = True
|
|
524
|
+
# If still in the license section (first lines)
|
|
525
|
+
elif in_license_section and line_number < LICENSE_HEADER_MAX_LINES:
|
|
526
|
+
pass # Still in the license section. Keep looking.
|
|
527
|
+
else:
|
|
528
|
+
if in_license_section:
|
|
529
|
+
self.print_trace(f'Line {line_number}: End of license header section')
|
|
530
|
+
in_license_section = False
|
|
531
|
+
continue
|
|
532
|
+
# If not a comment but we find a non-empty line, end license section
|
|
533
|
+
if not is_a_comment:
|
|
534
|
+
in_license_section = False
|
|
535
|
+
# Handle import blocks in Go
|
|
536
|
+
if language == 'go':
|
|
537
|
+
if stripped.startswith('import ('):
|
|
538
|
+
self.print_trace(f'Line {line_number}: Detected Go import block start')
|
|
539
|
+
in_import_block = True
|
|
540
|
+
continue
|
|
541
|
+
if in_import_block:
|
|
542
|
+
if stripped == ')':
|
|
543
|
+
self.print_trace(f'Line {line_number}: Detected Go import block end')
|
|
544
|
+
in_import_block = False
|
|
545
|
+
continue
|
|
546
|
+
if (stripped.startswith('"') or stripped.startswith('_') or
|
|
547
|
+
re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*\s+"', stripped)):
|
|
548
|
+
# It's part of the import block
|
|
549
|
+
continue
|
|
550
|
+
# Check if it's an import
|
|
551
|
+
if self.is_import(line, import_patterns):
|
|
552
|
+
if consecutive_imports_count == 0:
|
|
553
|
+
self.print_trace(f'Line {line_number}: Detected import section')
|
|
554
|
+
consecutive_imports_count += 1
|
|
555
|
+
continue
|
|
556
|
+
# If we get here, it's implementation code - return immediately!
|
|
557
|
+
self.print_trace(f'Line {line_number}: First implementation line detected')
|
|
558
|
+
return line_number
|
|
559
|
+
# End for loop?
|
|
560
|
+
return None
|
|
561
|
+
#
|
|
562
|
+
# End of HeaderFilter Class
|
|
563
|
+
#
|
scanoss/scanner.py
CHANGED
|
@@ -31,7 +31,6 @@ from pathlib import Path
|
|
|
31
31
|
from typing import Any, Dict, List, Optional
|
|
32
32
|
|
|
33
33
|
import importlib_resources
|
|
34
|
-
from progress.bar import Bar
|
|
35
34
|
from progress.spinner import Spinner
|
|
36
35
|
from pypac.parser import PACFile
|
|
37
36
|
|
|
@@ -72,7 +71,6 @@ class Scanner(ScanossBase):
|
|
|
72
71
|
|
|
73
72
|
def __init__( # noqa: PLR0913, PLR0915
|
|
74
73
|
self,
|
|
75
|
-
wfp: str = None,
|
|
76
74
|
scan_output: str = None,
|
|
77
75
|
output_format: str = 'plain',
|
|
78
76
|
debug: bool = False,
|
|
@@ -84,7 +82,6 @@ class Scanner(ScanossBase):
|
|
|
84
82
|
nb_threads: int = 5,
|
|
85
83
|
post_size: int = 32,
|
|
86
84
|
timeout: int = 180,
|
|
87
|
-
no_wfp_file: bool = False,
|
|
88
85
|
all_extensions: bool = False,
|
|
89
86
|
all_folders: bool = False,
|
|
90
87
|
hidden_files_folders: bool = False,
|
|
@@ -109,6 +106,8 @@ class Scanner(ScanossBase):
|
|
|
109
106
|
scan_settings: 'ScanossSettings | None' = None,
|
|
110
107
|
req_headers: dict = None,
|
|
111
108
|
use_grpc: bool = False,
|
|
109
|
+
skip_headers: bool = False,
|
|
110
|
+
skip_headers_limit: int = 0,
|
|
112
111
|
):
|
|
113
112
|
"""
|
|
114
113
|
Initialise scanning class, including Winnowing, ScanossApi, ThreadedScanning
|
|
@@ -118,10 +117,8 @@ class Scanner(ScanossBase):
|
|
|
118
117
|
skip_folders = []
|
|
119
118
|
if skip_extensions is None:
|
|
120
119
|
skip_extensions = []
|
|
121
|
-
self.wfp = wfp if wfp else 'scanner_output.wfp'
|
|
122
120
|
self.scan_output = scan_output
|
|
123
121
|
self.output_format = output_format
|
|
124
|
-
self.no_wfp_file = no_wfp_file
|
|
125
122
|
self.isatty = sys.stderr.isatty()
|
|
126
123
|
self.all_extensions = all_extensions
|
|
127
124
|
self.all_folders = all_folders
|
|
@@ -137,6 +134,7 @@ class Scanner(ScanossBase):
|
|
|
137
134
|
|
|
138
135
|
self.winnowing = Winnowing(
|
|
139
136
|
debug=debug,
|
|
137
|
+
trace=trace,
|
|
140
138
|
quiet=quiet,
|
|
141
139
|
skip_snippets=self._skip_snippets,
|
|
142
140
|
all_extensions=all_extensions,
|
|
@@ -145,6 +143,8 @@ class Scanner(ScanossBase):
|
|
|
145
143
|
strip_hpsm_ids=strip_hpsm_ids,
|
|
146
144
|
strip_snippet_ids=strip_snippet_ids,
|
|
147
145
|
skip_md5_ids=skip_md5_ids,
|
|
146
|
+
skip_headers=skip_headers,
|
|
147
|
+
skip_headers_limit=skip_headers_limit,
|
|
148
148
|
)
|
|
149
149
|
self.scanoss_api = ScanossApi(
|
|
150
150
|
debug=debug,
|
|
@@ -367,8 +367,6 @@ class Scanner(ScanossBase):
|
|
|
367
367
|
spinner_ctx = Spinner('Fingerprinting ') if (not self.quiet and self.isatty) else nullcontext()
|
|
368
368
|
|
|
369
369
|
with spinner_ctx as spinner:
|
|
370
|
-
save_wfps_for_print = not self.no_wfp_file or not self.threaded_scan
|
|
371
|
-
wfp_list = []
|
|
372
370
|
scan_block = ''
|
|
373
371
|
scan_size = 0
|
|
374
372
|
queue_size = 0
|
|
@@ -389,8 +387,6 @@ class Scanner(ScanossBase):
|
|
|
389
387
|
if wfp is None or wfp == '':
|
|
390
388
|
self.print_debug(f'No WFP returned for {to_scan_file}. Skipping.')
|
|
391
389
|
continue
|
|
392
|
-
if save_wfps_for_print:
|
|
393
|
-
wfp_list.append(wfp)
|
|
394
390
|
file_count += 1
|
|
395
391
|
if self.threaded_scan:
|
|
396
392
|
wfp_size = len(wfp.encode('utf-8'))
|
|
@@ -424,12 +420,6 @@ class Scanner(ScanossBase):
|
|
|
424
420
|
self.threaded_scan.queue_add(scan_block) # Make sure all files have been submitted
|
|
425
421
|
|
|
426
422
|
if file_count > 0:
|
|
427
|
-
if save_wfps_for_print: # Write a WFP file if no threading is requested
|
|
428
|
-
self.print_debug(f'Writing fingerprints to {self.wfp}')
|
|
429
|
-
with open(self.wfp, 'w') as f:
|
|
430
|
-
f.write(''.join(wfp_list))
|
|
431
|
-
else:
|
|
432
|
-
self.print_debug(f'Skipping writing WFP file {self.wfp}')
|
|
433
423
|
if self.threaded_scan:
|
|
434
424
|
success = self.__run_scan_threaded(scan_started, file_count)
|
|
435
425
|
else:
|
|
@@ -637,8 +627,6 @@ class Scanner(ScanossBase):
|
|
|
637
627
|
spinner_ctx = Spinner('Fingerprinting ') if (not self.quiet and self.isatty) else nullcontext()
|
|
638
628
|
|
|
639
629
|
with spinner_ctx as spinner:
|
|
640
|
-
save_wfps_for_print = not self.no_wfp_file or not self.threaded_scan
|
|
641
|
-
wfp_list = []
|
|
642
630
|
scan_block = ''
|
|
643
631
|
scan_size = 0
|
|
644
632
|
queue_size = 0
|
|
@@ -658,8 +646,6 @@ class Scanner(ScanossBase):
|
|
|
658
646
|
if wfp is None or wfp == '':
|
|
659
647
|
self.print_debug(f'No WFP returned for {file}. Skipping.')
|
|
660
648
|
continue
|
|
661
|
-
if save_wfps_for_print:
|
|
662
|
-
wfp_list.append(wfp)
|
|
663
649
|
file_count += 1
|
|
664
650
|
if self.threaded_scan:
|
|
665
651
|
wfp_size = len(wfp.encode('utf-8'))
|
|
@@ -694,12 +680,6 @@ class Scanner(ScanossBase):
|
|
|
694
680
|
self.threaded_scan.queue_add(scan_block) # Make sure all files have been submitted
|
|
695
681
|
|
|
696
682
|
if file_count > 0:
|
|
697
|
-
if save_wfps_for_print: # Write a WFP file if no threading is requested
|
|
698
|
-
self.print_debug(f'Writing fingerprints to {self.wfp}')
|
|
699
|
-
with open(self.wfp, 'w') as f:
|
|
700
|
-
f.write(''.join(wfp_list))
|
|
701
|
-
else:
|
|
702
|
-
self.print_debug(f'Skipping writing WFP file {self.wfp}')
|
|
703
683
|
if self.threaded_scan:
|
|
704
684
|
success = self.__run_scan_threaded(scan_started, file_count)
|
|
705
685
|
else:
|
|
@@ -762,126 +742,22 @@ class Scanner(ScanossBase):
|
|
|
762
742
|
success = False
|
|
763
743
|
return success
|
|
764
744
|
|
|
765
|
-
def
|
|
766
|
-
"""
|
|
767
|
-
Scan the contents of the specified WFP file (in the current process)
|
|
768
|
-
:param file: Scan the contents of the specified WFP file (in the current process)
|
|
769
|
-
:return: True if successful, False otherwise
|
|
770
|
-
"""
|
|
771
|
-
success = True
|
|
772
|
-
wfp_file = file if file else self.wfp # If a WFP file is specified, use it, otherwise us the default
|
|
773
|
-
if not os.path.exists(wfp_file) or not os.path.isfile(wfp_file):
|
|
774
|
-
raise Exception(f'ERROR: Specified WFP file does not exist or is not a file: {wfp_file}')
|
|
775
|
-
file_count = Scanner.__count_files_in_wfp_file(wfp_file)
|
|
776
|
-
cur_files = 0
|
|
777
|
-
cur_size = 0
|
|
778
|
-
batch_files = 0
|
|
779
|
-
wfp = ''
|
|
780
|
-
max_component = {'name': '', 'hits': 0}
|
|
781
|
-
components = {}
|
|
782
|
-
self.print_debug(f'Found {file_count} files to process.')
|
|
783
|
-
raw_output = '{\n'
|
|
784
|
-
file_print = ''
|
|
785
|
-
bar_ctx = Bar('Scanning', max=file_count) if (not self.quiet and self.isatty) else nullcontext()
|
|
786
|
-
|
|
787
|
-
with bar_ctx as bar:
|
|
788
|
-
if bar:
|
|
789
|
-
bar.next(0)
|
|
790
|
-
with open(wfp_file) as f:
|
|
791
|
-
for line in f:
|
|
792
|
-
if line.startswith(WFP_FILE_START):
|
|
793
|
-
if file_print:
|
|
794
|
-
wfp += file_print # Store the WFP for the current file
|
|
795
|
-
cur_size = len(wfp.encode('utf-8'))
|
|
796
|
-
file_print = line # Start storing the next file
|
|
797
|
-
cur_files += 1
|
|
798
|
-
batch_files += 1
|
|
799
|
-
else:
|
|
800
|
-
file_print += line # Store the rest of the WFP for this file
|
|
801
|
-
l_size = cur_size + len(file_print.encode('utf-8'))
|
|
802
|
-
# Hit the max post size, so sending the current batch and continue processing
|
|
803
|
-
if l_size >= self.max_post_size and wfp:
|
|
804
|
-
self.print_debug(
|
|
805
|
-
f'Sending {batch_files} ({cur_files}) of'
|
|
806
|
-
f' {file_count} ({len(wfp.encode("utf-8"))} bytes) files to the ScanOSS API.'
|
|
807
|
-
)
|
|
808
|
-
if self.debug and cur_size > self.max_post_size:
|
|
809
|
-
Scanner.print_stderr(
|
|
810
|
-
f'Warning: Post size {cur_size} greater than limit {self.max_post_size}'
|
|
811
|
-
)
|
|
812
|
-
scan_resp = self.scanoss_api.scan(wfp, max_component['name']) # Scan current WFP and store
|
|
813
|
-
if bar:
|
|
814
|
-
bar.next(batch_files)
|
|
815
|
-
if scan_resp is not None:
|
|
816
|
-
for key, value in scan_resp.items():
|
|
817
|
-
raw_output += ' "%s":%s,' % (key, json.dumps(value, indent=2))
|
|
818
|
-
for v in value:
|
|
819
|
-
if hasattr(v, 'get'):
|
|
820
|
-
if v.get('id') != 'none':
|
|
821
|
-
vcv = '%s:%s:%s' % (v.get('vendor'), v.get('component'), v.get('version'))
|
|
822
|
-
components[vcv] = components[vcv] + 1 if vcv in components else 1
|
|
823
|
-
if max_component['hits'] < components[vcv]:
|
|
824
|
-
max_component['name'] = v.get('component')
|
|
825
|
-
max_component['hits'] = components[vcv]
|
|
826
|
-
else:
|
|
827
|
-
Scanner.print_stderr(f'Warning: Unknown value: {v}')
|
|
828
|
-
else:
|
|
829
|
-
success = False
|
|
830
|
-
batch_files = 0
|
|
831
|
-
wfp = ''
|
|
832
|
-
if file_print:
|
|
833
|
-
wfp += file_print # Store the WFP for the current file
|
|
834
|
-
if wfp:
|
|
835
|
-
self.print_debug(
|
|
836
|
-
f'Sending {batch_files} ({cur_files}) of'
|
|
837
|
-
f' {file_count} ({len(wfp.encode("utf-8"))} bytes) files to the ScanOSS API.'
|
|
838
|
-
)
|
|
839
|
-
scan_resp = self.scanoss_api.scan(wfp, max_component['name']) # Scan current WFP and store
|
|
840
|
-
if bar:
|
|
841
|
-
bar.next(batch_files)
|
|
842
|
-
first = True
|
|
843
|
-
if scan_resp is not None:
|
|
844
|
-
for key, value in scan_resp.items():
|
|
845
|
-
if first:
|
|
846
|
-
raw_output += ' "%s":%s' % (key, json.dumps(value, indent=2))
|
|
847
|
-
first = False
|
|
848
|
-
else:
|
|
849
|
-
raw_output += ',\n "%s":%s' % (key, json.dumps(value, indent=2))
|
|
850
|
-
else:
|
|
851
|
-
success = False
|
|
852
|
-
raw_output += '\n}'
|
|
853
|
-
if self.output_format == 'plain':
|
|
854
|
-
self.__log_result(raw_output)
|
|
855
|
-
elif self.output_format == 'cyclonedx':
|
|
856
|
-
cdx = CycloneDx(self.debug, self.scan_output)
|
|
857
|
-
cdx.produce_from_str(raw_output)
|
|
858
|
-
elif self.output_format == 'spdxlite':
|
|
859
|
-
spdxlite = SpdxLite(self.debug, self.scan_output)
|
|
860
|
-
success = spdxlite.produce_from_str(raw_output)
|
|
861
|
-
elif self.output_format == 'csv':
|
|
862
|
-
csvo = CsvOutput(self.debug, self.scan_output)
|
|
863
|
-
csvo.produce_from_str(raw_output)
|
|
864
|
-
else:
|
|
865
|
-
self.print_stderr(f'ERROR: Unknown output format: {self.output_format}')
|
|
866
|
-
success = False
|
|
867
|
-
|
|
868
|
-
return success
|
|
869
|
-
|
|
870
|
-
def scan_wfp_with_options(self, wfp: str, deps_file: str, file_map: dict = None) -> bool:
|
|
745
|
+
def scan_wfp_with_options(self, wfp_file: str, deps_file: str, file_map: dict = None) -> bool:
|
|
871
746
|
"""
|
|
872
747
|
Scan the given WFP file for whatever scaning options that have been configured
|
|
873
|
-
:param
|
|
748
|
+
:param wfp_file: WFP file to scan
|
|
874
749
|
:param deps_file: pre-parsed dependency file to decorate
|
|
875
750
|
:param file_map: mapping of obfuscated files back into originals
|
|
876
751
|
:return: True if successful, False otherwise
|
|
877
752
|
"""
|
|
878
753
|
success = True
|
|
879
|
-
|
|
754
|
+
if not wfp_file:
|
|
755
|
+
raise Exception('ERROR: Please specify a WFP file to scan')
|
|
880
756
|
if not os.path.exists(wfp_file) or not os.path.isfile(wfp_file):
|
|
881
757
|
raise Exception(f'ERROR: Specified WFP file does not exist or is not a file: {wfp_file}')
|
|
882
758
|
|
|
883
759
|
if not self.is_file_or_snippet_scan() and not self.is_dependency_scan():
|
|
884
|
-
raise Exception(f'ERROR: No scan options defined to scan WFP: {
|
|
760
|
+
raise Exception(f'ERROR: No scan options defined to scan WFP: {wfp_file}')
|
|
885
761
|
|
|
886
762
|
if self.scan_output:
|
|
887
763
|
self.print_msg(f'Writing results to {self.scan_output}...')
|
|
@@ -896,14 +772,15 @@ class Scanner(ScanossBase):
|
|
|
896
772
|
success = False
|
|
897
773
|
return success
|
|
898
774
|
|
|
899
|
-
def scan_wfp_file_threaded(self,
|
|
775
|
+
def scan_wfp_file_threaded(self, wfp_file: str) -> bool: # noqa: PLR0912
|
|
900
776
|
"""
|
|
901
777
|
Scan the contents of the specified WFP file (threaded)
|
|
902
|
-
:param
|
|
778
|
+
:param wfp_file: WFP file to scan
|
|
903
779
|
return: True if successful, False otherwise
|
|
904
780
|
"""
|
|
905
781
|
success = True
|
|
906
|
-
|
|
782
|
+
if not wfp_file:
|
|
783
|
+
raise Exception('ERROR: Please specify a WFP file to scan')
|
|
907
784
|
if not os.path.exists(wfp_file) or not os.path.isfile(wfp_file):
|
|
908
785
|
raise Exception(f'ERROR: Specified WFP file does not exist or is not a file: {wfp_file}')
|
|
909
786
|
cur_size = 0
|
scanoss/scanossapi.py
CHANGED
|
@@ -78,7 +78,7 @@ class ScanossApi(ScanossBase):
|
|
|
78
78
|
:param api_key: API Key (default None)
|
|
79
79
|
:param debug: Enable debug (default False)
|
|
80
80
|
:param trace: Enable trace (default False)
|
|
81
|
-
:param quiet: Enable
|
|
81
|
+
:param quiet: Enable quiet mode (default False)
|
|
82
82
|
|
|
83
83
|
To set a custom certificate use:
|
|
84
84
|
REQUESTS_CA_BUNDLE=/path/to/cert.pem
|
scanoss/scanossbase.py
CHANGED
scanoss/winnowing.py
CHANGED
|
@@ -37,6 +37,7 @@ from typing import Tuple
|
|
|
37
37
|
from binaryornot.check import is_binary
|
|
38
38
|
from crc32c import crc32c
|
|
39
39
|
|
|
40
|
+
from .header_filter import HeaderFilter
|
|
40
41
|
from .scanossbase import ScanossBase
|
|
41
42
|
|
|
42
43
|
# Winnowing configuration. DO NOT CHANGE.
|
|
@@ -172,6 +173,8 @@ class Winnowing(ScanossBase):
|
|
|
172
173
|
strip_hpsm_ids=None,
|
|
173
174
|
strip_snippet_ids=None,
|
|
174
175
|
skip_md5_ids=None,
|
|
176
|
+
skip_headers: bool = False,
|
|
177
|
+
skip_headers_limit: int = 0,
|
|
175
178
|
):
|
|
176
179
|
"""
|
|
177
180
|
Instantiate Winnowing class
|
|
@@ -198,7 +201,9 @@ class Winnowing(ScanossBase):
|
|
|
198
201
|
self.strip_hpsm_ids = strip_hpsm_ids
|
|
199
202
|
self.strip_snippet_ids = strip_snippet_ids
|
|
200
203
|
self.hpsm = hpsm
|
|
204
|
+
self.skip_headers = skip_headers
|
|
201
205
|
self.is_windows = platform.system() == 'Windows'
|
|
206
|
+
self.header_filter = HeaderFilter(debug=debug, trace=trace, quiet=quiet, skip_limit=skip_headers_limit)
|
|
202
207
|
if hpsm:
|
|
203
208
|
self.crc8_maxim_dow_table = []
|
|
204
209
|
self.crc8_generate_table()
|
|
@@ -353,6 +358,48 @@ class Winnowing(ScanossBase):
|
|
|
353
358
|
self.print_debug(f'Stripped snippet ids from {file}')
|
|
354
359
|
return wfp
|
|
355
360
|
|
|
361
|
+
def __strip_lines_until_offset(self, file: str, wfp: str, line_offset: int) -> str:
|
|
362
|
+
"""
|
|
363
|
+
Strip lines from the WFP up to and including the line_offset
|
|
364
|
+
|
|
365
|
+
:param file: name of fingerprinted file
|
|
366
|
+
:param wfp: WFP to clean
|
|
367
|
+
:param line_offset: line number offset to strip up to
|
|
368
|
+
:return: updated WFP
|
|
369
|
+
"""
|
|
370
|
+
# No offset specified, return original WFP
|
|
371
|
+
if line_offset <= 0:
|
|
372
|
+
return wfp
|
|
373
|
+
lines = wfp.split('\n')
|
|
374
|
+
filtered_lines = []
|
|
375
|
+
start_line_added = False
|
|
376
|
+
for line in lines:
|
|
377
|
+
# Check if a line contains snippet data (format: line_number=hash,hash,...)
|
|
378
|
+
line_details = line.split('=')
|
|
379
|
+
if line_details[0].isdigit():
|
|
380
|
+
try:
|
|
381
|
+
line_num = int(line_details[0])
|
|
382
|
+
# Keep lines that are after the offset
|
|
383
|
+
# (line_offset is the last line previous to real code)
|
|
384
|
+
if line_num > line_offset:
|
|
385
|
+
# Add the start_line tag before the first snippet line
|
|
386
|
+
if not start_line_added:
|
|
387
|
+
filtered_lines.append(f'start_line={line_offset}')
|
|
388
|
+
start_line_added = True
|
|
389
|
+
filtered_lines.append(line)
|
|
390
|
+
except (ValueError, IndexError) as e:
|
|
391
|
+
self.print_stderr(f'Error decoding line number from line {line} in {file}: {e}')
|
|
392
|
+
# Keep non-snippet lines (like file=, hpsm=, etc.)
|
|
393
|
+
filtered_lines.append(line)
|
|
394
|
+
else:
|
|
395
|
+
# Keep non-snippet lines (like file=, hpsm=, etc.)
|
|
396
|
+
filtered_lines.append(line)
|
|
397
|
+
# End for loop comment
|
|
398
|
+
wfp = '\n'.join(filtered_lines)
|
|
399
|
+
if start_line_added:
|
|
400
|
+
self.print_debug(f'Stripped lines up to offset {line_offset} from {file}')
|
|
401
|
+
return wfp
|
|
402
|
+
|
|
356
403
|
def __detect_line_endings(self, contents: bytes) -> Tuple[bool, bool, bool]:
|
|
357
404
|
"""Detect the types of line endings present in file contents.
|
|
358
405
|
|
|
@@ -362,13 +409,14 @@ class Winnowing(ScanossBase):
|
|
|
362
409
|
Returns:
|
|
363
410
|
Tuple of (has_crlf, has_lf_only, has_cr_only, has_mixed) indicating which line ending types are present.
|
|
364
411
|
"""
|
|
412
|
+
if not contents:
|
|
413
|
+
self.print_debug('Warning: No file contents provided')
|
|
365
414
|
has_crlf = b'\r\n' in contents
|
|
366
415
|
# For LF detection, we need to find LF that's not part of CRLF
|
|
367
416
|
content_without_crlf = contents.replace(b'\r\n', b'')
|
|
368
417
|
has_standalone_lf = b'\n' in content_without_crlf
|
|
369
418
|
# For CR detection, we need to find CR that's not part of CRLF
|
|
370
419
|
has_standalone_cr = b'\r' in content_without_crlf
|
|
371
|
-
|
|
372
420
|
return has_crlf, has_standalone_lf, has_standalone_cr
|
|
373
421
|
|
|
374
422
|
def __calculate_opposite_line_ending_hash(self, contents: bytes):
|
|
@@ -384,13 +432,11 @@ class Winnowing(ScanossBase):
|
|
|
384
432
|
Hash with opposite line endings as hex string, or None if no line endings detected.
|
|
385
433
|
"""
|
|
386
434
|
has_crlf, has_standalone_lf, has_standalone_cr = self.__detect_line_endings(contents)
|
|
387
|
-
|
|
388
435
|
if not has_crlf and not has_standalone_lf and not has_standalone_cr:
|
|
436
|
+
self.print_debug('No line endings detected in file contents')
|
|
389
437
|
return None
|
|
390
|
-
|
|
391
|
-
# Normalize all line endings to LF first
|
|
438
|
+
# Normalise all line endings to LF first
|
|
392
439
|
normalized = contents.replace(b'\r\n', b'\n').replace(b'\r', b'\n')
|
|
393
|
-
|
|
394
440
|
# Determine the dominant line ending type
|
|
395
441
|
if has_crlf and not has_standalone_lf and not has_standalone_cr:
|
|
396
442
|
# File is Windows (CRLF) - produce Unix (LF) hash
|
|
@@ -398,7 +444,7 @@ class Winnowing(ScanossBase):
|
|
|
398
444
|
else:
|
|
399
445
|
# File is Unix (LF/CR) or mixed - produce Windows (CRLF) hash
|
|
400
446
|
opposite_contents = normalized.replace(b'\n', b'\r\n')
|
|
401
|
-
|
|
447
|
+
# Return the MD5 hash of the opposite contents
|
|
402
448
|
return hashlib.md5(opposite_contents).hexdigest()
|
|
403
449
|
|
|
404
450
|
def wfp_for_contents(self, file: str, bin_file: bool, contents: bytes) -> str: # noqa: PLR0912, PLR0915
|
|
@@ -420,27 +466,26 @@ class Winnowing(ScanossBase):
|
|
|
420
466
|
# Print file line
|
|
421
467
|
content_length = len(contents)
|
|
422
468
|
original_filename = file
|
|
423
|
-
|
|
424
469
|
if self.is_windows:
|
|
425
470
|
original_filename = file.replace('\\', '/')
|
|
426
471
|
wfp_filename = repr(original_filename).strip("'") # return a utf-8 compatible version of the filename
|
|
427
|
-
|
|
472
|
+
# hide the real size of the file and its name but keep the suffix
|
|
473
|
+
if self.obfuscate:
|
|
428
474
|
wfp_filename = f'{self.ob_count}{pathlib.Path(original_filename).suffix}'
|
|
429
475
|
self.ob_count = self.ob_count + 1
|
|
430
476
|
self.file_map[wfp_filename] = original_filename # Save the file name map for later (reverse lookup)
|
|
431
|
-
|
|
477
|
+
# Construct the WFP header
|
|
432
478
|
wfp = 'file={0},{1},{2}\n'.format(file_md5, content_length, wfp_filename)
|
|
433
|
-
|
|
434
|
-
# Add opposite line ending hash based on line ending analysis
|
|
479
|
+
# Add the opposite line ending hash based on line ending analysis
|
|
435
480
|
if not bin_file:
|
|
436
481
|
opposite_hash = self.__calculate_opposite_line_ending_hash(contents)
|
|
437
482
|
if opposite_hash is not None:
|
|
438
483
|
wfp += f'fh2={opposite_hash}\n'
|
|
439
|
-
|
|
440
484
|
# We don't process snippets for binaries, or other uninteresting files, or if we're requested to skip
|
|
441
|
-
|
|
485
|
+
decoded_contents = contents.decode('utf-8', 'ignore')
|
|
486
|
+
if bin_file or self.skip_snippets or self.__skip_snippets(file, decoded_contents):
|
|
442
487
|
return wfp
|
|
443
|
-
# Add HPSM
|
|
488
|
+
# Add HPSM (calculated from original contents, not filtered)
|
|
444
489
|
if self.hpsm:
|
|
445
490
|
hpsm = self.__strip_hpsm(file, self.calc_hpsm(contents))
|
|
446
491
|
if len(hpsm) > 0:
|
|
@@ -448,7 +493,7 @@ class Winnowing(ScanossBase):
|
|
|
448
493
|
# Initialize variables
|
|
449
494
|
gram = ''
|
|
450
495
|
window = []
|
|
451
|
-
line = 1
|
|
496
|
+
line = 1 # Line counter for WFP generation
|
|
452
497
|
last_hash = MAX_CRC32
|
|
453
498
|
last_line = 0
|
|
454
499
|
output = ''
|
|
@@ -503,12 +548,19 @@ class Winnowing(ScanossBase):
|
|
|
503
548
|
wfp += output + '\n'
|
|
504
549
|
else:
|
|
505
550
|
self.print_debug(f'Warning: skipping output in WFP for {file} - "{output}"')
|
|
506
|
-
|
|
551
|
+
# Warn if we don't have any WFP content
|
|
507
552
|
if wfp is None or wfp == '':
|
|
508
553
|
self.print_stderr(f'Warning: No WFP content data for {file}')
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
554
|
+
else:
|
|
555
|
+
# Apply line filter to remove headers, comments, and imports from the beginning (if enabled)
|
|
556
|
+
if self.skip_headers:
|
|
557
|
+
line_offset = self.header_filter.filter(file, decoded_contents)
|
|
558
|
+
if line_offset > 0:
|
|
559
|
+
wfp = self.__strip_lines_until_offset(file, wfp, line_offset)
|
|
560
|
+
# Strip snippet IDs from the WFP (if enabled)
|
|
561
|
+
if self.strip_snippet_ids:
|
|
562
|
+
wfp = self.__strip_snippets(file, wfp)
|
|
563
|
+
# Return the WFP contents
|
|
512
564
|
return wfp
|
|
513
565
|
|
|
514
566
|
def calc_hpsm(self, content):
|
|
@@ -6,8 +6,8 @@ protoc_gen_swagger/options/annotations_pb2_grpc.py,sha256=KZOW9Ciio-f9iL42FuLFnS
|
|
|
6
6
|
protoc_gen_swagger/options/openapiv2_pb2.py,sha256=w0xDs63uyrWGgzRaQZXfJpfI7Jpyvh-i9ay_uzOR-aM,16475
|
|
7
7
|
protoc_gen_swagger/options/openapiv2_pb2.pyi,sha256=hYOV6uQ2yqhP89042_V3GuAsvoBBiXf5CGuYmnFnfv4,54665
|
|
8
8
|
protoc_gen_swagger/options/openapiv2_pb2_grpc.py,sha256=sje9Nh3yE7CHCUWZwtjTgwsKB4GvyGz5vOrGTnRXJfc,917
|
|
9
|
-
scanoss/__init__.py,sha256=
|
|
10
|
-
scanoss/cli.py,sha256=
|
|
9
|
+
scanoss/__init__.py,sha256=NOzcEaG8ME9k2ZMAfMdsNT1pM14UeAeJtZFkX0JT9Tc,1146
|
|
10
|
+
scanoss/cli.py,sha256=CEj2soeZ5FjYsAqkdt-nbviuwoPstjLBEs6-WIz5jbQ,104238
|
|
11
11
|
scanoss/components.py,sha256=NFyt_w3aoMotr_ZaFU-ng00_89sruc0kgY7ERnJXkmM,15891
|
|
12
12
|
scanoss/constants.py,sha256=vurzLNIfP_dnRMwOdZsUWvr5XAVuGoj98XZ0yjXNOjQ,632
|
|
13
13
|
scanoss/cryptography.py,sha256=lOoD_dW16ARQxYiYyb5R8S7gx0FqWIsnGkKfsB0nGaU,10627
|
|
@@ -17,20 +17,21 @@ scanoss/delta.py,sha256=slmgnD7SsUOmfSE2zb0zdRAGo-JcjPJAtxyzuCSzO3I,9455
|
|
|
17
17
|
scanoss/file_filters.py,sha256=QcLqunaBKQIafjNZ9_Snh9quBX5_-fsTusVmxwjC1q8,18511
|
|
18
18
|
scanoss/filecount.py,sha256=icWaKN_xapMrH3ZZ-D3nldx7hWiguIOjoKg4gCeKDOM,6678
|
|
19
19
|
scanoss/gitlabqualityreport.py,sha256=_VG0Xoh8wYF3lsXGJvjoj-Ty58OS_-H1Domiq9OpQEo,8830
|
|
20
|
+
scanoss/header_filter.py,sha256=-Dqore9coROLMWWw9yP3nz8dpCB7jYAVm842hoRTmeE,21879
|
|
20
21
|
scanoss/osadl.py,sha256=VWalcHpshWxtRDGje2cK32SfFeSBAO62knfSW9pyYqc,4558
|
|
21
22
|
scanoss/results.py,sha256=47ZXXuU2sDjYa5vhtbWTmikit9jHhA0rsYKwkvZFI5w,9252
|
|
22
23
|
scanoss/scancodedeps.py,sha256=JbpoGW1POtPMmowzfwa4oh8sSBeeQCqaW9onvc4UFYM,11517
|
|
23
|
-
scanoss/scanner.py,sha256=
|
|
24
|
+
scanoss/scanner.py,sha256=3dkkNwi4KSHeBMk0Pmjf3WJ6SA28-om2iGxuDsqnfGg,39778
|
|
24
25
|
scanoss/scanoss_settings.py,sha256=W8uFQ6uRIqtE-DXXA56bO8I4GsbJ-aA1c84hQ_qBel4,12161
|
|
25
|
-
scanoss/scanossapi.py,sha256=
|
|
26
|
-
scanoss/scanossbase.py,sha256=
|
|
26
|
+
scanoss/scanossapi.py,sha256=O1ZNH9Kt8JzhLVBxfOSmJdEwSJTDP-rA54DulYdE8e4,13243
|
|
27
|
+
scanoss/scanossbase.py,sha256=tKlHPAi50ZarGaPXsNi1XrowQBynsSqSSst-NuG2ScI,3163
|
|
27
28
|
scanoss/scanossgrpc.py,sha256=9UuVPUjBLUhqim_tSntyoRZW-OAtiz5iP_VjjNr5RPY,41715
|
|
28
29
|
scanoss/scanpostprocessor.py,sha256=-JsThlxrU70r92GHykTMERnicdd-6jmwNsE4PH0MN2o,11063
|
|
29
30
|
scanoss/scantype.py,sha256=gFmyVmKQpHWogN2iCmMj032e_sZo4T92xS3_EH5B3Tc,1310
|
|
30
31
|
scanoss/spdxlite.py,sha256=4JMxmyNmvcL6fjScihk8toWfSuQ-Pj1gzaT3SIn1fXA,29425
|
|
31
32
|
scanoss/threadeddependencies.py,sha256=aN8E43iKS1pWJLJP3xCle5ewlfR5DE2-ljUzI_29Xwk,9851
|
|
32
33
|
scanoss/threadedscanning.py,sha256=Y-OYamD3xJvFiqwCn5y_4QD5gk_rJ5xs2jI1DxNtJlc,9661
|
|
33
|
-
scanoss/winnowing.py,sha256=
|
|
34
|
+
scanoss/winnowing.py,sha256=py4gFKVHI5ZsLNyQIvNtnO6k3tBbvEXYNw24yuMgoTc,24751
|
|
34
35
|
scanoss/api/__init__.py,sha256=hx-P78xbDsh6WQIigewkJ7Y7y1fqc_eYnyHC5IZTKmo,1122
|
|
35
36
|
scanoss/api/common/__init__.py,sha256=hx-P78xbDsh6WQIigewkJ7Y7y1fqc_eYnyHC5IZTKmo,1122
|
|
36
37
|
scanoss/api/common/v2/__init__.py,sha256=hx-P78xbDsh6WQIigewkJ7Y7y1fqc_eYnyHC5IZTKmo,1122
|
|
@@ -66,7 +67,7 @@ scanoss/api/vulnerabilities/__init__.py,sha256=IFrDk_DTJgKSZmmU-nuLXuq_s8sQZlrSC
|
|
|
66
67
|
scanoss/api/vulnerabilities/v2/__init__.py,sha256=IFrDk_DTJgKSZmmU-nuLXuq_s8sQZlrSCHhIDMJT4r0,1122
|
|
67
68
|
scanoss/api/vulnerabilities/v2/scanoss_vulnerabilities_pb2.py,sha256=pmm0MSiXkdf8e4rCIIDRcsNRixR2vGvD1Xak4l-wdwI,16550
|
|
68
69
|
scanoss/api/vulnerabilities/v2/scanoss_vulnerabilities_pb2_grpc.py,sha256=BNxT5kUKQ-mgtOt5QYBM1Qrg5LNDqSpWKpfEZquIlsM,19127
|
|
69
|
-
scanoss/data/build_date.txt,sha256=
|
|
70
|
+
scanoss/data/build_date.txt,sha256=9ZRq_fVViEv4K8-UYAx-AhWlfHhrDEekJ0NIIqXD2tM,40
|
|
70
71
|
scanoss/data/osadl-copyleft.json,sha256=O9b2XAfpjQY0TL0fYzO6kwMcp5IwQbF6f_YWbB10MhQ,4761
|
|
71
72
|
scanoss/data/scanoss-settings-schema.json,sha256=ClkRYAkjAN0Sk704G8BE_Ok006oQ6YnIGmX84CF8h9w,8798
|
|
72
73
|
scanoss/data/spdx-exceptions.json,sha256=s7UTYxC7jqQXr11YBlIWYCNwN6lRDFTR33Y8rpN_dA4,17953
|
|
@@ -101,9 +102,9 @@ scanoss/utils/crc64.py,sha256=TMrwQimSdE6imhFOUL7oAG6Kxu-8qMpGWMuMg8QpSVs,3169
|
|
|
101
102
|
scanoss/utils/file.py,sha256=62cA9a17TU9ZvfA3FY5HY4-QOajJeSrc8S6xLA_f-3M,2980
|
|
102
103
|
scanoss/utils/scanoss_scan_results_utils.py,sha256=ho9-DKefHFJlVZkw4gXOmMI-mgPIbV9Y2ftkI83fC1k,1727
|
|
103
104
|
scanoss/utils/simhash.py,sha256=6iu8DOcecPAY36SZjCOzrrLMT9oIE7-gI6QuYwUQ7B0,5793
|
|
104
|
-
scanoss-1.
|
|
105
|
-
scanoss-1.
|
|
106
|
-
scanoss-1.
|
|
107
|
-
scanoss-1.
|
|
108
|
-
scanoss-1.
|
|
109
|
-
scanoss-1.
|
|
105
|
+
scanoss-1.43.0.dist-info/licenses/LICENSE,sha256=LLUaXoiyOroIbr5ubAyrxBOwSRLTm35ETO2FmLpy8QQ,1074
|
|
106
|
+
scanoss-1.43.0.dist-info/METADATA,sha256=R7fUdlsRnWStAXPzqagWcXwUtHasnNefCplWUyHe284,6181
|
|
107
|
+
scanoss-1.43.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
108
|
+
scanoss-1.43.0.dist-info/entry_points.txt,sha256=Uy28xnaDL5KQ7V77sZD5VLDXPNxYYzSr5tsqtiXVzAs,48
|
|
109
|
+
scanoss-1.43.0.dist-info/top_level.txt,sha256=V11PrQ6Pnrc-nDF9xnisnJ8e6-i7HqSIKVNqduRWcL8,27
|
|
110
|
+
scanoss-1.43.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|