Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[issue-307] implement validation #371

Merged
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
cb5e892
[issue-321] introduce dataclass with properties and typeguard
armintaenzertng Nov 28, 2022
0a9fa39
[issue-307] create validation classes
armintaenzertng Nov 24, 2022
649b7cf
WIP: relationship validator
armintaenzertng Nov 28, 2022
f771a57
[issue-307] create barebones validation tests
armintaenzertng Dec 5, 2022
2379069
[WIP] validation tests
armintaenzertng Dec 6, 2022
6887fd1
[issue-307] valid_defaults and creation_info test
armintaenzertng Dec 6, 2022
413dc40
[WIP] creation_info_validator refinement
armintaenzertng Dec 7, 2022
7d857b4
[WIP] actor_validator
armintaenzertng Dec 7, 2022
75ad6d0
[WIP] package validation
armintaenzertng Dec 7, 2022
cb11b5e
[WIP] file and snippet validation
armintaenzertng Dec 8, 2022
352a40d
[WIP] relationship and annotation validation
armintaenzertng Dec 9, 2022
4adad6d
[WIP] package_verification_code_validator and external_document_ref_v…
armintaenzertng Dec 9, 2022
aec9546
[WIP] extracted licensing info validator
armintaenzertng Dec 12, 2022
9770e7e
[issue-307] more validation and tests
armintaenzertng Dec 12, 2022
ce3d1eb
[issue-307] almost all validation
armintaenzertng Dec 14, 2022
fda24dd
[issue-307] package and document relationship validation
armintaenzertng Dec 14, 2022
092338b
[issue-307] skip unimplemented tests
armintaenzertng Dec 14, 2022
d038804
[issue-307] add uritools to dependencies
armintaenzertng Dec 14, 2022
43f7e01
[review] autoformat (includes removing unused imports)
armintaenzertng Dec 15, 2022
eeb01d2
[review] renamings and quotation marks
armintaenzertng Dec 15, 2022
90ff8ee
[review] reduce some code in valid_defaults.py
armintaenzertng Dec 15, 2022
71da2fe
[review] replace correct with valid and wrong with invalid
armintaenzertng Dec 15, 2022
d7d0545
[review] fix tests
armintaenzertng Dec 15, 2022
49f6872
[review] reduce file checksum validation output
armintaenzertng Dec 15, 2022
26ab9b8
[review] some smaller edits
armintaenzertng Dec 21, 2022
9a7a5a2
[review] remove spdx_id validation tests
armintaenzertng Dec 21, 2022
f8abb0f
[review] remove redundant eq=True from validation_message.py
armintaenzertng Dec 21, 2022
58c7a88
[review] remove class structure
armintaenzertng Dec 21, 2022
5876e94
[review] add methods to validate packages, files and snippets individ…
armintaenzertng Dec 22, 2022
6e05680
[review] make Document and ValidationContext optional in validation c…
armintaenzertng Dec 28, 2022
7930a3a
[review] refactoring and small additions
armintaenzertng Dec 28, 2022
c93f6f4
[review] add copyright
armintaenzertng Dec 28, 2022
4046503
[review] add issue link
armintaenzertng Dec 28, 2022
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
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ classifiers = [
]
urls = {Homepage = "https://github.com/spdx/tools-python"}
requires-python = ">=3.7"
dependencies = ["ply", "rdflib", "click", "pyyaml", "xmltodict", "typeguard"]
dependencies = ["ply", "rdflib", "click", "pyyaml", "xmltodict", "typeguard", "uritools"]
dynamic = ["version"]

[project.optional-dependencies]
Expand Down
Empty file added src/validation/__init__.py
Empty file.
26 changes: 26 additions & 0 deletions src/validation/actor_validator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from typing import List

from src.model.actor import Actor, ActorType
from src.validation.validation_message import ValidationMessage, ValidationContext, SpdxElementType


def validate_actors(actors: List[Actor], parent_id: str) -> List[ValidationMessage]:
validation_messages = []
for actor in actors:
validation_messages.extend(validate_actor(actor, parent_id))

return validation_messages


def validate_actor(actor: Actor, parent_id: str) -> List[ValidationMessage]:
validation_messages = []

if actor.actor_type == ActorType.TOOL and actor.email is not None:
validation_messages.append(
ValidationMessage(
f"email must be None if actor_type is TOOL, but is: {actor.email}",
ValidationContext(parent_id=parent_id, element_type=SpdxElementType.ACTOR, full_element=actor)
)
)

meretp marked this conversation as resolved.
Show resolved Hide resolved
return validation_messages
29 changes: 29 additions & 0 deletions src/validation/annotation_validator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from typing import List

