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

Add: standalone plugin for evaluating dependencies with a graph #774

Merged
merged 21 commits into from
Mar 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
56d3db1
Add: standalone plugin for evaluating dependencies with a graph
NiklasHargarter Dec 11, 2024
13fb6a7
Change: dependency_graph codeql mixed returns fix
NiklasHargarter Dec 11, 2024
c33b6e3
Change: Revert "Change: dependency_graph codeql mixed returns fix"
NiklasHargarter Dec 12, 2024
25aeecd
Add: standalone dependency_graph unittests
NiklasHargarter Dec 12, 2024
3be9e46
Add: include when a dependency is gated in the dependency graph
NiklasHargarter Dec 31, 2024
109ea7b
Change: dependency_graph minor restructure and changes
NiklasHargarter Jan 6, 2025
de70954
Add: category order, deprecated dependencies to graph standalone plugin
NiklasHargarter Jan 16, 2025
c980a56
Change: dependency_graph checks result structure and output
NiklasHargarter Jan 20, 2025
f2bcd32
Change: various refactors in dependency graph standalone
NiklasHargarter Jan 22, 2025
332a237
Change: correct dependency graph unittests not fixed in last commit
NiklasHargarter Jan 22, 2025
8326517
Change: dependency graph minor refactors
NiklasHargarter Jan 24, 2025
288c3dc
Change: dependency graph rename variables
NiklasHargarter Jan 27, 2025
9c4f3e6
Add: dir and file type for argparser that only allow existing
NiklasHargarter Jan 28, 2025
226f2b5
Change: reuse method for splitting dependencies
NiklasHargarter Jan 28, 2025
a2c65e0
Change: Cleanup dependency_graph
mbrinkhoff Jan 29, 2025
2a7db59
Change: use restructured imports in dependency graph test
NiklasHargarter Jan 30, 2025
ea65f2e
Change: dependency graph optimize duplicate check
NiklasHargarter Feb 10, 2025
3df4d6d
Change: dependency graph cli incorrect error and remove logging
NiklasHargarter Feb 10, 2025
8e9d84d
Change: dependency graph tests
NiklasHargarter Feb 11, 2025
abcb837
Change: dependency graph tests remove test with result in random order
NiklasHargarter Feb 11, 2025
cc8d77e
Change: typos in dependency graph files
NiklasHargarter Mar 3, 2025
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -136,3 +136,4 @@ link.sh

# vts/nasl folder used for testing
nasl
!tests/standalone_plugins/nasl/
29 changes: 24 additions & 5 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ chardet = ">=4,<6"
validators = "^0.34.0"
gitpython = "^3.1.31"
charset-normalizer = "^3.2.0"
networkx = "^3.4.2"

[tool.poetry.group.dev.dependencies]
autohooks = ">=21.7.0"
Expand Down Expand Up @@ -81,6 +82,7 @@ troubadix-changed-cves = 'troubadix.standalone_plugins.changed_cves:main'
troubadix-allowed-rev-diff = 'troubadix.standalone_plugins.allowed_rev_diff:main'
troubadix-file-extensions = 'troubadix.standalone_plugins.file_extensions:main'
troubadix-deprecate-vts = 'troubadix.standalone_plugins.deprecate_vts:main'
troubadix-dependency-graph = 'troubadix.standalone_plugins.dependency_graph.dependency_graph:main'

[build-system]
requires = ["poetry-core>=1.0.0"]
Expand Down
6 changes: 6 additions & 0 deletions tests/standalone_plugins/nasl/21.04/21_script.nasl
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
if(description)
{
script_category(ACT_GATHER_INFO);
script_dependencies( "foo.nasl" );
exit(0);
}
6 changes: 6 additions & 0 deletions tests/standalone_plugins/nasl/22.04/22_script.nasl
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
if(description)
{
script_category(ACT_GATHER_INFO);
script_dependencies( "foo.nasl" );
exit(0);
}
10 changes: 10 additions & 0 deletions tests/standalone_plugins/nasl/common/bar.nasl
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
if(description)
{
script_category(ACT_GATHER_INFO);
script_dependencies( "foo.nasl", "foo.nasl" );

if(FEED_NAME == "GSF" || FEED_NAME == "GEF" || FEED_NAME == "SCM")
script_dependencies("gsf/enterprise_script.nasl");

exit(0);
}
8 changes: 8 additions & 0 deletions tests/standalone_plugins/nasl/common/foo.nasl
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
if(description)
{
script_category(ACT_ATTACK);
script_dependencies( "foobar.nasl", "gsf/enterprise_script.nasl" );
exit(0);
}

