1
0
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:
Caleb Stewart 2021-05-23 17:28:48 -04:00
parent 67cd1033c5
commit 97c4d256ab
12 changed files with 188 additions and 16 deletions

View 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 %}

View File

@ -0,0 +1,5 @@
{% extends "generic.md" %}
{% block platform %}
## Linux Specific Info!
{% endblock %}

View File

View 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

View File

@ -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

View 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

View File

@ -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):

View File

@ -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):

View File

@ -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]."
)

View File

@ -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):

View File

@ -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

View File

@ -38,3 +38,4 @@ wcwidth==0.1.9
python-rapidjson==0.9.1
ZODB==5.6.0
zodburi==2.4.0
jinja2