from src.model.annotation import Annotation
from src.model.document import Document
from src.validation.actor_validator import validate_actor
from src.validation.spdx_id_validators import validate_spdx_id
from src.validation.validation_message import ValidationMessage, ValidationContext, SpdxElementType


def validate_annotations(annotations: List[Annotation], document: Document) -> List[ValidationMessage]:
validation_messages = []
for annotation in annotations:
validation_messages.extend(validate_annotation(annotation, document))

return validation_messages


def validate_annotation(annotation: Annotation, document: Document) -> List[ValidationMessage]:
validation_messages = []
context = ValidationContext(element_type=SpdxElementType.ANNOTATION,
full_element=annotation)

validation_messages.extend(validate_actor(annotation.annotator, "annotation"))

messages: List[str] = validate_spdx_id(annotation.spdx_id, document, check_document=True)
for message in messages:
validation_messages.append(ValidationMessage(message, context))

return validation_messages
56 changes: 56 additions & 0 deletions src/validation/checksum_validator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import re
from typing import List, Dict

from src.model.checksum import Checksum, ChecksumAlgorithm
from src.validation.validation_message import ValidationMessage, ValidationContext, SpdxElementType

# in hexadecimal digits
algorithm_length: Dict = {
ChecksumAlgorithm.SHA1: "40",
ChecksumAlgorithm.SHA224: "56",
ChecksumAlgorithm.SHA256: "64",
ChecksumAlgorithm.SHA384: "96",
ChecksumAlgorithm.SHA512: "128",
ChecksumAlgorithm.SHA3_256: "64",
ChecksumAlgorithm.SHA3_384: "96",
ChecksumAlgorithm.SHA3_512: "128",
ChecksumAlgorithm.BLAKE2B_256: "64",
ChecksumAlgorithm.BLAKE2B_384: "96",
ChecksumAlgorithm.BLAKE2B_512: "128",
ChecksumAlgorithm.BLAKE3: "256,", # at least 256 bits
ChecksumAlgorithm.MD2: "32",
ChecksumAlgorithm.MD4: "32",
ChecksumAlgorithm.MD5: "32",
ChecksumAlgorithm.MD6: "0,512", # between 0 and 512 bits
ChecksumAlgorithm.ADLER32: "8",
}


def validate_checksums(checksums: List[Checksum], parent_id: str) -> List[ValidationMessage]:
validation_messages = []
for checksum in checksums:
validation_messages.extend(validate_checksum(checksum, parent_id))

return validation_messages


def validate_checksum(checksum: Checksum, parent_id: str) -> List[ValidationMessage]:
validation_messages = []
algorithm = checksum.algorithm
context = ValidationContext(parent_id=parent_id, element_type=SpdxElementType.CHECKSUM,
full_element=checksum)

if not re.match("^[0-9a-f]{" + algorithm_length[algorithm] + "}$", checksum.value):
if algorithm == ChecksumAlgorithm.BLAKE3:
length = "at least 256"
elif algorithm == ChecksumAlgorithm.MD6:
length = "between 0 and 512"
else:
length = algorithm_length[algorithm]
validation_messages.append(
ValidationMessage(
f"value of {algorithm} must consist of {length} hexadecimal digits, but is: {checksum.value} (length: {len(checksum.value)} digits)",
context)
)

return validation_messages
51 changes: 51 additions & 0 deletions src/validation/creation_info_validator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import re
from typing import List

from src.model.document import CreationInfo
from src.validation.actor_validator import validate_actors
from src.validation.external_document_ref_validator import validate_external_document_refs
from src.validation.uri_validators import validate_uri
from src.validation.validation_message import ValidationMessage, ValidationContext, SpdxElementType


def validate_creation_info(creation_info: CreationInfo) -> List[ValidationMessage]:
validation_messages: List[ValidationMessage] = []

context = ValidationContext(spdx_id=creation_info.spdx_id, element_type=SpdxElementType.DOCUMENT)

if not re.match(r"^SPDX-\d+.\d+$", creation_info.spdx_version):
validation_messages.append(
ValidationMessage(
f'spdx_version must be of the form "SPDX-[major].[minor]" but is: {creation_info.spdx_version}',
context
)
)

if creation_info.spdx_id != "SPDXRef-DOCUMENT":
validation_messages.append(
ValidationMessage(
f'spdx_id must be "SPDXRef-DOCUMENT", but is: {creation_info.spdx_id}',
context
)
)

