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
2 changes: 1 addition & 1 deletion .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
max-parallel: 1
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
python-version: ['3.10', '3.11', '3.12', '3.13']
python-version: ['3.10', '3.11', '3.12', '3.13', '3.14']

steps:
- uses: actions/checkout@v4
Expand Down
44 changes: 44 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,49 @@ Visit [LimberDuck.org][2] to find out more!
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.8.0] - 2026-02-07

### Added

#### CLI

NFR CLI got new `scan` option `--plugin-publication-date` / `-plpd`. Read more about plugin publication date feature in [NFR documentation](https://limberduck.org/en/latest/tools/nessus-file-reader/using.html#nfr-scan-plugin-publication-date).


- Usage: `--plugin-publication-date <format> <group>` where
- `format` is one of: `table`, `bar`, `line`, `heatmap`
- `group` is one of: `day`, `month`, `year`

- Use it with filter option `--filter` / `-f` to narrow down the results, e.g.:
- `-f "[?risk_factor != 'None']"` to include only plugins with risk factor other than None
- `-f "[?severity > '2']"` to include only plugins with severity higher then Medium (0 - Informational, 1 - Low, 2 - Medium, 3 - High, 4 - Critical)
- `-f "[?plugin_type == 'local']"` to include only local plugins
- `-f "[?plugin_id == '19506']"` to include only plugin with ID 19506

- Requirements update
- new:
- plotext>=5.3.2


- Other command options have been shortened with aliases:
- `file` command:
- `--size` -> `-si`
- `--structure` -> `-st`
- `--split` -> `-sp`
- `scan` command:
- `--scan-summary` -> `--scs`
- `--scan-summary-legend` -> `--scsl`
- `--scan-file-source` -> `--scfs`
- `--plugin-severity` -> `--pls`
- `--plugin-severity-legend` -> `--plsl`
- `--policy-summary` -> `--pos`

### Changed

- tests for python
- added: 3.14


## [0.7.2] - 2026-01-23

### Fixed
Expand Down Expand Up @@ -156,6 +199,7 @@ Plugins number used during the scan.

- Initial release

[0.8.0]: https://github.com/LimberDuck/nessus-file-reader/compare/v0.7.2...v0.8.0
[0.7.2]: https://github.com/LimberDuck/nessus-file-reader/compare/v0.7.1...v0.7.2
[0.7.1]: https://github.com/LimberDuck/nessus-file-reader/compare/v0.7.0...v0.7.1
[0.7.0]: https://github.com/LimberDuck/nessus-file-reader/compare/v0.6.0...v0.7.0
Expand Down
4 changes: 2 additions & 2 deletions nessus_file_reader/__about__.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@
"performed by Tenable Nessus and Tenable Security Center."
)
__uri__ = "https://github.com/LimberDuck"
__version__ = "0.7.2"
__release_date__ = "2026.01.23"
__version__ = "0.8.0"
__release_date__ = "2026.02.07"
__author__ = "Damian Krawczyk"
__email__ = "damian.krawczyk@limberduck.org"
__license_name__ = "GNU GPLv3"
Expand Down
197 changes: 184 additions & 13 deletions nessus_file_reader/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
import glob
import tabulate
import jmespath

import plotext as plt
from datetime import datetime

def print_version(ctx, param, value):
if not value or ctx.resilient_parsing:
Expand Down Expand Up @@ -78,10 +79,10 @@ def cli(ctx, update_check):

@cli.command()
@add_arguments(_file_arguments)
@click.option("--size", is_flag=True, help="file size")
@click.option("--structure", is_flag=True, help="file structure")
@click.option("--size", "-si", is_flag=True, help="file size")
@click.option("--structure", "-st", is_flag=True, help="file structure")
@click.option(
"--split", type=int, help="file split into batches per number of ReportHost"
"--split", "-sp", type=int, help="file split into batches per number of ReportHost"
)
def file(files, size, structure, split):
"""Options related to nessus file."""
Expand Down Expand Up @@ -151,23 +152,36 @@ def file(files, size, structure, split):

