使用Helix QAC 进行静态代码扫描

使用Helix QAC 进行静态代码扫描

Helix QAC 是一款由 Perforce 开发的静态代码分析工具,专为 C 和 C++ 语言设计。它致力于在软件开发早期阶段,帮助开发者识别并修复代码中的潜在缺陷、安全漏洞和编码标准违规。该工具的最大亮点在于其深度语义分析能力,能够理解代码的执行流程和数据流,从而发现深层次、难以及时捕捉的问题,例如内存泄漏和空指针引用。Helix QAC 支持多种业界主流的编码标准,包括 MISRA C/C++、CERT C/C++ 等,这对于需要满足严格行业规范(如汽车、航空航天)的项目至关重要。它以高准确性和低误报率著称,能有效减少开发者处理无效警告的时间。此外,Helix QAC 提供详细的报告和度量指标,可无缝集成到主流 IDE 中,并支持对大型代码库进行高效分析。它不仅能提高代码的可靠性、安全性和可维护性,还能有效降低开发成本和风险,是关键性嵌入式系统开发领域的理想选择。

这里作者使用的是2023.03版本,使用前确保QAC的插件正常

QAC做代码分析需要有对应的编译环境,这里准备一个包含一些CWE的C语言代码以及对应的Makefile

测试源代码如下:

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

// CWE-121: Stack-based Buffer Overflow
void stack_overflow_example() {
char buffer[10];
printf("Enter a string to copy (may cause stack overflow): ");
scanf("%s", buffer); // 不安全,没有限制输入长度
printf("You entered: %s\n", buffer);
}

// CWE-119: Improper Restriction of Operations within the Bounds of a Memory Buffer
void buffer_overread_example() {
char str[] = "short";
char c = str[10]; // 越界读取
printf("Character at index 10: %c\n", c); // 未定义行为
}

// CWE-476: NULL Pointer Dereference
void null_pointer_example() {
char *ptr = NULL;
printf("Trying to dereference NULL pointer: %c\n", *ptr); // Crashes
}

// CWE-467: Use of sizeof() on a Pointer Type (instead of what it points to)
void wrong_sizeof_example() {
char *buffer = malloc(10);
char arr[10];

// ⚠️ 漏洞:对指针类型使用sizeof可能导致误判缓冲区大小
printf("Size of buffer (pointer): %zu\n", sizeof(buffer)); // 返回指针大小(通常是8)
printf("Size of arr (array): %zu\n", sizeof(arr)); // 返回数组大小(10)

free(buffer);
}

// CWE-478: Missing Default Case in Switch
void missing_default_case(int choice) {
switch(choice) {
case 1:
printf("Choice 1\n");
break;
case 2:
printf("Choice 2\n");
break;
// 没有 default 处理非法选择
}
}

int main(int argc, char *argv[]) {
if (argc < 2) {
printf("Usage: %s <option>\n", argv[0]);
printf("Options:\n");
printf(" 1 - Stack Overflow\n");
printf(" 2 - Buffer Overread\n");
printf(" 3 - NULL Pointer\n");
printf(" 4 - Wrong sizeof()\n");
printf(" 5 - Missing Default Case\n");
return 1;
}

int option = atoi(argv[1]);

switch(option) {
case 1: stack_overflow_example(); break;
case 2: buffer_overread_example(); break;
case 3: null_pointer_example(); break;
case 4: wrong_sizeof_example(); break;
case 5: missing_default_case(3); break;
}

return 0;
}

Makefile如下:

1
2
3
4
5
6
7
8
# 默认目标
all:
gcc -Wall -Wextra -g -o demo demo.c

# 清理目标,删除生成的exe文件
clean:
del /Q demo.exe

将对应文件命名好后放到同一个文件夹内

在QAC中新建一个项目

在这里配置项目的地址

点击Next

建议选择与实际编译器匹配或接近的 cct,没有匹配的编译器时选择Helix_Generic_C或Helix_Generic_C++

在这里可以看到项目已经构建好了

现在开始添加代码到QAC工程

配置好相关信息

正常来说这样同步完成,但是由于作者本地环境问题,有时候同步会失败,可能与系统环境变量有关,这里作者使用命令行的模式进行了代码同步

通过命令行导入源代码

1
qacli sync -P D:\桌面\test1 -t INJECT -g -- cmd /c "cd /d D:\桌面\demo1 && make"

随后在可视化界面里可以选择扫描的规则

点开小齿轮后,我们可以选择对应的规则,因为从我们要扫描CWE,所以要添加CWECCM进去

这里可以看到具体的规则信息

完成后进行分析,可以直接用可视化界面进行分析

作者这里使用的是命令行

1
qacli analyze -P D:\桌面\test1 -f

重新进入可视化界面,发现已经完成了扫描,相应的规则触发信息已经显示出来了

这里我们导出报告看一下

一共有七种报告供我们选择,这里我们选择SCR报告

生成后默认在项目文件夹下的configs/Initial/reports

打开报告看一下

可以看到代码触发的CWE规则