if creation_info.data_license != "CC0-1.0":
validation_messages.append(
ValidationMessage(
f'data_license must be "CC0-1.0", but is: {creation_info.data_license}',
context
)
)

for message in validate_uri(creation_info.document_namespace):
validation_messages.append(
ValidationMessage(
"document_namespace " + message, context
)
)

validation_messages.extend(validate_actors(creation_info.creators, creation_info.spdx_id))

validation_messages.extend(validate_external_document_refs(creation_info.external_document_refs, creation_info.spdx_id))

return validation_messages
39 changes: 39 additions & 0 deletions src/validation/document_validator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from typing import List

from src.model.document import Document
from src.model.relationship import RelationshipType
from src.validation.annotation_validator import validate_annotations
from src.validation.creation_info_validator import validate_creation_info
from src.validation.extracted_licensing_info_validator import validate_extracted_licensing_infos
from src.validation.file_validator import validate_files
from src.validation.package_validator import validate_packages
from src.validation.relationship_validator import validate_relationships
from src.validation.snippet_validator import validate_snippets
from src.validation.validation_message import ValidationMessage, ValidationContext, SpdxElementType


def validate_full_spdx_document(document: Document, spdx_version: str) -> List[ValidationMessage]:
validation_messages: List[ValidationMessage] = []

validation_messages.extend(validate_creation_info(document.creation_info))
validation_messages.extend(validate_packages(document.packages, document))
validation_messages.extend(validate_files(document.files, document))
validation_messages.extend(validate_snippets(document.snippets, document))
validation_messages.extend(validate_annotations(document.annotations, document))
validation_messages.extend(validate_relationships(document.relationships, document, spdx_version))
validation_messages.extend(validate_extracted_licensing_infos(document.extracted_licensing_info))

document_id = document.creation_info.spdx_id
document_describes_relationships = [relationship for relationship in document.relationships if
relationship.relationship_type == RelationshipType.DESCRIBES and relationship.spdx_element_id == document_id]
described_by_document_relationships = [relationship for relationship in document.relationships if
relationship.relationship_type == RelationshipType.DESCRIBED_BY and relationship.related_spdx_element_id == document_id]

if not document_describes_relationships + described_by_document_relationships:
validation_messages.append(
ValidationMessage(
f'there must be at least one relationship "{document_id} DESCRIBES ..." or "... DESCRIBED_BY {document_id}"',
ValidationContext(spdx_id=document_id,
element_type=SpdxElementType.DOCUMENT)))

return validation_messages
42 changes: 42 additions & 0 deletions src/validation/external_document_ref_validator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import re
armintaenzertng marked this conversation as resolved.
Show resolved Hide resolved
from typing import List

from src.model.external_document_ref import ExternalDocumentRef
from src.validation.checksum_validator import validate_checksum
from src.validation.spdx_id_validators import is_valid_external_doc_ref_id
from src.validation.uri_validators import validate_uri
from src.validation.validation_message import ValidationMessage, ValidationContext, SpdxElementType


def validate_external_document_refs(external_document_refs: List[ExternalDocumentRef], parent_id: str) -> List[
ValidationMessage]:
validation_messages = []
for external_document_ref in external_document_refs:
validation_messages.extend(validate_external_document_ref(external_document_ref, parent_id))

return validation_messages


def validate_external_document_ref(external_document_ref: ExternalDocumentRef, parent_id: str) -> List[ValidationMessage]:
validation_messages = []
context = ValidationContext(parent_id=parent_id, element_type=SpdxElementType.EXTERNAL_DOCUMENT_REF,
full_element=external_document_ref)

if not is_valid_external_doc_ref_id(external_document_ref.document_ref_id):
validation_messages.append(
ValidationMessage(
f'document_ref_id must only contain letters, numbers, ".", "-" and "+" and must begin with "DocumentRef-", but is: {external_document_ref.document_ref_id}',
context
)
)

for message in validate_uri(external_document_ref.document_uri):
validation_messages.append(
ValidationMessage(
"document_uri " + message, context
)
)

validation_messages.extend(validate_checksum(external_document_ref.checksum, parent_id))

return validation_messages
18 changes: 18 additions & 0 deletions src/validation/external_package_ref_validator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from typing import List

from src.model.package import ExternalPackageRef
from src.validation.validation_message import ValidationMessage


def validate_external_package_refs(external_package_refs: List[ExternalPackageRef], parent_id: str) -> List[
ValidationMessage]:
validation_messages = []
for external_package_ref in external_package_refs:
validation_messages.extend(validate_external_package_ref(external_package_ref, parent_id))

return validation_messages


