Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,26 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added
- Upcoming changes...
- Added `--license-sources` (`-ls`) option to copyleft inspection
- Filter which license sources to check (component_declared, license_file, file_header, file_spdx_tag, scancode)
- Supports both `-ls source1 source2` and `-ls source1 -ls source2` syntax

### Changed
- **Switched to OSADL authoritative copyleft license data**
- Copyleft detection now uses [OSADL (Open Source Automation Development Lab)](https://www.osadl.org/) checklist data
- Adds missing `-or-later` license variants (GPL-2.0-or-later, GPL-3.0-or-later, LGPL-2.1-or-later, etc.)
- Expands copyleft coverage from 21 to 32 licenses
- Custom include/exclude/explicit filters still use legacy behavior for backward compatibility
- Dataset attribution added to README (CC-BY-4.0 license)

- Copyleft inspection now defaults to component-level licenses only (component_declared, license_file)
- Reduces noise from file-level license detections (file_header, scancode)
- Use `-ls` to override and check specific sources

### Fixed
- Fixed the terminal cursor disappearing after aborting scan with Ctrl+C

## [1.40.1] - 2025-10-29
### Changed
Expand Down
12 changes: 11 additions & 1 deletion LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,14 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
THE SOFTWARE.

===============================================================================

In addition, this repository includes an unmodified copy of the OSADL copyleft
license checklist data (src/scanoss/data/osadl-copyleft.json), which is
licensed under the Creative Commons Attribution 4.0 International license
(CC-BY-4.0) by the Open Source Automation Development Lab (OSADL) eG.

The OSADL data file contains its own license header with full attribution
information.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,3 +135,8 @@ Details of major changes to the library can be found in [CHANGELOG.md](CHANGELOG

## Background
Details about the Winnowing algorithm used for scanning can be found [here](WINNOWING.md).

## Dataset License Notice
This application is licensed under the MIT License. In addition, it includes an unmodified copy of the OSADL copyleft license dataset ([osadl-copyleft.json](src/scanoss/data/osadl-copyleft.json)) which is licensed under the [Creative Commons Attribution 4.0 International license (CC-BY-4.0)](https://creativecommons.org/licenses/by/4.0/) by the [Open Source Automation Development Lab (OSADL) eG](https://www.osadl.org/).

**Attribution:** A project by the Open Source Automation Development Lab (OSADL) eG. Original source: [https://www.osadl.org/fileadmin/checklists/copyleft.json](https://www.osadl.org/fileadmin/checklists/copyleft.json)
2 changes: 1 addition & 1 deletion src/scanoss/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,4 @@
THE SOFTWARE.
"""

__version__ = '1.40.1'
__version__ = '1.41.0'
14 changes: 14 additions & 0 deletions src/scanoss/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
from .components import Components
from .constants import (
DEFAULT_API_TIMEOUT,
DEFAULT_COPYLEFT_LICENSE_SOURCES,
DEFAULT_HFH_DEPTH,
DEFAULT_HFH_MIN_ACCEPTED_SCORE,
DEFAULT_HFH_RANK_THRESHOLD,
Expand All @@ -64,6 +65,7 @@
DEFAULT_TIMEOUT,
MIN_TIMEOUT,
PYTHON_MAJOR_VERSION,
VALID_LICENSE_SOURCES,
)
from .csvoutput import CsvOutput
from .cyclonedx import CycloneDx
Expand Down Expand Up @@ -699,6 +701,17 @@ def setup_args() -> None: # noqa: PLR0912, PLR0915
p.add_argument('--exclude', help='Licenses to exclude from analysis (comma-separated list)')
p.add_argument('--explicit', help='Use only these specific licenses for analysis (comma-separated list)')

# License source filtering
for p in [p_inspect_raw_copyleft, p_inspect_legacy_copyleft]:
p.add_argument(
'-ls', '--license-sources',
action='extend',
nargs='+',
choices=VALID_LICENSE_SOURCES,
help=f'Specify which license sources to check for copyleft violations. Each license object in scan results '
f'has a source field indicating its origin. Default: {", ".join(DEFAULT_COPYLEFT_LICENSE_SOURCES)}',
)

# Common options for (legacy) copyleft and undeclared component inspection
for p in [p_inspect_raw_copyleft, p_inspect_raw_undeclared, p_inspect_legacy_copyleft, p_inspect_legacy_undeclared]:
p.add_argument('-i', '--input', nargs='?', help='Path to scan results file to analyse')
Expand Down Expand Up @@ -1752,6 +1765,7 @@ def inspect_copyleft(parser, args):
include=args.include, # Additional licenses to check
exclude=args.exclude, # Licenses to ignore
explicit=args.explicit, # Explicit license list
license_sources=args.license_sources, # License sources to check (list)
)
# Execute inspection and exit with appropriate status code
status, _ = i_copyleft.run()
Expand Down
3 changes: 3 additions & 0 deletions src/scanoss/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,6 @@
DEFAULT_HFH_DEPTH = 1
DEFAULT_HFH_RECURSIVE_THRESHOLD = 0.8
DEFAULT_HFH_MIN_ACCEPTED_SCORE = 0.15

VALID_LICENSE_SOURCES = ['component_declared', 'license_file', 'file_header', 'file_spdx_tag', 'scancode']
DEFAULT_COPYLEFT_LICENSE_SOURCES = ['component_declared', 'license_file']
133 changes: 133 additions & 0 deletions src/scanoss/data/osadl-copyleft.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
{
"title": "OSADL Open Source License Obligations Checklist (https:\/\/www.osadl.org\/Checklists)",
"license": "Creative Commons Attribution 4.0 International license (CC-BY-4.0)",
"attribution": "A project by the Open Source Automation Development Lab (OSADL) eG. For further information about the project see the description at www.osadl.org\/checklists.",
"copyright": "(C) 2017 - 2024 Open Source Automation Development Lab (OSADL) eG and contributors, info@osadl.org",
"disclaimer": "The checklists and particularly the copyleft data have been assembled with maximum diligence and care; however, the authors do not warrant nor can be held liable in any way for its correctness, usefulness, merchantibility or fitness for a particular purpose as far as permissible by applicable law. Anyone who uses the information does this on his or her sole responsibility. For any individual legal advice, it is recommended to contact a lawyer.",
"timeformat": "%Y-%m-%dT%H:%M:%S%z",
"timestamp": "2025-10-30T11:23:00+0000",
"copyleft":
{
"0BSD": "No",
"AFL-2.0": "No",
"AFL-2.1": "No",
"AFL-3.0": "No",
"AGPL-3.0-only": "Yes",
"AGPL-3.0-or-later": "Yes",
"Apache-1.0": "No",
"Apache-1.1": "No",
"Apache-2.0": "No",
"APSL-2.0": "Yes (restricted)",
"Artistic-1.0": "No",
"Artistic-1.0-Perl": "No",
"Artistic-2.0": "No",
"Bitstream-Vera": "No",
"blessing": "No",
"BlueOak-1.0.0": "No",
"BSD-1-Clause": "No",
"BSD-2-Clause": "No",
"BSD-2-Clause-Patent": "No",
"BSD-3-Clause": "No",
"BSD-3-Clause-Open-MPI": "No",
"BSD-4-Clause": "No",
"BSD-4-Clause-UC": "No",
"BSD-4.3TAHOE": "No",
"BSD-Source-Code": "No",
"BSL-1.0": "No",
"bzip2-1.0.5": "No",
"bzip2-1.0.6": "No",
"CC-BY-2.5": "No",
"CC-BY-3.0": "No",
"CDDL-1.0": "Yes (restricted)",
"CDDL-1.1": "Yes (restricted)",
"CPL-1.0": "Yes",
"curl": "No",
"ECL-1.0": "No",
"ECL-2.0": "No",
"EFL-2.0": "No",
"EPL-1.0": "Yes",
"EPL-2.0": "Yes (restricted)",
"EUPL-1.1": "Yes",
"EUPL-1.2": "Yes",
"FSFAP": "No",
"FSFUL": "No",
"FSFULLR": "No",
"FSFULLRWD": "No",
"FTL": "No",
"GPL-1.0-only": "Yes",
"GPL-1.0-or-later": "Yes",
"GPL-2.0-only": "Yes",
"GPL-2.0-only WITH Classpath-exception-2.0": "Yes (restricted)",
"GPL-2.0-or-later": "Yes",
"GPL-3.0-only": "Yes",
"GPL-3.0-or-later": "Yes",
"HPND": "No",
"IBM-pibs": "No",
"ICU": "No",
"IJG": "No",
"ImageMagick": "No",
"Info-ZIP": "No",
"IPL-1.0": "Yes",
"ISC": "No",
"JasPer-2.0": "No",
"LGPL-2.0-only": "Yes (restricted)",
"LGPL-2.0-or-later": "Yes (restricted)",
"LGPL-2.1-only": "Yes (restricted)",
"LGPL-2.1-or-later": "Yes (restricted)",
"LGPL-3.0-only": "Yes (restricted)",
"LGPL-3.0-or-later": "Yes (restricted)",
"Libpng": "No",
"libpng-2.0": "No",
"libtiff": "No",
"LicenseRef-scancode-bsla-no-advert": "No",
"LicenseRef-scancode-info-zip-2003-05": "No",
"LicenseRef-scancode-ppp": "No",
"Minpack": "No",
"MirOS": "No",
"MIT": "No",
"MIT-0": "No",
"MIT-CMU": "No",
"MPL-1.1": "Yes (restricted)",
"MPL-2.0": "Yes (restricted)",
"MPL-2.0-no-copyleft-exception": "Yes (restricted)",
"MS-PL": "Questionable",
"MS-RL": "Yes (restricted)",
"NBPL-1.0": "No",
"NCSA": "No",
"NTP": "No",
"OFL-1.1": "Yes (restricted)",
"OGC-1.0": "No",
"OLDAP-2.8": "No",
"OpenSSL": "Questionable",
"OSL-3.0": "Yes",
"PHP-3.01": "No",
"PostgreSQL": "No",
"PSF-2.0": "No",
"Python-2.0": "No",
"Qhull": "No",
"RSA-MD": "No",
"Saxpath": "No",
"SGI-B-2.0": "No",
"Sleepycat": "Yes",
"SMLNJ": "No",
"Spencer-86": "No",
"SSH-OpenSSH": "No",
"SSH-short": "No",
"SunPro": "No",
"Ubuntu-font-1.0": "Yes (restricted)",
"Unicode-3.0": "No",
"Unicode-DFS-2015": "No",
"Unicode-DFS-2016": "No",
"Unlicense": "No",
"UPL-1.0": "No",
"W3C": "No",
"W3C-19980720": "No",
"W3C-20150513": "No",
"WTFPL": "No",
"X11": "No",
"XFree86-1.1": "No",
"Zlib": "No",
"zlib-acknowledgement": "No",
"ZPL-2.0": "No"
}
}
75 changes: 37 additions & 38 deletions src/scanoss/filecount.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import os
import pathlib
import sys
from contextlib import nullcontext

from progress.spinner import Spinner

Expand Down Expand Up @@ -105,48 +106,46 @@ def count_files(self, scan_dir: str) -> bool:
"""
success = True
if not scan_dir:
raise Exception(f'ERROR: Please specify a folder to scan')
raise Exception('ERROR: Please specify a folder to scan')
if not os.path.exists(scan_dir) or not os.path.isdir(scan_dir):
raise Exception(f'ERROR: Specified folder does not exist or is not a folder: {scan_dir}')

self.print_msg(f'Searching {scan_dir} for files to count...')
spinner = None
if not self.quiet and self.isatty:
spinner = Spinner('Searching ')
file_types = {}
file_count = 0
file_size = 0
for root, dirs, files in os.walk(scan_dir):
self.print_trace(f'U Root: {root}, Dirs: {dirs}, Files {files}')
dirs[:] = self.__filter_dirs(dirs) # Strip out unwanted directories
filtered_files = self.__filter_files(files) # Strip out unwanted files
self.print_trace(f'F Root: {root}, Dirs: {dirs}, Files {filtered_files}')
for file in filtered_files: # Cycle through each filtered file
path = os.path.join(root, file)
f_size = 0
try:
f_size = os.stat(path).st_size
except Exception as e:
self.print_trace(f'Ignoring missing symlink file: {file} ({e})') # broken symlink
if f_size > 0: # Ignore broken links and empty files
file_count = file_count + 1
file_size = file_size + f_size
f_suffix = pathlib.Path(file).suffix
if not f_suffix or f_suffix == '':
f_suffix = 'no_suffix'
self.print_trace(f'Counting {path} ({f_suffix} - {f_size})..')
fc = file_types.get(f_suffix)
if not fc:
fc = [1, f_size]
else:
fc[0] = fc[0] + 1
fc[1] = fc[1] + f_size
file_types[f_suffix] = fc
if spinner:
spinner.next()
# End for loop
if spinner:
spinner.finish()
spinner_ctx = Spinner('Searching ') if (not self.quiet and self.isatty) else nullcontext()

with spinner_ctx as spinner:
file_types = {}
Comment on lines +114 to +117
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Spinner context misuse; wrap instance and finish.

Same fix as in scanner.py.

-        spinner_ctx = Spinner('Searching ') if (not self.quiet and self.isatty) else nullcontext()
+        spinner_ctx = nullcontext(Spinner('Searching ')) if (not self.quiet and self.isatty) else nullcontext()
@@
-                        if spinner:
-                            spinner.next()
+                        if spinner:
+                            spinner.next()
+            if spinner:
+                spinner.finish()

Also applies to: 146-148

file_count = 0
file_size = 0
for root, dirs, files in os.walk(scan_dir):
self.print_trace(f'U Root: {root}, Dirs: {dirs}, Files {files}')
dirs[:] = self.__filter_dirs(dirs) # Strip out unwanted directories
filtered_files = self.__filter_files(files) # Strip out unwanted files
self.print_trace(f'F Root: {root}, Dirs: {dirs}, Files {filtered_files}')
for file in filtered_files: # Cycle through each filtered file
path = os.path.join(root, file)
f_size = 0
try:
f_size = os.stat(path).st_size
except Exception as e:
self.print_trace(f'Ignoring missing symlink file: {file} ({e})') # broken symlink
if f_size > 0: # Ignore broken links and empty files
file_count = file_count + 1
file_size = file_size + f_size
f_suffix = pathlib.Path(file).suffix
if not f_suffix or f_suffix == '':
f_suffix = 'no_suffix'
self.print_trace(f'Counting {path} ({f_suffix} - {f_size})..')
fc = file_types.get(f_suffix)
if not fc:
fc = [1, f_size]
else:
fc[0] = fc[0] + 1
fc[1] = fc[1] + f_size
file_types[f_suffix] = fc
if spinner:
spinner.next()
# End for loop
self.print_stderr(f'Found {file_count:,.0f} files with a total size of {file_size / (1 << 20):,.2f} MB.')
if file_types:
csv_dict = []
Expand Down
8 changes: 7 additions & 1 deletion src/scanoss/inspection/policy_check/scanoss/copyleft.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
from dataclasses import dataclass
from typing import Dict, List

from scanoss.constants import DEFAULT_COPYLEFT_LICENSE_SOURCES

from ...policy_check.policy_check import PolicyCheck, PolicyOutput, PolicyStatus
from ...utils.markdown_utils import generate_jira_table, generate_table
from ...utils.scan_result_processor import ScanResultProcessor
Expand Down Expand Up @@ -63,6 +65,7 @@ def __init__( # noqa: PLR0913
include: str = None,
exclude: str = None,
explicit: str = None,
license_sources: list = None,
):
"""
Initialise the Copyleft class.
Expand All @@ -77,6 +80,7 @@ def __init__( # noqa: PLR0913
:param include: Licenses to include in the analysis
:param exclude: Licenses to exclude from the analysis
:param explicit: Explicitly defined licenses
:param license_sources: List of license sources to check
"""
super().__init__(
debug, trace, quiet, format_type, status, name='Copyleft Policy', output=output
Expand All @@ -85,14 +89,16 @@ def __init__( # noqa: PLR0913
self.filepath = filepath
self.output = output
self.status = status
self.license_sources = license_sources or DEFAULT_COPYLEFT_LICENSE_SOURCES
self.results_processor = ScanResultProcessor(
self.debug,
self.trace,
self.quiet,
self.filepath,
include,
exclude,
explicit)
explicit,
self.license_sources)

def _json(self, components: list[Component]) -> PolicyOutput:
"""
Expand Down
Loading
Loading