scanoss 1.41.0__py3-none-any.whl → 1.42.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 CHANGED
@@ -22,4 +22,4 @@ SPDX-License-Identifier: MIT
22
22
  THE SOFTWARE.
23
23
  """
24
24
 
25
- __version__ = '1.41.0'
25
+ __version__ = '1.42.0'
scanoss/cli.py CHANGED
@@ -1096,6 +1096,19 @@ def setup_args() -> None: # noqa: PLR0912, PLR0915
1096
1096
  p.add_argument('--skip-md5', '-5', type=str, action='append', help='Skip files matching MD5.')
1097
1097
  p.add_argument('--strip-hpsm', '-G', type=str, action='append', help='Strip HPSM string from WFP.')
1098
1098
  p.add_argument('--strip-snippet', '-N', type=str, action='append', help='Strip Snippet ID string from WFP.')
1099
+ p.add_argument(
1100
+ '--skip-headers',
1101
+ '-skh',
1102
+ action='store_true',
1103
+ help='Skip license headers, comments and imports at the beginning of files.',
1104
+ )
1105
+ p.add_argument(
1106
+ '--skip-headers-limit',
1107
+ '-shl',
1108
+ type=int,
1109
+ default=0,
1110
+ help='Maximum number of lines to skip when filtering headers (default: 0 = no limit).',
1111
+ )
1099
1112
 
1100
1113
  # Global Scan/GRPC options
1101
1114
  for p in [
@@ -1388,6 +1401,8 @@ def wfp(parser, args):
1388
1401
  strip_hpsm_ids=args.strip_hpsm,
1389
1402
  strip_snippet_ids=args.strip_snippet,
1390
1403
  scan_settings=scan_settings,
1404
+ skip_headers=args.skip_headers,
1405
+ skip_headers_limit=args.skip_headers_limit,
1391
1406
  )
1392
1407
  if args.stdin:
1393
1408
  contents = sys.stdin.buffer.read()
@@ -1583,6 +1598,8 @@ def scan(parser, args): # noqa: PLR0912, PLR0915
1583
1598
  scan_settings=scan_settings,
1584
1599
  req_headers=process_req_headers(args.header),
1585
1600
  use_grpc=args.grpc,
1601
+ skip_headers=args.skip_headers,
1602
+ skip_headers_limit=args.skip_headers_limit,
1586
1603
  )
1587
1604
  if args.wfp:
1588
1605
  if not scanner.is_file_or_snippet_scan():
@@ -1 +1 @@
1
- date: 20251117160705, utime: 1763395625
1
+ date: 20251218123159, utime: 1766061119
@@ -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
@@ -109,6 +109,8 @@ class Scanner(ScanossBase):
109
109
  scan_settings: 'ScanossSettings | None' = None,
110
110
  req_headers: dict = None,
111
111
  use_grpc: bool = False,
112
+ skip_headers: bool = False,
113
+ skip_headers_limit: int = 0,
112
114
  ):
113
115
  """
114
116
  Initialise scanning class, including Winnowing, ScanossApi, ThreadedScanning
@@ -137,6 +139,7 @@ class Scanner(ScanossBase):
137
139
 
138
140
  self.winnowing = Winnowing(
139
141
  debug=debug,
142
+ trace=trace,
140
143
  quiet=quiet,
141
144
  skip_snippets=self._skip_snippets,
142
145
  all_extensions=all_extensions,
@@ -145,6 +148,8 @@ class Scanner(ScanossBase):
145
148
  strip_hpsm_ids=strip_hpsm_ids,
146
149
  strip_snippet_ids=strip_snippet_ids,
147
150
  skip_md5_ids=skip_md5_ids,
151
+ skip_headers=skip_headers,
152
+ skip_headers_limit=skip_headers_limit,
148
153
  )
149
154
  self.scanoss_api = ScanossApi(
150
155
  debug=debug,
@@ -218,7 +218,7 @@ class ScannerHFHPresenter(AbstractPresenter):
218
218
  }
219
219
 
220
220
  get_vulnerabilities_json_request = {
221
- 'purls': [{'purl': purl, 'requirement': best_match_version['version']}],
221
+ 'components': [{'purl': purl, 'requirement': best_match_version['version']}],
222
222
  }
223
223
 
224
224
  decorated_scan_results = self.scanner.client.get_dependencies(get_dependencies_json_request)
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 quite mode (default False)
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
@@ -50,7 +50,7 @@ class ScanossBase:
50
50
 
51
51
  def print_msg(self, *args, **kwargs):
52
52
  """
53
- Print message if quite mode is not enabled
53
+ Print message if quiet mode is not enabled
54
54
  """
55
55
  if not self.quiet:
56
56
  self.print_stderr(*args, **kwargs)
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
- if self.obfuscate: # hide the real size of the file and its name, but keep the suffix
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
- if bin_file or self.skip_snippets or self.__skip_snippets(file, contents.decode('utf-8', 'ignore')):
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
- elif self.strip_snippet_ids:
510
- wfp = self.__strip_snippets(file, wfp)
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):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scanoss
3
- Version: 1.41.0
3
+ Version: 1.42.0
4
4
  Summary: Simple Python library to leverage the SCANOSS APIs