@cli.command()
@add_arguments(_file_arguments)
@click.option("--scan-summary", is_flag=True, help="Scan summary")
@click.option("--scan-summary-legend", is_flag=True, help="Show scan summary legend")
@click.option("--plugin-severity", is_flag=True, help="Plugin severity")
@click.option(
"--plugin-severity-legend", is_flag=True, help="Show plugin severity legend"
)
@click.option("--policy-summary", is_flag=True, help="Policy summary")
@click.option("--scan-summary", "-scs",is_flag=True, help="Scan summary")
@click.option("--scan-summary-legend", "-scsl", is_flag=True, help="Show scan summary legend")
@click.option(
"--scan-file-source",
"-scfs",
is_flag=True,
help="Source of scan file e.g. Nessus, Tenable.sc, Tenable.io",
)
@click.option("--plugin-severity", "-pls", is_flag=True, help="Plugin severity")
@click.option(
"--plugin-severity-legend", "-plsl", is_flag=True, help="Show plugin severity legend"
)
@click.option(
"--plugin-publication-date",
"-plpd",
nargs=2,
type=click.Tuple([
click.Choice(["table", "bar", "line", "heatmap"], case_sensitive=False),
click.Choice(["day", "month", "year"], case_sensitive=False),
]),
default=None,
help="Count plugins by publication date. Usage: --plugin-publication-date <format> <group> "
"where format is table|bar|line|heatmap and group is day|month|year (heatmap always shows months vs years)",
)
@click.option("--policy-summary", "-pos", is_flag=True, help="Policy summary")
@click.option(
"--filter",
"-f",
help="filter data with JMESPath. See https://jmespath.org/ for more information and examples. "
"Works with --plugin-severity only. ",
help="Filter data with JMESPath. See https://jmespath.org/ for more information and examples. "
"Works with --plugin-severity and --plugin-publication-date. ",
)
def scan(
files,
Expand All @@ -178,6 +192,7 @@ def scan(
scan_file_source,
policy_summary,
filter,
plugin_publication_date,
):
"""Options related to content of nessus file on scan level."""

Expand All @@ -187,6 +202,7 @@ def scan(
scan_file_source_data = []
policy_summary_data = []
plugin_severity_data = []
plugin_publication_date_data = []
for file in files:
if os.path.isdir(file):
os_separator = os.path.sep
Expand Down Expand Up @@ -367,6 +383,36 @@ def scan(
}
)

if plugin_publication_date:
for report_host in nfr.scan.report_hosts(root):
report_items_per_host = nfr.host.report_items(report_host)
for report_item in report_items_per_host:
pub_date = nfr.plugin.report_item_value(
report_item, "plugin_publication_date"
)
if pub_date:
risk_factor = nfr.plugin.report_item_value(
report_item, "risk_factor"
)
plugin_type = nfr.plugin.report_item_value(
report_item, "plugin_type"
)
severity = nfr.plugin.report_item_value(
report_item, "severity"
)
plugin_id = nfr.plugin.report_item_value(
report_item, "pluginID"
)
plugin_publication_date_data.append(
{
"plugin_publication_date": pub_date,
"risk_factor": risk_factor,
"severity": severity,
"plugin_id": plugin_id,
"plugin_type": plugin_type,
}
)

if scan_summary:
header = summary_data[0].keys()
rows = [x.values() for x in summary_data]
Expand Down Expand Up @@ -402,6 +448,131 @@ def scan(
rows = [x.values() for x in policy_summary_data]
print(tabulate.tabulate(rows, header))

if plugin_publication_date:
# Extract format and group from tuple
pub_date_format, pub_date_group = plugin_publication_date

# Apply JMESPath filter if provided
default_filter = "@"
if filter:
expression = jmespath.compile(filter)
else:
expression = jmespath.compile(default_filter)

filtered_data = expression.search(plugin_publication_date_data)

# Helper function to group date by day, month, or year
def group_date(date_str, group_by):

if "/" in date_str:
date_obj = datetime.strptime(date_str, "%Y/%m/%d")
else:
date_obj = datetime.strptime(date_str, "%Y-%m-%d")

if group_by == "year":
return date_obj.strftime("%Y")
elif group_by == "month":
return date_obj.strftime("%Y-%m")
else: # day
return date_obj.strftime("%Y-%m-%d")

# Aggregate by publication date (grouped)
date_counts = {}
for item in filtered_data:
pub_date = item["plugin_publication_date"]
grouped_date = group_date(pub_date, pub_date_group)
if grouped_date in date_counts:
date_counts[grouped_date] += 1
else:
date_counts[grouped_date] = 1

# Convert to sorted list for tabulate
publication_date_list = [
{"plugin_publication_date": date, "number_of_plugins": count}
for date, count in sorted(date_counts.items())
]

if publication_date_list:
if pub_date_format.lower() == "table":
header = publication_date_list[0].keys()
rows = [x.values() for x in publication_date_list]
print(tabulate.tabulate(rows, header))
else:

plt.clear_figure()

if pub_date_format.lower() == "bar":
# Prepare data for plotting (dates already normalized by grouping)
dates = [item["plugin_publication_date"] for item in publication_date_list]
counts = [item["number_of_plugins"] for item in publication_date_list]
group_label = pub_date_group.capitalize()

# Simple bar plot with width limit to prevent terminal wrapping
plt.simple_bar(dates, counts, width=60, color=(243, 121, 3))
plt.title(f"Plugins by Publication {group_label}")

elif pub_date_format.lower() == "line":
# Prepare data for plotting (dates already normalized by grouping)
dates = [item["plugin_publication_date"] for item in publication_date_list]
counts = [item["number_of_plugins"] for item in publication_date_list]
group_label = pub_date_group.capitalize()

# Line plot - dark theme with white axes
plt.theme("dark")

# Set date format based on grouping
if pub_date_group == "year":
plt.date_form("Y")
elif pub_date_group == "month":
plt.date_form("Y-m")
else:
plt.date_form("Y-m-d")

plt.plot(dates, counts, marker="braille", color=(243, 121, 3))
plt.title(f"Plugins by Publication {group_label}")
plt.xlabel(f"Publication {group_label}")
plt.ylabel("Number of Plugins")

elif pub_date_format.lower() == "heatmap":
# Heatmap showing months (rows) vs years (columns)

# Build month-year matrix from filtered data
month_year_counts = {}
for item in filtered_data:
pub_date = item["plugin_publication_date"]
if "/" in pub_date:
date_obj = datetime.strptime(pub_date, "%Y/%m/%d")
else:
date_obj = datetime.strptime(pub_date, "%Y-%m-%d")
year = date_obj.year
month = date_obj.month
key = (year, month)
month_year_counts[key] = month_year_counts.get(key, 0) + 1

if month_year_counts:
# Get all years and create matrix
years = sorted(set(k[0] for k in month_year_counts.keys()))
months = list(range(1, 13))
month_names = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]

# Create matrix: rows=months, cols=years
matrix = []
for month in months:
row = [month_year_counts.get((year, month), 0) for year in years]
matrix.append(row)

plt.theme("dark")

plt.matrix_plot(matrix)
plt.title("Plugins by Publication Month/Year")
plt.xlabel("Year")
plt.ylabel("Month")
plt.xticks(range(len(years)), [str(y) for y in years])
plt.yticks(range(12), month_names)

plt.show()

except FileNotFoundError as e:
print(e.strerror)

Expand Down
2 changes: 1 addition & 1 deletion nessus_file_reader/_version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.7.2"
__version__ = "0.8.0"
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ click>=8.2.1
tabulate>=0.9.0
jmespath>=1.0.1
requests>=2.32.5
packaging>=25.0
packaging>=25.0
plotext>=5.3.2
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
install_requires=required,
entry_points={"console_scripts": ["nfr = nessus_file_reader.__main__:main"]},
classifiers=[
"Programming Language :: Python :: 3.14",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.11",
Expand Down
Loading