如果想根据QAC扫描结果重新定位到每一行代码具体触发了什么规则,作者这里提供两种方式,第一种是直接通过告警界面查看

第二种是进行导出处理

首先在powershell中运行下面命令,路径替换成自己的即可,可以得到XML文件

1
qacli view -P "D:/桌面/test1" -t DIAGLST -m XML --xml-format "*" -o "D:/桌面/msg_test1"

在产生的XML文件夹中运行Dataprocessing.py即可,可以将每一个源文件产生的信息生成到一个CSV文件中保存到report文件夹中,届时按照所需的文件分析找到对应分析结果CSV即可

Dataprocessing.py内容如下:

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
import xml.etree.ElementTree as ET
import csv
import os
import traceback

def format_helix_qac_xml_to_csv(xml_file_path, csv_file_path):
"""
将Helix QAC的XML输出文件格式化为CSV文件,每条诊断(包括子诊断)占据一行。

Args:
xml_file_path (str): 输入的XML文件路径。
csv_file_path (str): 输出的CSV文件路径。
Returns:
bool: True 表示处理成功, False 表示处理失败。
"""
# 注意:此函数内的打印信息会被主循环的打印信息所补充
# print(f"\n--- 正在处理文件 (函数内): {xml_file_path} ---")
try:
tree = ET.parse(xml_file_path)
root = tree.getroot()
except FileNotFoundError:
print(f"错误: XML文件 '{xml_file_path}' 未找到。请确保文件存在于指定的路径。")
return False
except ET.ParseError as e:
print(f"错误: 解析XML文件 '{xml_file_path}' 时发生错误: {e}")
return False
except Exception as e:
print(f"读取或解析XML文件 '{xml_file_path}' 时发生未知错误: {e}")
traceback.print_exc()
return False

csv_columns = [
"Diag_ID", "Diag_Type", "Primary_File_Path", "Primary_Line", "Primary_Column",
"BaseName", "FileName", "FilePath", "Line", "Column",
"MsgText", "RuleId", "RuleNum", "MsgNum", "Severity",
"HelpPath", "RuleCategories", "RuleGroupName", "Producer"
]

extracted_data = []

for file_node in root.findall(".//File"):
primary_file_path = file_node.findtext("Name", default="N/A") # 使用 findtext 简化

for diag_node in file_node.findall(".//Diag"): # 使用 .//Diag 保留原逻辑的灵活性
diag_id = diag_node.get("id")

main_diag_row = {
"Diag_ID": f"Main-{diag_id}",
"Diag_Type": "Main Diag",
"Primary_File_Path": primary_file_path,
"Primary_Line": diag_node.findtext("Line", default="N/A"),
"Primary_Column": diag_node.findtext("Column", default="N/A"),
"BaseName": diag_node.findtext("BaseName", default="N/A"),
"FileName": diag_node.findtext("FileName", default="N/A"),
"FilePath": diag_node.findtext("FilePath", default="N/A"),
"Line": diag_node.findtext("Line", default="N/A"),
"Column": diag_node.findtext("Column", default="N/A"),
"MsgText": diag_node.findtext("MsgText", default="N/A").strip(),
"RuleId": diag_node.findtext("RuleId", default="N/A"),
"RuleNum": diag_node.findtext("RuleNum", default="N/A"),
"MsgNum": diag_node.findtext("MsgNum", default="N/A"),
"Severity": diag_node.findtext("Severity", default="N/A"),
"HelpPath": diag_node.findtext("HelpPath", default="N/A"),
"RuleCategories": diag_node.findtext("RuleCategories", default="N/A"),
"RuleGroupName": diag_node.findtext("RuleGroupName", default="N/A"),
"Producer": diag_node.findtext("Producer", default="N/A")
}
extracted_data.append(main_diag_row)

for sub_diag_node in diag_node.findall(".//SubDiag"): # 使用 .//SubDiag 保留原逻辑
sub_diag_id = sub_diag_node.get("id")

sub_diag_row = {
"Diag_ID": f"Sub-{diag_id}-{sub_diag_id}",
"Diag_Type": "Sub Diag",
"Primary_File_Path": primary_file_path,
"Primary_Line": diag_node.findtext("Line", default="N/A"),
"Primary_Column": diag_node.findtext("Column", default="N/A"),
"BaseName": sub_diag_node.findtext("BaseName", default="N/A"),
"FileName": sub_diag_node.findtext("FileName", default="N/A"),
"FilePath": sub_diag_node.findtext("FilePath", default="N/A"),
"Line": sub_diag_node.findtext("Line", default="N/A"),
"Column": sub_diag_node.findtext("Column", default="N/A"),
"MsgText": sub_diag_node.findtext("MsgText", default="N/A").strip(),
"RuleId": sub_diag_node.findtext("RuleId", default="N/A"),
"RuleNum": sub_diag_node.findtext("RuleNum", default="N/A"),
"MsgNum": sub_diag_node.findtext("MsgNum", default="N/A"),
"Severity": sub_diag_node.findtext("Severity", default="N/A"),
"HelpPath": sub_diag_node.findtext("HelpPath", default="N/A"),
"RuleCategories": sub_diag_node.findtext("RuleCategories", default="N/A"),
"RuleGroupName": sub_diag_node.findtext("RuleGroupName", default="N/A"),
"Producer": sub_diag_node.findtext("Producer", default="N/A")
}
extracted_data.append(sub_diag_row)