def validate_external_package_ref(external_package_ref: ExternalPackageRef, parent_id: str) -> List[ValidationMessage]:
# TODO: https://github.com/spdx/tools-python/issues/373
return []
42 changes: 42 additions & 0 deletions src/validation/extracted_licensing_info_validator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import re
from typing import List, Optional

from src.model.extracted_licensing_info import ExtractedLicensingInfo
from src.validation.uri_validators import validate_url
from src.validation.validation_message import ValidationMessage, ValidationContext, SpdxElementType


def validate_extracted_licensing_infos(extracted_licensing_infos: Optional[List[ExtractedLicensingInfo]]) -> List[ValidationMessage]:
validation_messages = []
for extracted_licensing_info in extracted_licensing_infos:
validation_messages.extend(validate_extracted_licensing_info(extracted_licensing_info))

return validation_messages


def validate_extracted_licensing_info(extracted_licensing_infos: ExtractedLicensingInfo) -> List[
ValidationMessage]:
validation_messages: List[ValidationMessage] = []
context = ValidationContext(element_type=SpdxElementType.EXTRACTED_LICENSING_INFO,
full_element=extracted_licensing_infos)

license_id: str = extracted_licensing_infos.license_id
if license_id and not re.match(r"^LicenseRef-[\da-zA-Z.-]+$", license_id):
validation_messages.append(
ValidationMessage(
f'license_id must only contain letters, numbers, "." and "-" and must begin with "LicenseRef-", but is: {license_id}',
context)
)

if license_id and not extracted_licensing_infos.extracted_text:
validation_messages.append(
ValidationMessage("extracted_text must be provided if there is a license_id assigned", context)
)

for cross_reference in extracted_licensing_infos.cross_references:
for message in validate_url(cross_reference):
validation_messages.append(
ValidationMessage("cross_reference " + message, context)
)

return validation_messages
54 changes: 54 additions & 0 deletions src/validation/file_validator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
from typing import List

from src.model.checksum import ChecksumAlgorithm
from src.model.document import Document
from src.model.file import File
from src.validation.checksum_validator import validate_checksums
from src.validation.license_expression_validator import validate_license_expressions, validate_license_expression
from src.validation.spdx_id_validators import validate_spdx_id
from src.validation.validation_message import ValidationMessage, ValidationContext, SpdxElementType


def validate_files(files: List[File], document: Document) -> List[ValidationMessage]:
validation_messages = []
for file in files:
validation_messages.extend(validate_file_within_document(file, document))

return validation_messages


def validate_file_within_document(file: File, document: Document) -> List[ValidationMessage]:
validation_messages: List[ValidationMessage] = []
context = ValidationContext(spdx_id=file.spdx_id, element_type=SpdxElementType.FILE, full_element=file)

for message in validate_spdx_id(file.spdx_id, document):
validation_messages.append(ValidationMessage(message, context))

validation_messages.extend(validate_file(file, context))

return validation_messages


def validate_file(file: File, context: ValidationContext) -> List[ValidationMessage]:
validation_messages = []

if not file.name.startswith("./"):
validation_messages.append(
ValidationMessage(
f'file name must be a relative path to the file, starting with "./", but is: {file.name}', context)
)

if ChecksumAlgorithm.SHA1 not in [checksum.algorithm for checksum in file.checksums]:
validation_messages.append(
ValidationMessage(
f"checksums must contain a SHA1 algorithm checksum, but only contains: {[checksum.algorithm for checksum in file.checksums]}",
context)
)

validation_messages.extend(validate_checksums(file.checksums, file.spdx_id))

validation_messages.extend(validate_license_expression(file.concluded_license))

validation_messages.extend(validate_license_expressions(file.license_info_in_file))

return validation_messages
24 changes: 24 additions & 0 deletions src/validation/license_expression_validator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from typing import List, Optional, Union

from src.model.license_expression import LicenseExpression
from src.model.spdx_no_assertion import SpdxNoAssertion
from src.model.spdx_none import SpdxNone
from src.validation.validation_message import ValidationMessage


def validate_license_expressions(license_expressions: Optional[
Union[List[LicenseExpression], SpdxNoAssertion, SpdxNone]]) -> List[ValidationMessage]:
if license_expressions in [SpdxNoAssertion(), SpdxNone(), None]:
return []

error_messages = []

for license_expression in license_expressions:
error_messages.extend(validate_license_expression(license_expression))

return error_messages


def validate_license_expression(license_expression: LicenseExpression) -> List[ValidationMessage]:
# TODO: implement this once we have a better license expression model: https://github.com/spdx/tools-python/issues/374
return []
Loading