mirror of
https://github.com/calebstewart/pwncat.git
synced 2024-11-27 19:04:15 +01:00
Added report module for templated markdown reports
Reports are generated based on platform and use Jinja2. Report templates are in pwncat/data/reports. I still need to implement the full report for the individual platforms, but have some boilerplate in the generic template. The module will also render markdown to the terminal via rich markdown, however tables are currently not rendered properly.
This commit is contained in:
parent
67cd1033c5
commit
97c4d256ab
55
pwncat/data/reports/generic.md
Normal file
55
pwncat/data/reports/generic.md
Normal file
@ -0,0 +1,55 @@
|
||||
# {{ platform.channel | remove_rich }} | {{ session.run("enumerate", types=["system.hostname"]) | first_or_none | attr_or("hostname", "unknown hostname") }}
|
||||
|
||||
This enumeration report was automatically generated with [pwncat](https://github.com/calebstewart/pwncat).
|
||||
The report was generated on {{ datetime }}.
|
||||
|
||||
## Common System Information
|
||||
|
||||
| Platform | {{ platform.name }} |
|
||||
|--------------|---------------------|
|
||||
| Architecture | {{ session.run("enumerate", types=["system.arch"]) | first_or_none | title_or_unknown }} |
|
||||
| Hostname | {{ session.run("enumerate", types=["system.hostname"]) | first_or_none | title_or_unknown }} |
|
||||
| ASLR | {{ session.run("enumerate", types=["system.aslr"]) | first_or_none | title_or_unknown }} |
|
||||
| Container | {{ session.run("enumerate", types=["system.container"]) | first_or_none | title_or_unknown }} |
|
||||
| Distribution | {{ session.run("enumerate", types=["system.distro"]) | first_or_none | title_or_unknown }} |
|
||||
|
||||
{% if session.run("enumerate", types=["implant.*"]) %}
|
||||
## Installed Implants
|
||||
{% for implant in session.run("enumerate", types=["implant.*"]) %}
|
||||
- {{ implant | title_or_unknown }}
|
||||
{% endfor %}
|
||||
|
||||
{% endif %}
|
||||
{% if session.run("enumerate", types=["escalate.*"]) %}
|
||||
## Escalation Methods
|
||||
{% for escalation in session.run("enumerate", types=["escalate.*"]) %}
|
||||
- {{ escalation | title_or_unknown }}
|
||||
{% endfor %}
|
||||
|
||||
{% endif %}
|
||||
{% if session.run("enumerate", types=["ability.*"]) %}
|
||||
## Abilities
|
||||
{% for ability in session.run("enumerate", types=["ability.*"]) %}
|
||||
- {{ ability | title_or_unknown }}
|
||||
{% endfor %}
|
||||
|
||||
{% endif %}
|
||||
{% if session.run("enumerate", types=["tamper"]) %}
|
||||
## Modified Settings and Files
|
||||
{% for tamper in session.run("enumerate", types=["tamper"]) %}
|
||||
- {{ tamper | title_or_unknown }}
|
||||
{% endfor %}
|
||||
|
||||
{% endif %}
|
||||
## Enumerated Users
|
||||
{% for user in session.iter_users() %}
|
||||
- {{ user | title_or_unknown }}
|
||||
{% endfor %}
|
||||
|
||||
## Enumerated Groups
|
||||
{% for group in session.iter_groups() %}
|
||||
- {{ group | title_or_unknown }}
|
||||
{% endfor %}
|
||||
|
||||
{% block platform %}
|
||||
{% endblock %}
|
5
pwncat/data/reports/linux.md
Normal file
5
pwncat/data/reports/linux.md
Normal file
@ -0,0 +1,5 @@
|
||||
{% extends "generic.md" %}
|
||||
|
||||
{% block platform %}
|
||||
## Linux Specific Info!
|
||||
{% endblock %}
|
0
pwncat/data/reports/windows.md
Normal file
0
pwncat/data/reports/windows.md
Normal file
@ -145,6 +145,11 @@ class Session:
|
||||
):
|
||||
return group
|
||||
|
||||
def iter_groups(self):
|
||||
""" Iterate over groups for the target """
|
||||
|
||||
yield from self.run("enumerate.gather", progress=False, types=["group"])
|
||||
|
||||
def register_fact(self, fact: "pwncat.db.Fact"):
|
||||
"""Register a fact with this session's target. This is useful when
|
||||
a fact is generated during execution of a command or module, but is
|
||||
|
@ -9,16 +9,11 @@ from pathlib import Path
|
||||
import pwncat.modules
|
||||
from rich import markup
|
||||
from pwncat import util
|
||||
from pwncat.util import console
|
||||
from pwncat.util import console, strip_markup
|
||||
from rich.progress import Progress
|
||||
from pwncat.modules.enumerate import EnumerateModule
|
||||
|
||||
|
||||
def strip_markup(styled_text: str) -> str:
|
||||
text = markup.render(styled_text)
|
||||
return text.plain
|
||||
|
||||
|
||||
def list_wrapper(iterable):
|
||||
"""Wraps a list in a generator"""
|
||||
yield from iterable
|
||||
|
106
pwncat/modules/agnostic/report.py
Normal file
106
pwncat/modules/agnostic/report.py
Normal file
@ -0,0 +1,106 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import datetime
|
||||
|
||||
import jinja2
|
||||
from pwncat.util import console, strip_markup
|
||||
from rich.markdown import Markdown
|
||||
from pwncat.modules import Bool, Argument, BaseModule, ModuleFailed
|
||||
|
||||
|
||||
class Module(BaseModule):
|
||||
"""
|
||||
Run common enumerations and produce a report. Optionally, write the report
|
||||
in markdown format to a file.
|
||||
"""
|
||||
|
||||
PLATFORM = None
|
||||
ARGUMENTS = {
|
||||
"output": Argument(
|
||||
str,
|
||||
default="terminal",
|
||||
help="Path to markdown file to store report (default: render to terminal)",
|
||||
),
|
||||
"template": Argument(
|
||||
str,
|
||||
default="platform name",
|
||||
help="The name of the template to use (default: platform name)",
|
||||
),
|
||||
"fmt": Argument(
|
||||
str,
|
||||
default="md",
|
||||
help='The format of the output. This can be "md" or "html". (default: md)',
|
||||
),
|
||||
"custom": Argument(
|
||||
Bool,
|
||||
default=False,
|
||||
help="Use a custom template; the template argument must be the path to a jinja2 template",
|
||||
),
|
||||
}
|
||||
|
||||
def run(self, session: "pwncat.manager.Session", output, template, fmt, custom):
|
||||
""" Perform enumeration and optionally write report """
|
||||
|
||||
if custom:
|
||||
env = jinja2.Environment(
|
||||
loader=jinja2.FileSystemLoader(os.getcwd()),
|
||||
# autoescape=jinja2.select_autoescape(["md", "html"]),
|
||||
trim_blocks=True,
|
||||
lstrip_blocks=True,
|
||||
)
|
||||
else:
|
||||
env = jinja2.Environment(
|
||||
loader=jinja2.PackageLoader("pwncat", "data/reports"),
|
||||
# autoescape=jinja2.select_autoescape(["md", "html"]),
|
||||
trim_blocks=True,
|
||||
lstrip_blocks=True,
|
||||
)
|
||||
|
||||
if template == "platform name":
|
||||
use_platform = True
|
||||
template = session.platform.name
|
||||
else:
|
||||
use_platform = False
|
||||
|
||||
env.filters["first_or_none"] = lambda thing: thing[0] if thing else None
|
||||
env.filters["attr_or"] = (
|
||||
lambda fact, name, default=None: getattr(fact, name)
|
||||
if fact is not None
|
||||
else default
|
||||
)
|
||||
env.filters["title_or_unknown"] = (
|
||||
lambda fact: strip_markup(fact.title(session))
|
||||
if fact is not None
|
||||
else "unknown"
|
||||
)
|
||||
env.filters["remove_rich"] = lambda thing: strip_markup(str(thing))
|
||||
|
||||
try:
|
||||
template = env.get_template(f"{template}.{fmt}")
|
||||
except jinja2.TemplateNotFound as exc:
|
||||
if use_platform:
|
||||
try:
|
||||
template = env.get_template(f"generic.{fmt}")
|
||||
except jinja2.TemplateNotFound as exc:
|
||||
raise ModuleFailed(str(exc)) from exc
|
||||
else:
|
||||
raise ModuleFailed(str(exc)) from exc
|
||||
|
||||
# Just some convenience things for the templates
|
||||
context = {
|
||||
"target": session.target,
|
||||
"manager": session.manager,
|
||||
"session": session,
|
||||
"platform": session.platform,
|
||||
"datetime": datetime.datetime.now(),
|
||||
}
|
||||
|
||||
try:
|
||||
if output != "terminal":
|
||||
with open(output, "w") as filp:
|
||||
template.stream(context).dump(filp)
|
||||
else:
|
||||
markdown = Markdown(template.render(context))
|
||||
console.print(markdown)
|
||||
except jinja2.TemplateError as exc:
|
||||
raise ModuleFailed(str(exc)) from exc
|
@ -18,8 +18,8 @@ class ASLRStateData(Fact):
|
||||
|
||||
def title(self, session):
|
||||
if self.state == 0:
|
||||
return f"ASLR is [green]disabled[/green]"
|
||||
return f"ASLR is [red]enabled[/red]"
|
||||
return "[green]disabled[/green]"
|
||||
return "[red]enabled[/red]"
|
||||
|
||||
|
||||
class Module(EnumerateModule):
|
||||
|
@ -18,7 +18,7 @@ class ContainerData(Fact):
|
||||
""" what type of container? either docker or lxd """
|
||||
|
||||
def title(self, session):
|
||||
return f"Running in a [yellow]{self.type}[/yellow] container"
|
||||
return f"[yellow]{self.type}[/yellow]"
|
||||
|
||||
|
||||
class Module(EnumerateModule):
|
||||
|
@ -2,9 +2,8 @@
|
||||
import dataclasses
|
||||
from typing import List
|
||||
|
||||
import rich.markup
|
||||
|
||||
import pwncat
|
||||
import rich.markup
|
||||
from pwncat import util
|
||||
from pwncat.db import Fact
|
||||
from pwncat.platform.linux import Linux
|
||||
@ -30,7 +29,7 @@ class DistroVersionData(Fact):
|
||||
|
||||
def title(self, session):
|
||||
return (
|
||||
f"Running [blue]{rich.markup.escape(str(self.name))}[/blue] ([cyan]{rich.markup.escape(self.ident)}[/cyan]), "
|
||||
f"[blue]{rich.markup.escape(str(self.name))}[/blue] ([cyan]{rich.markup.escape(self.ident)}[/cyan]), "
|
||||
f"Version [red]{rich.markup.escape(str(self.version))}[/red], "
|
||||
f"Build ID [green]{rich.markup.escape(str(self.build_id))}[/green]."
|
||||
)
|
||||
|
@ -3,11 +3,10 @@ import json
|
||||
import dataclasses
|
||||
from typing import List, Optional
|
||||
|
||||
import pkg_resources
|
||||
|
||||
import pwncat
|
||||
from pwncat.db import Fact
|
||||
import pkg_resources
|
||||
from pwncat import util
|
||||
from pwncat.db import Fact
|
||||
from pwncat.platform.linux import Linux
|
||||
from pwncat.modules.enumerate import Schedule, EnumerateModule
|
||||
|
||||
@ -41,7 +40,7 @@ class HostnameData(Fact):
|
||||
""" The determined architecture. """
|
||||
|
||||
def title(self, session):
|
||||
return f"Hostname [cyan]{self.hostname}[/cyan]"
|
||||
return f"[cyan]{self.hostname}[/cyan]"
|
||||
|
||||
|
||||
class KernelVersionData(Fact):
|
||||
|
@ -19,6 +19,7 @@ from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||
from socketserver import TCPServer, BaseRequestHandler
|
||||
|
||||
import netifaces
|
||||
from rich import markup
|
||||
from colorama import Fore, Style
|
||||
from rich.console import Console
|
||||
from prompt_toolkit.shortcuts import ProgressBar
|
||||
@ -100,6 +101,12 @@ class RawModeExit(Exception):
|
||||
<prefix>+<C-d> key combination to return to the local prompt."""
|
||||
|
||||
|
||||
def strip_markup(styled_text: str) -> str:
|
||||
""" Strip rich markup from text """
|
||||
text = markup.render(styled_text)
|
||||
return text.plain
|
||||
|
||||
|
||||
def isprintable(data) -> bool:
|
||||
"""
|
||||
This is a convenience function to be used rather than the usual
|
||||
|
@ -38,3 +38,4 @@ wcwidth==0.1.9
|
||||
python-rapidjson==0.9.1
|
||||
ZODB==5.6.0
|
||||
zodburi==2.4.0
|
||||
jinja2
|
||||
|
Loading…
Reference in New Issue
Block a user