if extracted_data:
try:
# 确保输出目录存在 (虽然主脚本会创建,但函数内多一层检查更安全,尤其当函数被独立调用时)
os.makedirs(os.path.dirname(csv_file_path), exist_ok=True)
with open(csv_file_path, 'w', newline='', encoding='utf-8') as csvfile:
writer = csv.DictWriter(csvfile, fieldnames=csv_columns)
writer.writeheader()
writer.writerows(extracted_data)
print(f"成功将格式化数据写入到 '{csv_file_path}'")
return True
except IOError as e:
print(f"写入CSV文件 '{csv_file_path}' 时发生IO错误: {e}")
return False
else:
print(f"文件 '{xml_file_path}' 中没有可写入CSV的诊断数据。")
# 考虑一个没有数据的XML文件是否算作“成功”处理。
# 如果空数据也算成功转换(即文件被正确读取但无内容),则返回True。
# 如果这意味着某种形式的“失败”或“警告”,则可能返回False或不同状态。
# 当前根据原代码逻辑,视为空文件处理成功。
return True

# --- 主执行部分 ---
if __name__ == "__main__":
# 获取当前脚本所在的目录 (更健壮的方式)
# 如果你的脚本总是从特定工作目录运行,也可以用 os.getcwd()
try:
script_directory = os.path.dirname(os.path.abspath(__file__))
except NameError: # 处理在某些解释器环境(如直接执行选中代码)中 __file__ 未定义的情况
script_directory = os.getcwd()

# 定义报告文件夹的路径
report_directory_name = "report"
report_directory = os.path.join(script_directory, report_directory_name)

# 创建 report 文件夹 (如果它还不存在)
if not os.path.exists(report_directory):
try:
os.makedirs(report_directory)
print(f"已创建文件夹: '{report_directory}'")
except OSError as e:
print(f"错误: 创建文件夹 '{report_directory}' 失败: {e}")
exit() # 如果无法创建报告文件夹,则终止脚本

print(f"将在 '{script_directory}' (及其子文件夹, 不包括 '{report_directory_name}') 中查找XML文件...")
print(f"转换后的CSV文件将保存在 '{report_directory}' 中。")

xml_files_to_process = []
# 递归遍历目录和子目录
for root, dirs, files in os.walk(script_directory):
# 从遍历中排除 'report' 文件夹本身以及其他常见的非源码文件夹
# dirs[:] = [...] 会修改 os.walk 的后续遍历目标
dirs[:] = [d for d in dirs if d not in [report_directory_name, '.git', '.vscode', '.idea', '__pycache__', 'venv', '.venv']]

for filename in files:
if filename.endswith(".xml"):
xml_files_to_process.append(os.path.join(root, filename))

if not xml_files_to_process:
print(f"在 '{script_directory}' (及其子文件夹, 不包括 '{report_directory_name}') 下未找到任何 .xml 文件。")
else:
print(f"找到 {len(xml_files_to_process)} 个XML文件进行处理。")
success_count = 0
failure_count = 0

for xml_file_path in xml_files_to_process:
print(f"\n--- 开始处理文件: {xml_file_path} ---")

# 获取XML文件的基本名称 (不含扩展名)
base_name = os.path.splitext(os.path.basename(xml_file_path))[0]

# 构造输出CSV文件的完整路径,存放在 report 文件夹中
# 注意:如果不同子目录中有同名XML文件,它们会生成同名CSV文件并可能导致覆盖。
# 例如: subdir1/myreport.xml 和 subdir2/myreport.xml 都会变成 report/myreport.csv
# 如果需要避免这种情况,文件名构造逻辑需要更复杂,例如包含部分相对路径。
output_csv_file = os.path.join(report_directory, f"{base_name}.csv")

if format_helix_qac_xml_to_csv(xml_file_path, output_csv_file):
success_count += 1
else:
failure_count += 1

print("\n--- 所有XML文件处理完毕 ---")
print(f"总计处理文件数: {len(xml_files_to_process)}")
print(f"✅ 成功转换: {success_count} 个文件")
if failure_count > 0:
print(f"❌ 转换失败: {failure_count} 个文件")

接下来可以打开report文件夹,即可看到一个CSV文件

打开后就是具体的触发规则信息,可以根据这个表格重新定位到哪一行代码触发了什么规则

作者这里先进行了简单使用参考,更复杂的作者也在学习摸索中。


使用Helix QAC 进行静态代码扫描
https://erkangkang.github.io/2025/08/04/QAC使用/
作者
尔康康康康
发布于
2025年8月4日
许可协议