pwndoc-ng / pwndoc-ng

Pentest Report Generator
https://pwndoc-ng.github.io/pwndoc-ng/#/
MIT License
351 stars 82 forks source link

Numbered lists continue between report sections instead of resetting to one. #318

Open Elevennails opened 7 months ago

Elevennails commented 7 months ago

Is your feature request related to a problem? Please describe. In our template and the default untouched one, number lists continue numbering between findings and report sections image

second finding looks like this image

Describe the solution you'd like the information in pwndoc POC looks like this. Which is what was expected in the default template report (test1) image

(test2) image

Describe alternatives you've considered I've tried modifying numbering.xls, but it I can't seem to make the numbering restart as expected

diggidong commented 7 months ago

Could you pls provide the code you used in your word template to generate that output? i have the suspicion that it is due to looping..

Elevennails commented 7 months ago

The example shown in the issue was from the default template. You should be able to reproduce.

Thanks Si On Mon, 19 Feb 2024, 15:19 diggidong, @.***> wrote:

Could you pls provide the code you used in your word template to generate that output? i have the suspicion that it is due to looping..

— Reply to this email directly, view it on GitHub https://github.com/pwndoc-ng/pwndoc-ng/issues/318#issuecomment-1952673888, or unsubscribe https://github.com/notifications/unsubscribe-auth/AF6VEQCWFSMVSTWQ2FCZHKTYUNURNAVCNFSM6AAAAABDKNC4N2VHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMYTSNJSGY3TGOBYHA . You are receiving this because you authored the thread.Message ID: @.***>

PereProteus commented 7 months ago

This is word issue and another style based problem that needs to be solved in the document and not in pwndoc-ng. I havent tried to resolve it myself and just use bullets to make life easier. These are the recommendations I've come across: https://answers.microsoft.com/en-us/msoffice/forum/all/numbered-lists-will-not-restart-at-1/f51a57a1-870b-40dd-9192-5535223eb8d0 https://superuser.com/questions/1762050/fix-microsoft-word-numbering Basically create a new numbered list style, set it based on how you want numbering at list index 2 or whatever works for your document and ensure it starts at '1.' Then ensure all of your report templates have this style. If I get excited about it I might update the demo document and do a pull request.

Elevennails commented 6 months ago

If you get time, I would be very interested to see your new template. I've spent some considerable time with the formatting suggested in the links, but I seem to still be getting the same result as before even with a custom multi-list. It's as if pwndoc needs to send some kind of end-of-list marker to reset the count.

Thanks for the links they were very interesting and have allowed me to fix some other formatting problems I had with bullets

Thanks Si

Elevennails commented 6 months ago

I've made some progress with the following code. It looks like the problem is that word expects a new numId for each list. Setting numID to "2" as per the document doesn't work for numbered lists as it thinks the next list is a continuation.

The following code is a partial success. However, I need to somehow apply some formatting to the new abstraction for each new list at level 0 to make it render appropriately in my doc. For my requirements, I need to add the following to the new list definitions:

<w:pPr>
    <w:spacing w:before="0" w:after="0"/>
    <w:ind w:left="795" w:hanging="624"/>
</w:pPr>

If anyone has any ideas, I'd be grateful.

The following code will fix issues for number restarts but without formatting controls. You will be able to force the font it uses but specifying.

{@text | convertHTML: ‘ReportText‘}

in your template. Where "ReportText" is a style in Word. Unfortunately, this won't allow me to set the paragraph or number indents as they would interfere with the rest of the document body.

Somehow we need to define formatting in the abstraction and match it to the Id used in the ol tag below:

Here is my html2ooxml.js thought someone might find this useful. FYI I have done other mods to increase some usability. The modified fonts H5 and H6 also need some CSS changes in the frontend to make them work in the editor

Plenty of comments inline my changes all start //11: hopefully they make sense.

let docx = require("docx");
let xml = require("xml");
let htmlparser = require("htmlparser2");
let nextNumId = 1000; //11: Creates a counter high up in the range for numID for numbered lists to use so that we don't clash with existing ones
let currentNumIds = {}; //11: This is a counter that we will use to create new lists within each loop

