Skip to content

Commit

Permalink
Show collected items data
Browse files Browse the repository at this point in the history
Currently, the "Show test details" section displays definition
of OVAL Object and OVAL State. This patch will add also displaying
OVAL Items collected from the system for the given OVAL Object.
This is a very popular feature of the old report which I think
will be useful also in the new report because it gives detailed
information about the actual state of the target.
  • Loading branch information
jan-cerny committed Jun 18, 2024
1 parent be59973 commit b0100b7
Show file tree
Hide file tree
Showing 9 changed files with 264 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -626,6 +626,76 @@ function get_OVAL_object_info_heading(oval_object) {
return div;
}

function generate_collected_items(collected_items, div) {
const h1 = H1.cloneNode();
h1.textContent = "Collected Items:";
h1.className = "pf-c-title pf-m-lg";
div.appendChild(h1);

if (collected_items == null) {
const explanation = DIV.cloneNode();
explanation.textContent = "No items have been collected from the target.";
div.appendChild(explanation);
return;
}

const table_div = DIV.cloneNode();
table_div.className = "pf-c-scroll-inner-wrapper oval-test-detail-table";
div.appendChild(table_div);

const table = TABLE.cloneNode();
table.className = "pf-c-table pf-m-compact pf-m-grid-md";
table.setAttribute("role", "grid");
table_div.appendChild(table);

const table_thead = THEAD.cloneNode();
const row = ROW.cloneNode();
row.setAttribute("role", "row");
table_thead.appendChild(row);

const fragment = document.createDocumentFragment();
const header_col = HEADER_COL.cloneNode();
header_col.setAttribute("role", "columnheader");
header_col.setAttribute("scope", "col");
header_col.className = "pf-m-truncate pf-m-fit-content";

for (const item of collected_items.header) {
const clone_header_col = header_col.cloneNode();
clone_header_col.textContent = format_header_item(item);
fragment.appendChild(clone_header_col);
}
row.appendChild(fragment);
table.appendChild(table_thead);

const tbody = TBODY.cloneNode();
tbody.setAttribute("role", "rowgroup");
const rows_fragment = document.createDocumentFragment();

const col = COL.cloneNode();
col.setAttribute("role", "cell");
col.className = "pf-m-truncate pf-m-fit-content";

for (const entry of collected_items.entries) {
const clone_of_row = row.cloneNode();
rows_fragment.appendChild(clone_of_row);
const cols_fragment = document.createDocumentFragment();
for (const value of entry) {
const clone_col = col.cloneNode();
clone_col.textContent = value;
cols_fragment.appendChild(clone_col);
}
clone_of_row.appendChild(cols_fragment);
}
tbody.appendChild(rows_fragment);
table.appendChild(tbody);

if (collected_items.message != null) {
const msg = DIV.cloneNode();
msg.textContent = collected_items.message;
div.appendChild(msg);
}
}

