Skip to content
Merged
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 @@ -7,7 +7,25 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p

## [Unreleased]

### TODO: add at least one Added, Changed, Deprecated, Removed, Fixed or Security section
### Changed

- Use ROBOT v1.9.11 compiled with [updated dependencies without vulnerabilities](https://github.com/eccenca/robot/tree/feature/updateDependenciesV1.9.11)
- Scan the SBOM of robot.jar (`robot.cdx.json`) with Trivy, since the jar scan cannot identify all bundled libraries
- Updated dependencies and template
- Validate plugin: the output graph is now declared a `void:Dataset` instead of an
`owl:Ontology`.
- Reason plugin: only one of "Data graph IRI" and "Ontology graph IRI" is required. With only
one, or the same graph in both, that graph is reasoned on its own, and the result is described as
"Reasoning results of graph <X>".

### Fixed

- Fix robot.jar vulnerabilities CVE-2024-7254, CVE-2026-43869, CVE-2026-43871.
- Reason plugin: selecting the same graph as data graph and ontology graph failed with
`OWLOntologyDocumentAlreadyExistsException`.
- Reason and Validate plugins: provenance data is now written with every DataIntegration
workspace provider. Before, it was only written if the workspace was stored as RDF (workspace
provider `backend` or `fileAndDataPlatform`), because it was read from the project graph.

## [2.4.1] 2026-10-07

Expand Down
9 changes: 8 additions & 1 deletion Taskfile.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ tasks:
JUNIT_FILE: ./{{.DIST_DIR}}/junit-mypy.xml

check:trivy:
desc: Scan for vulnerabilities using Trivy
desc: Scan the dependencies, robot.jar and its SBOM for vulnerabilities using Trivy
deps:
- prepare
cmds:
Expand All @@ -176,6 +176,13 @@ tasks:
--scanners vuln
--exit-code 1
cmem_plugin_reason/robot.jar
# robot.jar is a shaded jar: libraries without Maven metadata (e.g. protobuf-java,
# libthrift) are invisible to the jar scan. robot.cdx.json is the CycloneDX SBOM
# generated by the ROBOT build of the same robot.jar and lists all bundled libraries.
- >
poetry run trivy sbom
--exit-code 1
robot.cdx.json

check:deptry:
desc: Complain about unused or missing dependencies
Expand Down
15 changes: 10 additions & 5 deletions cmem_plugin_reason/doc/reason_doc.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
A task performing OWL reasoning. With an OWL ontology and a data graph as input the reasoning result is written to a specified graph.
A task performing OWL reasoning. With an OWL ontology and/or a data graph as input the reasoning result is written to a
specified graph. At least one of the two input graphs must be specified. With two different input graphs, the data
graph is reasoned with the ontology imported; with only one, or the same graph in both, that graph is reasoned on its
own.

## Options

### Data graph IRI

The IRI of the input data graph. The graph IRI is selected from a list of graphs of types `di:Dataset`, `void:Dataset`
and `owl:Ontology`.
and `owl:Ontology`. Optional if an ontology graph is specified.

### Ontology graph IRI

The IRI of the input ontology graph. The graph IRI is selected from a list of graphs of type`owl:Ontology`.
The IRI of the input ontology graph. The graph IRI is selected from a list of graphs of type `owl:Ontology`. Optional if
a data graph is specified.

### Ignore missing imports

Expand Down Expand Up @@ -136,7 +140,8 @@ If there are classes `Person`, `Student` and `Professor` as wells as the object

### Validate OWL2 profiles

Validate the input ontology against OWL profiles (DL, EL, QL, RL, and Full) and annotate the result graph.
Validate the input ontology against OWL profiles (DL, EL, QL, RL, and Full) and annotate the result graph.
Needs an ontology graph.

### Process valid OWL profiles from input

Expand All @@ -147,7 +152,7 @@ profiles validation is not done in the plugin. The valid profiles input is a com
### Output graph import

Add the triple <output_graph_iri> owl:imports <ontology_graph_iri> into the output graph or add the triple
<ontology_graph_iri> owl:imports <output_graph_iri> into the ontology graph.
<ontology_graph_iri> owl:imports <output_graph_iri> into the ontology graph. Needs an ontology graph.

### Maximum RAM Percentage

Expand Down
4 changes: 4 additions & 0 deletions cmem_plugin_reason/doc/validate_doc.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ The IRI of the output graph for the reasoning result.

⚠️ Existing graphs will be overwritten.

The output graph holds the explanation axioms plus the validation result and is declared a
`void:Dataset`. It is not declared an `owl:Ontology`: it does not contain an ontology of its
own, it describes the validated one.

### Write markdown explanation file

If enabled, an explanation markdown file is written to the project.
Expand Down
126 changes: 93 additions & 33 deletions cmem_plugin_reason/plugin_reason.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
IGNORE_MISSING_IMPORTS_PARAMETER,
MAX_RAM_PERCENTAGE_DEFAULT,
MAX_RAM_PERCENTAGE_PARAMETER,
ONTOLOGY_GRAPH_IRI_PARAMETER,
REASON_REASONER_PARAMETER,
REASONERS,
VALIDATE_PROFILES_PARAMETER,
Expand Down Expand Up @@ -149,7 +148,13 @@
documentation=REASON_DOC,
parameters=[
IGNORE_MISSING_IMPORTS_PARAMETER,
ONTOLOGY_GRAPH_IRI_PARAMETER,
PluginParameter(
param_type=GraphParameterType(classes=["http://www.w3.org/2002/07/owl#Ontology"]),
name="ontology_graph_iri",
label="Ontology graph IRI",
description="""The IRI of the input ontology graph. At least one of data graph and
ontology graph must be specified.""",
),
VALIDATE_PROFILES_PARAMETER,
REASON_REASONER_PARAMETER,
MAX_RAM_PERCENTAGE_PARAMETER,
Expand All @@ -163,7 +168,8 @@
),
name="data_graph_iri",
label="Data graph IRI",
description="The IRI of the input data graph.",
description="""The IRI of the input data graph. At least one of data graph and
ontology graph must be specified.""",
),
PluginParameter(
param_type=GraphParameterType(
Expand Down Expand Up @@ -285,7 +291,7 @@
label="Output graph import",
description="""Add the triple <output_graph_iri> owl:imports <ontology_graph_iri> to the
output graph or add the triple <ontology_graph_iri> owl:imports <output_graph_iri> to
the ontology graph.""",
the ontology graph. Needs an ontology graph.""",
default_value="none",
),
PluginParameter(
Expand All @@ -301,10 +307,10 @@
class ReasonPlugin(WorkflowPlugin):
"""Reason plugin"""

def __init__( # noqa: PLR0913 C901 PLR0917
def __init__( # noqa: PLR0913, PLR0917
self,
data_graph_iri: str,
ontology_graph_iri: str,
data_graph_iri: str = "",
ontology_graph_iri: str = "",
ignore_missing_imports: bool = False,
output_graph_iri: str | None = None,
reasoner: str | None = None,
Expand Down Expand Up @@ -340,17 +346,9 @@ def __init__( # noqa: PLR0913 C901 PLR0917
"ObjectPropertyRange": object_property_range,
"ObjectPropertyDomain": object_property_domain,
}
errors = ""
if not is_valid_uri(data_graph_iri):
errors += 'Invalid IRI for parameter "Data graph IRI". '
if not is_valid_uri(ontology_graph_iri):
errors += 'Invalid IRI for parameter "Ontology graph IRI". '
if not is_valid_uri(output_graph_iri):
errors += 'Invalid IRI for parameter "Result graph IRI". '
if output_graph_iri == data_graph_iri:
errors += "Result graph IRI cannot be the same as the data graph IRI. "
if output_graph_iri == ontology_graph_iri:
errors += "Result graph IRI cannot be the same as the ontology graph IRI. "
errors = self.graph_parameter_errors(
data_graph_iri, ontology_graph_iri, output_graph_iri, imports, validate_profile
)
if reasoner not in REASONERS:
errors += 'Invalid value for parameter "Reasoner". '
if True not in self.axioms.values():
Expand Down Expand Up @@ -389,6 +387,49 @@ def __init__( # noqa: PLR0913 C901 PLR0917
self.input_ports = FixedNumberOfInputs([])
self.output_port = None

@staticmethod
def graph_parameter_errors(
data_graph_iri: str,
ontology_graph_iri: str,
output_graph_iri: str | None,
imports: str,
validate_profile: bool,
) -> str:
"""Check the graph parameters; only one of data graph and ontology graph is needed"""
errors = ""
if not data_graph_iri and not ontology_graph_iri:
errors += "At least one of data graph IRI and ontology graph IRI must be specified. "
if data_graph_iri and not is_valid_uri(data_graph_iri):
errors += 'Invalid IRI for parameter "Data graph IRI". '
if ontology_graph_iri and not is_valid_uri(ontology_graph_iri):
errors += 'Invalid IRI for parameter "Ontology graph IRI". '
if not is_valid_uri(output_graph_iri):
errors += 'Invalid IRI for parameter "Result graph IRI". '
if data_graph_iri and output_graph_iri == data_graph_iri:
errors += "Result graph IRI cannot be the same as the data graph IRI. "
if ontology_graph_iri and output_graph_iri == ontology_graph_iri:
errors += "Result graph IRI cannot be the same as the ontology graph IRI. "
if imports != "none" and not ontology_graph_iri:
errors += 'Parameter "Output graph import" needs an ontology graph IRI. '
if validate_profile and not ontology_graph_iri:
errors += 'Parameter "Validate OWL2 profiles" needs an ontology graph IRI. '
return errors

@property
def input_graph_iris(self) -> list[str]:
"""The specified input graphs, each once: data graph and/or ontology graph"""
return list(dict.fromkeys(i for i in (self.data_graph_iri, self.ontology_graph_iri) if i))

@property
def main_graph_iri(self) -> str:
"""The graph passed to ROBOT; with two different inputs, the data graph"""
return self.input_graph_iris[0]

@property
def two_inputs(self) -> bool:
"""Whether two different input graphs are given, so the data graph imports the ontology"""
return len(self.input_graph_iris) > 1

@staticmethod
def underscore(word: str) -> str:
"""Make an underscored, lowercase form from the expression in the string"""
Expand All @@ -406,7 +447,7 @@ def get_graphs(self, graphs: dict, missing: list) -> None:
continue
self.log.info(f"Fetching graph {iri}.")
fetch_graph(self.client, iri, path)
if iri == self.data_graph_iri:
if iri == self.data_graph_iri and self.two_inputs:
with path.open("a", encoding="utf-8") as file:
file.write(
f"\n<{iri}> "
Expand All @@ -418,7 +459,7 @@ def get_graphs_tree(self) -> tuple[dict, list]: # noqa: C901
"""Get graph import tree. Last item in graph_iris is output_graph_iri which is excluded"""
missing = []
graphs = {}
for graph_iri in [self.data_graph_iri, self.ontology_graph_iri]:
for graph_iri in self.input_graph_iris:
if graph_iri not in graphs:
graphs[graph_iri] = f"{uuid4().hex}.nt"
tree = self.client.graph_imports.get_import_tree(graph_iri)
Expand All @@ -443,8 +484,9 @@ def get_graphs_tree(self) -> tuple[dict, list]: # noqa: C901
def reason(self, graphs: dict) -> None:
"""Reason"""
axioms = " ".join(k for k, v in self.axioms.items() if v)
data_location = f"{self.temp}/{graphs[self.data_graph_iri]}"
label = get_output_graph_label(self, self.data_graph_iri, "Reasoning Results")
data_location = f"{self.temp}/{graphs[self.main_graph_iri]}"
label = get_output_graph_label(self, self.main_graph_iri, "Reasoning Results")
sources = " ".join(f'--link-annotation dc:source "{iri}"' for iri in self.input_graph_iris)
cmd = (
f'reason --input "{data_location}" '
f"--reasoner {self.reasoner} "
Expand All @@ -459,11 +501,8 @@ def reason(self, graphs: dict) -> None:
f'annotate --ontology-iri "{self.output_graph_iri}" '
f"--remove-annotations "
f'--language-annotation rdfs:label "{label}" en '
f"--language-annotation rdfs:comment "
f'"Reasoning results of data graph <{self.data_graph_iri}> with ontology '
f'<{self.ontology_graph_iri}>" en '
f'--link-annotation dc:source "{self.data_graph_iri}" '
f'--link-annotation dc:source "{self.ontology_graph_iri}" '
f'--language-annotation rdfs:comment "{self.result_comment()}" en '
f"{sources} "
f'--output "{self.temp}/result.ttl"'
)
response = robot(cmd, self.max_ram_percentage)
Expand All @@ -474,6 +513,28 @@ def reason(self, graphs: dict) -> None:
raise OSError(response.stderr.decode())
raise OSError("ROBOT error")

def result_comment(self) -> str:
"""Describe the input graphs of the reasoning result"""
if self.two_inputs:
return (
f"Reasoning results of data graph <{self.data_graph_iri}> with ontology "
f"<{self.ontology_graph_iri}>"
)
# one graph, whichever field it is in: its role is not known
return f"Reasoning results of graph <{self.main_graph_iri}>"

def add_ontology_import(self) -> None:
"""Add ontology graph import to output graph"""
query = f"""
INSERT DATA {{
GRAPH <{self.output_graph_iri}> {{
<{self.output_graph_iri}> <http://www.w3.org/2002/07/owl#imports>
<{self.ontology_graph_iri}>
}}
}}
"""
self.client.store.sparql.update(query)

def add_result_import(self) -> None:
"""Add result graph import to ontology graph"""
query = f"""
Expand Down Expand Up @@ -519,7 +580,10 @@ def _execute(self) -> None:

if self.imports == "import_result":
self.add_result_import()
if self.imports != "import_ontology" and not self.data_imports_ontology:
if self.imports == "import_ontology" and not self.two_inputs:
# with two inputs, the import added to the data graph is carried into the output
self.add_ontology_import()
if self.two_inputs and self.imports != "import_ontology" and not self.data_imports_ontology:
self.remove_ontology_import()

self.context.report.update(
Expand All @@ -534,11 +598,7 @@ def execute(self, inputs: Sequence[Entities], context: ExecutionContext) -> None
"""Execute plugin with temporary directory"""
self.context = context
self.client = get_client(context)
not_exist = [
iri
for iri in (self.data_graph_iri, self.ontology_graph_iri)
if iri not in self.client.graphs
]
not_exist = [iri for iri in self.input_graph_iris if iri not in self.client.graphs]
if not_exist:
raise ValueError(f"Graphs do not exist: {', '.join(not_exist)}")

Expand Down
30 changes: 28 additions & 2 deletions cmem_plugin_reason/plugin_validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
IGNORE_MISSING_IMPORTS_PARAMETER,
MAX_RAM_PERCENTAGE_DEFAULT,
MAX_RAM_PERCENTAGE_PARAMETER,
ONTOLOGY_GRAPH_IRI_PARAMETER,
REASONERS,
VALIDATE_PROFILES_PARAMETER,
VALIDATE_REASONER_PARAMETER,
Expand All @@ -41,6 +40,8 @@
)

LABEL = "Validate OWL consistency"
OWL_ONTOLOGY = "http://www.w3.org/2002/07/owl#Ontology"
VOID_DATASET = "http://rdfs.org/ns/void#Dataset"


@Plugin(
Expand All @@ -50,7 +51,12 @@
icon=Icon(file_name="file-icons--owl.svg", package=__package__),
parameters=[
IGNORE_MISSING_IMPORTS_PARAMETER,
ONTOLOGY_GRAPH_IRI_PARAMETER,
PluginParameter(
param_type=GraphParameterType(classes=["http://www.w3.org/2002/07/owl#Ontology"]),
name="ontology_graph_iri",
label="Ontology graph IRI",
description="The IRI of the input ontology graph.",
),
MAX_RAM_PERCENTAGE_PARAMETER,
VALIDATE_PROFILES_PARAMETER,
VALIDATE_REASONER_PARAMETER,
Expand Down Expand Up @@ -239,6 +245,25 @@ def explain(self, graphs: dict) -> None:
raise OSError(response.stderr.decode())
raise OSError("ROBOT error")

def declare_dataset(self) -> None:
"""Declare the output graph a void:Dataset instead of the owl:Ontology ROBOT declares it

The output graph describes the validated ontology, it is not an ontology of its own.
"""
query = f"""
DELETE DATA {{
GRAPH <{self.output_graph_iri}> {{
<{self.output_graph_iri}> a <{OWL_ONTOLOGY}> .
}}
}};
INSERT DATA {{
GRAPH <{self.output_graph_iri}> {{
<{self.output_graph_iri}> a <{VOID_DATASET}> .
}}
}}
"""
self.client.store.sparql.update(query)

def make_resource(self, context: ExecutionContext) -> None:
"""Make MD resource in project"""
self.client.files.import_item(
Expand Down Expand Up @@ -283,6 +308,7 @@ def _execute(self) -> Entities | None:
return None
if self.output_graph_iri:
send_result(self.client, self.output_graph_iri, get_file_with_datetime(self))
self.declare_dataset()
post_provenance(self)
if cancel_workflow(self):
return None
Expand Down
Binary file modified cmem_plugin_reason/robot.jar
Binary file not shown.
Loading
Loading