使用正規表達式解析 Fortify 產出的 PDF 文件數據

需求

目前工作的開發環境有用 CI/CD,且會執行 Fortify 掃描網站弱點,PM 要手動將 Fortify 產出的 PDF 表格數據整理到 Word 文件,他希望這種重覆性質的工作可以做成自動化,本文主要是記錄如何將 PDF 文件內表格數據解析出來的過程。

概念

  1. 使用 IFilterTextReader (NuGet 套件) 讀取 PDF 文件
  2. 使用正規表達式將表格數據解析成資料集合的物件
  3. 將資料集合填入到 Word 文件 (本文不會實作)

實作

PDF 文件內容

紅框是這次要取出的表格數據

01 PDF 內容

安裝 NuGet 套件

安裝 IFilterTextReader

02 IFilterTextReader

使用 IFilterTextReader 讀取 PDF 文件

1
2
3
4
5
6
7
8
using IFilterTextReader;

// 讀取 PDF
string filePath = @"X:\SomeWhere\Fortify.pdf";
string source = new IFilterTextReader.FilterReader(filePath).ReadToEnd();

// 輸出檢視讀取的文件內容
Console.WriteLine(source);

PDF 讀取出來的內容被轉換成純文字了,這次需要的表格數據部份如下

03 表格數據的字串

使用正規表達式解析內容

將關鍵片段手動排版一下,以便思考該怎麼寫正規表達式的 pattern

1
2
3
4
5
6
7
8
9
10
A1 Injection 0 21 0 1 22 1.2
A2 Broken Authentication and Session Management 0 1 0 0 1 0.2
A3 Cross-Site Scripting (XSS) 0 8 0 0 8 0.2
A4 Insecure Direct Object References 0 40 0 12 52 3.2
A5 Security Misconfiguration 1 0 3 0 4 0.5
A6 Sensitive Data Exposure 3 19 0 0 22 1.8
A7 Missing Function Level Access Control 0 0 0 0 0 0.0
A8 Cross-Site Request Forgery (CSRF) 0 0 0 0 0 0.0
A9 Using Components with Known Vulnerabilities 0 0 0 0 0 0.0
A10 Unvalidated Redirects and Forwards 0 0 0 0 0 0.0

找出文字的固定規律

A3 的標題帶有特殊符號,為相對複雜的情況,故以 A3 為例。

這裡先訂出需要用到的幾個欄位名稱,並針對每個欄位會出現的內容做分析,決定出每個欄位要使用的正規表達式 pattern。

  • IssueName: A3 Cross-Site Scripting (XSS)
    字串開頭固定為 A流水號,內容可能會有多組英、數組成的單字,每個單字間用空白分隔,且可能會有 -, (, ) 等符號
    給正規表達式用的 Pattern: \bA\d+\b [a-zA-Z0-9 \-\(\)]+
  • Critical: 0
    正整數
    給正規表達式用的 Pattern: \b\d+\b
  • High: 8
    正整數
    給正規表達式用的 Pattern: \b\d+\b
  • Medium: 0
    正整數
    給正規表達式用的 Pattern: \b\d+\b
  • Low: 0
    正整數
    給正規表達式用的 Pattern: \b\d+\b
  • TotalIssues: 8
    正整數
    給正規表達式用的 Pattern: \b\d+\b
  • Effort: 0.2
    浮點數,且即使為 0 也會顯示為 0.0
    給正規表達式用的 Pattern: \b\d+\.\d+\b

對正規表達式 pattern 特殊字元不熟的人可以參考本文結尾的參考資料

建立用來存放解析結果的資料類別

1
2
3
4
5
6
7
8
9
10
11
// 用來存放解析結果的資料類別
class DataColumn
{
public string IssueName { get; set; }
public string Critical { get; set; }
public string High { get; set; }
public string Medium { get; set; }
public string Low { get; set; }
public string TotalIssues { get; set; }
public string Effort { get; set; }
}

進行解析並存入結果資料集合物件

用前面準備好的 Pattern 加上正規表達式 群組命名 的寫法,以便後面將取出的資料以 群組名稱 的方式對應到資料物件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
using IFilterTextReader;

// 存放解析結果用的資料集合
List<DataColumn> dataList = new List<DataColumn>();

// 讀取 PDF
string filePath = @"X:\SomeWhere\Fortify.pdf";
string source = new IFilterTextReader.FilterReader(filePath).ReadToEnd();

// 解析用 Pattern
string pattern = @"(?'IssueName'\bA\d+\b [a-zA-Z0-9 \-\(\)]+) (?'Critical'\b\d+\b) (?'High'\b\d+\b) (?'Medium'\b\d+\b) (?'Low'\b\d+\b) (?'TotalIssues'\b\d+\b) (?'Effort'\b\d+\.\d+\b)";

// 解析成集合物件
MatchCollection matches = Regex.Matches(source, pattern);
if (matches.Count > 0)
{
// 從解析集合物件逐筆填入解析結果到資料集合
foreach (Match match in matches)
{
dataList.Add(
new DataColumn
{
IssueName = match.Groups["IssueName"].ToString(),
Critical = match.Groups["Critical"].ToString(),
High = match.Groups["High"].ToString(),
Medium = match.Groups["Medium"].ToString(),
Low = match.Groups["Low"].ToString(),
TotalIssues = match.Groups["TotalIssues"].ToString(),
Effort = match.Groups["Effort"].ToString(),
}
);
}

// 顯示解析結果的資料集合
dataList.Dump(); // (此為 LINQPad 的方法,輸出結果如下圖)
}
Console.WriteLine("執行結束");

用 LINQPad 檢視解析結果資料集合

04 解析出來的資料集合

感謝正規表達式讓我們快速完成需求,收工!

參考資料