function html2ooxml(html, style = "") {
  if (html === "") return html;
  if (!html.match(/^<.+>/)) html = `<p>${html}</p>`;
  let doc = new docx.Document({ sections: [] });
  let paragraphs = [];
  let cParagraph = null;
  let cRunProperties = {};
  let cParagraphProperties = {};
  let list_state = [];
  let inCodeBlock = false;
  let inTable = false;
  let inTableRow = false;
  let inTableCell = false;
  let cellHasText = false;
  let tmpAttribs = {};
  let tableHeader = false
  let tmpTable = [];
  let tmpCells = [];
  let tmpCellContent = [];
  let parser = new htmlparser.Parser(
    {
      onopentag(tag, attribs) {
        if (tag === "h1") {
          cParagraph = new docx.Paragraph({ heading: "Heading1" });
        } else if (tag === "h2") {
          cParagraph = new docx.Paragraph({ heading: "Heading2" });
        } else if (tag === "h3") {
          cParagraph = new docx.Paragraph({ heading: "Heading3" });
        } else if (tag === "h4") {
          cParagraph = new docx.Paragraph({ heading: "Heading4" });
        } else if (tag === "h5") {
          cParagraph = new docx.Paragraph({ heading: "Code" }); //11: reused H5 so I can have highlights in blocks (anywhere)
        } else if (tag === "h6") {
          cParagraph = new docx.Paragraph({ heading: "TableHeading" }); //11: Used H6 so i can give tables headings formatting
        } else if (tag === "div" || tag === "p") {
          if (style && typeof style === 'string')
            cParagraphProperties.style = style
          cParagraph = new docx.Paragraph(cParagraphProperties)
        } else if (tag === "table") {
          inTable = true;
        } else if (tag === "td") {
          tmpAttribs = attribs;
          inTableCell = true;
          cellHasText = false;
          tmpCellContent = [];
        } else if (tag === "th") {
          inTableCell = true;
          tableHeader = true;
          tmpAttribs = attribs;
          tmpCellContent = [];
          cellHasText = false;
        } else if (tag === "tr") {
          inTableRow = true;
        } else if (tag === "pre") {
          inCodeBlock = true;
          cParagraph = new docx.Paragraph({ style: "Code" });
        } else if (tag === "br") {
          if (inCodeBlock) {
            paragraphs.push(cParagraph)
            cParagraph = new docx.Paragraph({ style: "Code" })
          } else {
            cParagraph.addChildElement(new docx.Run({ break: 1 }))
          }
        } else if (tag === "b" || tag === "strong") {
          cRunProperties.bold = true;
        } else if (tag === "i" || tag === "em") {
          cRunProperties.italics = true;
        } else if (tag === "u") {
          cRunProperties.underline = {};
        } else if (tag === "strike" || tag === "s") {
          cRunProperties.strike = true;
        } else if (tag === "mark") {
          //Possible values are: black, blue, cyan, darkBlue, darkCyan, darkGray, darkGreen, darkMagenta, darkRed, darkYellow, green, lightGray, magenta, none, red, white, yellow
          let color;
          switch (attribs["data-color"]) {
            case "#ffff00":
              color = "yellow";
              break;
            case "#fe0000":
              color = "red";
              break;
            case "#00ff00":
              color = "green";
              break;
            case "#00ffff":
              color = "cyan";
              break;
          }
          cRunProperties.highlight = color;
        } else if (tag === "a") {
          cRunProperties.link = attribs.href;
        } else if (tag === "br") {
          if (inCodeBlock) {
            paragraphs.push(cParagraph);
            cParagraph = new docx.Paragraph({ style: "Code" });
          } else cParagraph.addChildElement(new docx.Run({ break: 1 }));
        } else if (tag === "ul") {
          list_state.push("bullet");
        } else if (tag === "ol") {
          const level = list_state.length; //11: Here we get the list length
          list_state.push("number"); 
          currentNumIds[level] = nextNumId++; //11:each time we create a new list we increment the list counter for numId
        } else if (tag === "li") {
          let level = list_state.length - 1;
          if (level >= 0 && list_state[level] === "bullet")
            cParagraphProperties.bullet = { level: 0 }; //11: I've forced this to 0 as the level was incrementing if a bullet list occurred immediately after a numbered list. current tiptap2 doesn't allow multi-level lists anyway.
          else if (level >= 0 && list_state[level] === "number") {
            cParagraphProperties.numbering = { reference: currentNumIds[level], level: level }; //11: This line has changed to use the current numId again level could be forced to 0 because of limitations in tiptap2.
          }
          else cParagraphProperties.bullet = { level: 0 };
        } else if (tag === "code") {
          cRunProperties.style = "CodeChar";
        } else if (tag === "legend" && attribs && attribs.alt !== "undefined") {
          let label = attribs.label || "Figure";
          cParagraph = new docx.Paragraph({
            style: "Caption",
            alignment: docx.AlignmentType.CENTER,
          });
          cParagraph.addChildElement(new docx.TextRun(`${label} `));
          cParagraph.addChildElement(new docx.SimpleField(`SEQ ${label}`, "1"));
          cParagraph.addChildElement(new docx.TextRun(` - ${attribs.alt}`));
        }
      },

      ontext(text) {
        if (cRunProperties.link) {
          cParagraph.addChildElement(new docx.TextRun({ "text": `{_|link|_{${text}|-|${cRunProperties.link}}_|link|_}`, "style": "Hyperlink" }));

        } else if (text && cParagraph) {
          if (inTableCell) {
            cellHasText = true;
          }
          cRunProperties.text = text;
          cParagraph.addChildElement(new docx.TextRun(cRunProperties));
        }
      },

      onclosetag(tag) {
        if (
          [
            "h1",
            "h2",
            "h3",
            "h4",
            "h5",
            "h6",
            "div",
            "p",
            "pre",
            "img",
            "legend",
            //"table",
            /* "tr",
            "th", */
          ].includes(tag)) {

          if (inTableCell) {
            tmpCellContent.push(cParagraph)
          } else {
            paragraphs.push(cParagraph);
          }

          cParagraph = null;
          cParagraphProperties = {};
          if (tag === "pre") inCodeBlock = false;
        } else if (tag === "b" || tag === "strong") {
          delete cRunProperties.bold;
        } else if (tag === "i" || tag === "em") {
          delete cRunProperties.italics;
        } else if (tag === "u") {
          delete cRunProperties.underline;
        } else if (tag === "mark") {
          delete cRunProperties.highlight;
        } else if (tag === "strike" || tag === "s") {
          delete cRunProperties.strike;
        } else if (tag === "ul" || tag === "ol") {
          list_state.pop();
          if (list_state.length === 0) cParagraphProperties = {};
        } else if (tag === "li") {            //11: added this block to control the closure of bullets and numbered lists 
          let level = list_state.length - 1;  
          if (level >= 0 && list_state[level] === "bullet")  
            cParagraphProperties.bullet = { level: 0 };    
          else if (level >= 0 && list_state[level] === "number"){ 
            cParagraphProperties.numbering = { reference: currentNumIds[level], level: level }; 
          }
          else cParagraphProperties.bullet = { level: 0 };
        }                                     //11: closes the new block to control the closure of bullets and numbered lists.
        else if (tag === "code") {
          delete cRunProperties.style;
        } else if (tag === "tr") {
          inTableRow = false;
          tableHeader = false;
          tmpTable.push(tmpCells);
          tmpCells = []
        } else if (tag === "a") {
          delete cRunProperties.link
        } else if (tag === "td" || tag === "th") {
          tmpCells.push({
            text: cellHasText === true ? tmpCellContent : "",
            width: tmpAttribs.colwidth ? tmpAttribs.colwidth : "250",
            header: tableHeader,
          });

          tmpAttribs = {};
          tmpCellContent = [];
          inTableCell = false;
        } else if (tag === "table") {
          inTable = false;
          let tblRows = [];
          tmpTable.map((row) => {
            let tmpCells = [];
            let isHeader = false
            let widthTotal = row.map(cell => parseInt(cell.width)).reduce((prev, next) => prev + next);

            row.map((cell) => {
              isHeader = cell.header;
              tmpCells.push(new docx.TableCell({
                width: {
                  size: Math.round(parseFloat(cell.width / widthTotal)),
                  type: "pct",
                },
                children: cell.text,
              }))
            });

            tblRows.push(new docx.TableRow({
              children: tmpCells,
              tableHeader: isHeader,
            }))
          });
          // build table and push to paragraphs array
          cParagraph = new docx.Table({
        style: "TableGrid",       //11: I'm using my own table style here
            rows: tblRows,
            width: {
              size: 100,
              type: "pct"
            }
          });

          paragraphs.push(cParagraph);
          cParagraph = null;
          cParagraphProperties = {};
          tmpTable = [];
          tmpCells = [];
        }
      },

      onend() {
        doc.addSection({
          children: paragraphs,
        });
      },
    },
    { decodeEntities: true }
  );

  // For multiline code blocks
  html = html.replace(/\n/g, "<br>");
  parser.write(html);
  parser.end();

  let prepXml = doc.documentWrapper.document.body.prepForXml({});
  let filteredXml = prepXml["w:body"].filter((e) => {
    return Object.keys(e)[0] === "w:p" || Object.keys(e)[0] === "w:tbl";
  });
  let dataXml = xml(filteredXml);
  // dataXml = dataXml.replace(/w:numId w:val="{2-0}"/g, 'w:numId w:val="2"'); // Replace numbering to have correct value
  dataXml = dataXml.replace(/w:numId w:val="{(\d+)-\d+}"/g, 'w:numId w:val="$1"');
  // This is a bit of a sledge hammer fix but it removes the out borders from the html table
  dataXml = dataXml.replace(/<w:tblBorders>(.*?)<\/w:tblBorders>/gm, '<w:tblBorders><w:insideH w:val="single" w:sz="4" w:space="0" w:color="auto"/><w:insideV w:val="single" w:sz="4" w:space="0" w:color="auto"/></w:tblBorders>'); 
  //a little dirty but until we do better it works
  dataXml = dataXml.replace(/\{_\|link\|_\{(.*?)\|\-\|(.*?)\}_\|link\|_\}/gm, '<w:r><w:fldChar w:fldCharType="begin"/></w:r><w:r><w:instrText xml:space="preserve"> HYPERLINK $2 </w:instrText></w:r><w:r><w:fldChar w:fldCharType="separate"/></w:r><w:r><w:rPr><w:rStyle w:val="Hyperlink"/></w:rPr><w:t> $1 </w:t> </w:r><w:r><w:fldChar w:fldCharType="end"/></w:r>'); 
  console.log(dataXml)
  return dataXml;
}

module.exports = html2ooxml;

If I'm going miles off track here, I'd be interested in your ideas. I'm in no way a node.js expert.