5
5
  Home-page: https://scanoss.com
6
6
  Author: SCANOSS
@@ -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=gwiLnpxvm3FTkfCt7CRIdD8To8AjOpOG_5oXLMvgpOk,1146
10
- scanoss/cli.py,sha256=YAE9ihA2LswSLHBRHnpyqM-e2tcE7lPd8-ghABbnyU8,103984
9
+ scanoss/__init__.py,sha256=9ZAHE5MaLo7fYIxovUewqgv-BFsZ7YIIgJalSwDtGc4,1146
10
+ scanoss/cli.py,sha256=cHkms9nVYniZMAT184VkKQTcd7SL1SEnByxPSAsBVwo,104610
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=P2gswXapSVYC-tH6_oY763DYpe9Awi2g9xW1U9WGgQI,46374
24
+ scanoss/scanner.py,sha256=E71i7sni3hc22b0Mpd2NyifajmX0YOkRyUGBJiiN58o,46562
24
25
  scanoss/scanoss_settings.py,sha256=W8uFQ6uRIqtE-DXXA56bO8I4GsbJ-aA1c84hQ_qBel4,12161
25
- scanoss/scanossapi.py,sha256=U3IytJXA0mQHYsylzjwCuFakuhD_dJhstY37NmyvtB8,13243
26
- scanoss/scanossbase.py,sha256=Dkpwxa8NH8XN1iRl03NM_Mkvby0JQ4qfvCiiUrJ5ul0,3163
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=RsR9jRTR3TzS1pEeKQ2RuYlIG8Q7RnUQFfgPLog6B-A,21679
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=9AXzlvNjRrd9YhQ9pyrxQftmN0vVNptg1mAwlPWlkuY,40
70
+ scanoss/data/build_date.txt,sha256=e-9ZOSAGNgP3bfrxejydpbf1_yVzmVCB3S0XOeP7nOs,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
@@ -93,7 +94,7 @@ scanoss/scanners/__init__.py,sha256=D4C0lWLuNp8k_BjQZEc07WZcUgAvriVwQWOk063b0ZU,
93
94
  scanoss/scanners/container_scanner.py,sha256=fOrb64owrstX7LnTuxiIan059YgLeKXeBS6g2QaCyq0,16346
94
95
  scanoss/scanners/folder_hasher.py,sha256=UzOmtYTLMqeL3Jf4CpvT-L4qaPNCy9-7xV0BwYhAuRc,12973
95
96
  scanoss/scanners/scanner_config.py,sha256=egG7cw3S2akU-D9M1aLE5jLrfz_c8e7_DIotMnnpM84,2601
96
- scanoss/scanners/scanner_hfh.py,sha256=8CAYZ0FGFSW8Dcp6ci0ZV6bxphR9D_WxMY17fvalICk,9507
97
+ scanoss/scanners/scanner_hfh.py,sha256=gn2DnWD0J1_EPAKm5Zmu6ZjbGBPPSdz0YjqcPESPE-c,9512
97
98
  scanoss/services/dependency_track_service.py,sha256=JIpqev4I-x_ZajMxD5W2Y3OAUvEJ_4nstzAPV90vfP8,5070
98
99
  scanoss/utils/__init__.py,sha256=0hjb5ktavp7utJzFhGMPImPaZiHWgilM2HwvTp5lXJE,1122
99
100
  scanoss/utils/abstract_presenter.py,sha256=teiDTxBj5jBMCk2T8i4l1BJPf_u4zBLWrtCTFHSSECM,3148
@@ -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.41.0.dist-info/licenses/LICENSE,sha256=LLUaXoiyOroIbr5ubAyrxBOwSRLTm35ETO2FmLpy8QQ,1074
105
- scanoss-1.41.0.dist-info/METADATA,sha256=PlxJlKB07rRGpvxVduJV_McBVlNdWWtBGcoFl50d2Xc,6181
106
- scanoss-1.41.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
107
- scanoss-1.41.0.dist-info/entry_points.txt,sha256=Uy28xnaDL5KQ7V77sZD5VLDXPNxYYzSr5tsqtiXVzAs,48
108
- scanoss-1.41.0.dist-info/top_level.txt,sha256=V11PrQ6Pnrc-nDF9xnisnJ8e6-i7HqSIKVNqduRWcL8,27
109
- scanoss-1.41.0.dist-info/RECORD,,
105
+ scanoss-1.42.0.dist-info/licenses/LICENSE,sha256=LLUaXoiyOroIbr5ubAyrxBOwSRLTm35ETO2FmLpy8QQ,1074
106
+ scanoss-1.42.0.dist-info/METADATA,sha256=qIMoqDLLS5apaE0c3zuoEFb5NYY9qwupeH5pbshUkc4,6181
107
+ scanoss-1.42.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
108
+ scanoss-1.42.0.dist-info/entry_points.txt,sha256=Uy28xnaDL5KQ7V77sZD5VLDXPNxYYzSr5tsqtiXVzAs,48
109
+ scanoss-1.42.0.dist-info/top_level.txt,sha256=V11PrQ6Pnrc-nDF9xnisnJ8e6-i7HqSIKVNqduRWcL8,27
110
+ scanoss-1.42.0.dist-info/RECORD,,