script_dependencies( "missing.nasl" );
7 changes: 7 additions & 0 deletions tests/standalone_plugins/nasl/common/foobar.nasl
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
if(description)
{
script_category(ACT_GATHER_INFO);
script_dependencies( "bar.nasl" );
exit(0);
script_tag(name:"deprecated", value:TRUE);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
if(description)
{
script_category(ACT_GATHER_INFO);
exit(0);
}
201 changes: 201 additions & 0 deletions tests/standalone_plugins/test_dependency_graph.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
# SPDX-License-Identifier: GPL-3.0-or-later
# SPDX-FileCopyrightText: 2024 Greenbone AG
import os
import unittest
from io import StringIO
from pathlib import Path
from unittest.mock import patch

from troubadix.plugins.dependency_category_order import VTCategory
from troubadix.standalone_plugins.dependency_graph.cli import parse_args
from troubadix.standalone_plugins.dependency_graph.dependency_graph import (
Reporter,
create_graph,
determine_feed,
extract_category,
extract_dependencies,
get_feed,
get_scripts,
main,
)
from troubadix.standalone_plugins.dependency_graph.models import (
Dependency,
Feed,
Result,
Script,
)


class TestReporter(unittest.TestCase):
def setUp(self):
self.result = Result(
name="TestScript",
warnings=["duplicate dependencies"],
errors=["missing dependencies"],
)

@patch("sys.stdout", new_callable=StringIO)
def test_report_verbosity_2(self, mock_stdout):
reporter = Reporter(verbosity=2)
reporter.report([self.result])

output = mock_stdout.getvalue()

self.assertIn("TestScript - warnings: 1, errors: 1", output)
self.assertIn("warning: duplicate dependencies", output)
self.assertIn("error: missing dependencies", output)


class TestCLIArgs(unittest.TestCase):
@patch(
"sys.argv",
[
"prog",
"--root",
"tests/standalone_plugins/nasl",
"--feed",
"feed_22_04",
"--log",
"info",
],
)
def test_parse_args_ok(self):
args = parse_args()
self.assertEqual(args.root, Path("tests/standalone_plugins/nasl"))
self.assertEqual(args.feed, [Feed.FEED_22_04])
self.assertEqual(args.log, "info")

@patch("sys.stderr", new_callable=StringIO)
@patch("sys.argv", ["prog", "--root", "not_real_dir"])
def test_parse_args_no_dir(self, mock_stderr):
with self.assertRaises(SystemExit):
parse_args()
self.assertRegex(mock_stderr.getvalue(), "invalid directory_type")

@patch("sys.stderr", new_callable=StringIO)
@patch(
"sys.argv",
[
"prog",
"--root",
"tests/standalone_plugins/nasl",
"--feed",
"invalid_feed",
],
)
def test_parse_args_invalid_feed(self, mock_stderr):
with self.assertRaises(SystemExit):
parse_args()
self.assertRegex(mock_stderr.getvalue(), "Invalid Feed value")

@patch.dict(os.environ, {"VTDIR": "/mock/env/path"})
@patch("sys.argv", ["prog"])
def test_parse_args_with_env(self):
args = parse_args()
self.assertEqual(args.root, Path("/mock/env/path"))

@patch("sys.argv", ["prog", "--root", "tests/standalone_plugins/nasl"])
def test_parse_args_defaults(self):
args = parse_args()
self.assertEqual(args.log, "WARNING")
self.assertEqual(args.feed, [Feed.FULL])


class TestDependencyGraph(unittest.TestCase):

def setUp(self) -> None:
self.local_root = "tests/standalone_plugins/nasl"
self.script_content = """
if(description)
{
script_category(ACT_GATHER_INFO);
script_dependencies( "foo.nasl", "foo.nasl" );

if(FEED_NAME == "GSF" || FEED_NAME == "GEF" || FEED_NAME == "SCM")
script_dependencies("gsf/enterprise_script.nasl");

exit(0);
}
"""

def test_get_feed(self):
feed = [Feed.FULL]
scripts = get_feed(Path(self.local_root), feed)
self.assertEqual(len(scripts), 6)

@patch("pathlib.Path.read_text")
def test_get_scripts(self, mock_read_text):
mock_read_text.return_value = self.script_content
scripts = get_scripts(Path(self.local_root) / "common")
self.assertEqual(len(scripts), 4)
self.assertEqual(scripts[0].feed, "community")
self.assertEqual(len(scripts[0].dependencies), 3)
self.assertEqual(scripts[0].category, VTCategory.ACT_GATHER_INFO)
self.assertEqual(scripts[0].deprecated, False)

def test_determine_feed(self):
community_script = Path("foo/script.nasl")
enterprise_script = Path("gsf/script.nasl")

self.assertEqual(determine_feed(community_script), "community")
self.assertEqual(determine_feed(enterprise_script), "enterprise")

def test_extract_dependencies(self):
dependencies = extract_dependencies(self.script_content)
self.assertEqual(len(dependencies), 3)
self.assertEqual(dependencies[0].name, "foo.nasl")
self.assertEqual(dependencies[1].name, "foo.nasl")
self.assertEqual(dependencies[2].name, "gsf/enterprise_script.nasl")
self.assertEqual(dependencies[0].is_enterprise_feed, False)
self.assertEqual(dependencies[1].is_enterprise_feed, False)
self.assertEqual(dependencies[2].is_enterprise_feed, True)

def test_extract_category(self):
category = extract_category(self.script_content)
self.assertEqual(category, VTCategory.ACT_GATHER_INFO)

def test_create_graph(self):
dependency1 = Dependency("bar.nasl", False)
scripts = [
Script("foo.nasl", "community", [dependency1], 0, False),
Script("bar.nasl", "enterprise", [], 0, False),
]
graph = create_graph(scripts)
self.assertEqual(len(list(graph.nodes)), 2)

@patch("sys.stdout", new_callable=StringIO) # mock_stdout (second argument)
@patch("sys.stderr", new_callable=StringIO) # mock_stderr (first argument)
@patch(
"sys.argv", ["prog", "--root", "tests/standalone_plugins/nasl", "-v"]
) # no argument
def test_full_run(self, mock_stderr, mock_stdout):
return_code = main()
output = mock_stdout.getvalue()

self.assertIn("error: missing dependency file: missing.nasl:", output)
self.assertIn(
"error: cyclic dependency: ", # order is random so can't match the output
output,
)
self.assertIn(
"error: unchecked cross-feed-dependency: foo.nasl(community feed) depends on"
" gsf/enterprise_script.nasl(enterprise feed), but the"
" current feed is not properly checked",
output,
)
self.assertIn(
"error: bar.nasl depends on foo.nasl which has a lower category order",
output,
)
self.assertIn(
"error: foo.nasl depends on deprecated script foobar.nasl", output
)
self.assertIn(
"warning: Duplicate dependencies in bar.nasl: foo.nasl", output
)
self.assertIn(
"warning: cross-feed-dependency: bar.nasl(community feed)"
" depends on gsf/enterprise_script.nasl(enterprise feed)",
output,
)
self.assertEqual(return_code, 1)
1 change: 1 addition & 0 deletions tests/test_naslinter.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ def test_generate_file_list_with_exclude_patterns(self):
"**/templates/*/*.nasl",
"**/test_files/*",
"**/test_files/**/*.nasl",
"**/tests/standalone_plugins/**/*.nasl",
],
include_patterns=["**/*.nasl", "**/*.inc"],
)
Expand Down
18 changes: 18 additions & 0 deletions troubadix/argparser.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,20 +26,38 @@
from pontos.terminal import Terminal


# allows non existent paths and directory paths
def directory_type(string: str) -> Path:
directory_path = Path(string)
if directory_path.exists() and not directory_path.is_dir():
raise ValueError(f"{string} is not a directory.")
return directory_path


# allows only existing directory paths
def directory_type_existing(string: str) -> Path:
directory_path = Path(string)
if not directory_path.is_dir():
raise ValueError(f"{string} is not a directory.")
return directory_path


# allows non existent paths and file paths
def file_type(string: str) -> Path:
file_path = Path(string)
if file_path.exists() and not file_path.is_file():
raise ValueError(f"{string} is not a file.")
return file_path


# allows only existing file paths
def file_type_existing(string: str) -> Path:
file_path = Path(string)
if not file_path.is_file():
raise ValueError(f"{string} is not a file.")
return file_path


def check_cpu_count(number: str) -> int:
"""Make sure this value is valid
Default: use half of the available cores to not block the machine"""
Expand Down
Loading