function generate_OVAL_object(test_info, oval_object, div) {
if (oval_object === undefined) {
// eslint-disable-next-line no-console
Expand Down Expand Up @@ -836,13 +906,15 @@ function get_OVAL_test_info(test_info) {

div.appendChild(get_spacer());

generate_collected_items(test_info.oval_object.collected_items, div)

Check notice

Code scanning / CodeQL

Semicolon insertion Note

Avoid automated semicolon insertion (92% of all statements in
the enclosing function
have an explicit semicolon).

if (test_info.oval_states.length > 0) {
div.appendChild(get_spacer());
div.appendChild(get_OVAL_state_heading());
}

for (const oval_state of test_info.oval_states) {
generate_OVAL_state(test_info, oval_state, div);
div.appendChild(get_spacer());
}
return div;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from .group import Group
from .identifier import Identifier
from .oval_definition import OvalDefinition
from .oval_items import OVALItems
from .oval_node import OvalNode
from .oval_object import OvalObject, OvalObjectMessage
from .oval_reference import OvalReference
Expand Down
15 changes: 15 additions & 0 deletions openscap_report/scap_results_parser/data_structures/oval_items.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Copyright 2024, Red Hat, Inc.
# SPDX-License-Identifier: LGPL-2.1-or-later

from typing import List, Tuple
from openscap_report.dataclasses import asdict, dataclass, field


@dataclass
class OVALItems:
header: Tuple[str] = field(default_factory=tuple)
entries: List[Tuple[str]] = field(default_factory=list)
message: str = None

def as_dict(self):
return asdict(self)
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from typing import Dict

from openscap_report.dataclasses import asdict, dataclass, field
from openscap_report.scap_results_parser.data_structures.oval_items import OVALItems


@dataclass
Expand All @@ -20,6 +21,7 @@ class OvalObject:
comment: str = ""
object_type: str = ""
object_data: Dict[str, str] = field(default_factory=dict)
collected_items: OVALItems = None

def as_dict(self):
return asdict(self)
1 change: 1 addition & 0 deletions openscap_report/scap_results_parser/namespaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@
'cpe-dict': 'http://cpe.mitre.org/dictionary/2.0',
'ds': 'http://scap.nist.gov/schema/scap/source/1.2',
'cpe-lang': 'http://cpe.mitre.org/language/2.0',
'ind-sys': 'http://oval.mitre.org/XMLSchema/oval-system-characteristics-5#independent'
}
1 change: 1 addition & 0 deletions openscap_report/scap_results_parser/parsers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from .group_parser import GroupParser
from .known_references import KNOWN_REFERENCES, update_references
from .oval_definition_parser import OVALDefinitionParser
from .oval_items_parser import OVALItemsParser
from .oval_object_parser import OVALObjectParser
from .oval_result_parser import OVALResultParser
from .oval_state_parser import OVALStateParser
Expand Down
69 changes: 69 additions & 0 deletions openscap_report/scap_results_parser/parsers/oval_items_parser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Copyright 2024, Red Hat, Inc.
# SPDX-License-Identifier: LGPL-2.1-or-later

from .shared_static_methods_of_parser import SharedStaticMethodsOfParser
from ..data_structures import OVALItems
from ..namespaces import NAMESPACES

PROCURED_ITEMS_LIMIT = 100


class OVALItemsParser:
def __init__(self, collected_objects, system_data):
self.collected_objects = collected_objects
self.system_data = system_data

def _get_item(self, item_ref):
item_el = self.system_data.get(item_ref)
item_data = {}
for child_el in item_el:
if child_el.text and child_el.text.strip():
key = SharedStaticMethodsOfParser.get_key_of_xml_element(child_el)
item_data[key] = child_el.text
return item_data

def _get_items(self, references):
items = []
for reference_el in references:
item_ref = reference_el.get("item_ref")
item = self._get_item(item_ref)
items.append(item)
return items

@staticmethod
def _get_header(items):
header = []
for item in items:
for key in item.keys():
if key not in header:
header.append(key)
return tuple(header)

@staticmethod
def _get_entries(header, items):
entries = []
for item in items:
entry = []
for key in header:
entry.append(item.get(key, ""))
entries.append(tuple(entry))
return entries

def get_oval_items(self, object_id):
collected_object_el = self.collected_objects.get(object_id)
if collected_object_el is None:
return None
references = collected_object_el.findall(
"oval-characteristics:reference", NAMESPACES
)
if len(references) == 0:
return None
items = self._get_items(references)
header = self._get_header(items)
entries = self._get_entries(header, items)
message = None
len_entries = len(entries)
if len_entries > PROCURED_ITEMS_LIMIT:
entries = entries[:PROCURED_ITEMS_LIMIT]
message = f"Collected {len_entries} items, showing only first {PROCURED_ITEMS_LIMIT} items"
return OVALItems(header=header, entries=entries, message=message)
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@
from ..namespaces import NAMESPACES
from .oval_endpoint_information_parser import OVALEndpointInformation
from .shared_static_methods_of_parser import SharedStaticMethodsOfParser
from .oval_items_parser import OVALItemsParser


class OVALObjectParser(OVALEndpointInformation):
def __init__(self, objects, collected_objects, system_data):
self.objects = objects
self.collected_objects = collected_objects
self.system_data = system_data
self.item_parser = OVALItemsParser(collected_objects, system_data)

def _get_oval_message(self, xml_collected_object):
message = xml_collected_object.find(
Expand All @@ -33,6 +35,7 @@ def get_object(self, id_object):
"comment": xml_object.get("comment", ""),
"object_type": SharedStaticMethodsOfParser.get_key_of_xml_element(xml_object),
"object_data": self._get_items(xml_object),
"collected_items": self.item_parser.get_oval_items(id_object)
}
xml_collected_object = self._get_collected_object_xml(id_object)

Expand Down
99 changes: 99 additions & 0 deletions tests/unit_tests/test_oval_items_parser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Copyright 2024, Red Hat, Inc.
# SPDX-License-Identifier: LGPL-2.1-or-later

import pytest
from lxml import etree

from openscap_report.scap_results_parser.namespaces import NAMESPACES
from openscap_report.scap_results_parser.parsers.oval_items_parser import OVALItemsParser


@pytest.fixture
def parser():
sc_ns = NAMESPACES["oval-characteristics"]
is_ns = NAMESPACES["ind-sys"]

o1_id = "oval:example:obj:1"
o2_id = "oval:example:obj:2"
o3_id = "oval:example:obj:3"
i1_id = "11117777"
i2_id = "11117777"
i3_id = "33339999"

o1 = etree.Element("{%s}object" % sc_ns, nsmap=NAMESPACES, id=o1_id, version="1", flag="complete")
o1_r1 = etree.Element("{%s}reference" % sc_ns, nsmap=NAMESPACES, item_ref=i1_id)
o1.append(o1_r1)

o2 = etree.Element("{%s}object" % sc_ns, nsmap=NAMESPACES, id=o2_id, version="1", flag="complete")
o2_r1 = etree.Element("{%s}reference" % sc_ns, nsmap=NAMESPACES, item_ref=i2_id)
o2.append(o2_r1)
o2_r2 = etree.Element("{%s}reference" % sc_ns, nsmap=NAMESPACES, item_ref=i3_id)
o2.append(o2_r2)

o3 = etree.Element("{%s}object" % sc_ns, nsmap=NAMESPACES, id=o3_id, version="1", flag="does not exist")

collected_objects = {
o1_id: o1,
o2_id: o2,
o3_id: o3
}

i1 = etree.Element("{%s}textfilecontent_item" % is_ns, nsmap=NAMESPACES, id=i1_id, status="exists")
i1_filepath = etree.Element("{%s}filepath" % is_ns, nsmap=NAMESPACES)
i1_filepath.text = "/var/cities"
i1.append(i1_filepath)
i1_text = etree.Element("{%s}text" % is_ns, nsmap=NAMESPACES)
i1_text.text = "Paris"
i1.append(i1_text)

i2 = etree.Element("{%s}textfilecontent_item" % is_ns, nsmap=NAMESPACES, id=i2_id, status="exists")
i2_filepath = etree.Element("{%s}filepath" % is_ns, nsmap=NAMESPACES)
i2_filepath.text = "/var/cities"
i2.append(i2_filepath)
i2_text = etree.Element("{%s}text" % is_ns, nsmap=NAMESPACES)
i2_text.text = "London"
i2.append(i2_text)

i3 = etree.Element("{%s}textfilecontent_item" % is_ns, nsmap=NAMESPACES, id=i3_id, status="exists")
i3_filepath = etree.Element("{%s}filepath" % is_ns, nsmap=NAMESPACES)
i3_filepath.text = "/var/cities"
i3.append(i3_filepath)
i3_text = etree.Element("{%s}text" % is_ns, nsmap=NAMESPACES)
i3_text.text = "Prague"
i3.append(i3_text)

system_data = {
i1_id: i1,
i2_id: i2,
i3_id: i3,
}

return OVALItemsParser(collected_objects, system_data)


@pytest.mark.unit_test
def test_oval_items_parser_single(parser):
oi = parser.get_oval_items("oval:example:obj:1")
assert oi is not None
assert oi.header == ("filepath", "text")
assert len(oi.entries) == 1
assert oi.entries[0] == ("/var/cities", "London")

@pytest.mark.unit_test
def test_oval_items_parser_multiple(parser):
oi = parser.get_oval_items("oval:example:obj:2")
assert oi is not None
assert oi.header == ("filepath", "text")
assert len(oi.entries) == 2
assert oi.entries[0] == ("/var/cities", "London")
assert oi.entries[1] == ("/var/cities", "Prague")

@pytest.mark.unit_test
def test_oval_items_parser_dne(parser):
oi = parser.get_oval_items("oval:example:obj:3")
assert oi is None

@pytest.mark.unit_test
def test_oval_items_parser_wrong_object_id(parser):
oi = parser.get_oval_items("oval:example:obj:666")
assert oi is None

0 comments on commit b0100b7

Please sign in to comment.