Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/pr-quality-gate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ permissions:
contents: read

env:
QUALITY_GATE_VERSION: v2.0.5
QUALITY_GATE_VERSION: v2.0.6

jobs:
incremental_check:
Expand Down
10 changes: 7 additions & 3 deletions README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@ the commits, files, and changed lines introduced by a pull request.

[中文文档](README.md)

This document describes `v2.0.5`. Release tags remain fixed; `stable` is the rolling upgrade entry point.
This document describes `v2.0.6`. Release tags remain fixed; `stable` is the rolling upgrade entry point.

v2.0.6 narrowly downgrades the reproduced Semgrep C++ `single name expected for simple var`
compatibility warning to explicit incomplete coverage. Files and other findings are not skipped.
Unknown parsing errors, tool failures, and missing reports still fail the gate.

## Quick start

Expand All @@ -16,13 +20,13 @@ This document describes `v2.0.5`. Release tags remain fixed; `stable` is the rol
3. Replace `QUALITY_GATE_REF` with a reviewed release tag or full Commit SHA.

The following example uses the current stable release
[`v2.0.5`](https://github.com/HYGON-AI/quality-gate/releases/tag/v2.0.5):
[`v2.0.6`](https://github.com/HYGON-AI/quality-gate/releases/tag/v2.0.6):

```yaml
jobs:
checks:
name: Checks
uses: HYGON-AI/quality-gate/.github/workflows/pr-quality-gate.yml@v2.0.5
uses: HYGON-AI/quality-gate/.github/workflows/pr-quality-gate.yml@v2.0.6
permissions:
contents: read
```
Expand Down
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ HYGON Quality Gate 是面向 Pull Request(PR)的增量质量、安全与开

[English documentation](README.en.md)

本文描述 `v2.0.5`。固定版本标签保持不变,`stable` 是集中升级的滚动入口。
本文描述 `v2.0.6`。固定版本标签保持不变,`stable` 是集中升级的滚动入口。

v2.0.6 精确降级已复现的 Semgrep C++ `single name expected for simple var` 兼容性告警,
明确显示“扫描覆盖不完整”,不跳过文件、不删除其他安全发现。未知解析异常、工具故障和缺失报告仍阻断。

## 快速接入

Expand All @@ -14,13 +17,13 @@ HYGON Quality Gate 是面向 Pull Request(PR)的增量质量、安全与开
2. 根据目标仓库实际情况调整 `pull_request.branches`。
3. 将 `QUALITY_GATE_REF` 替换为已审核的发布 Tag 或完整 Commit SHA。

以下示例使用固定版本 [`v2.0.5`](https://github.com/HYGON-AI/quality-gate/releases/tag/v2.0.5):
以下示例使用固定版本 [`v2.0.6`](https://github.com/HYGON-AI/quality-gate/releases/tag/v2.0.6):

```yaml
jobs:
checks:
name: Checks
uses: HYGON-AI/quality-gate/.github/workflows/pr-quality-gate.yml@v2.0.5
uses: HYGON-AI/quality-gate/.github/workflows/pr-quality-gate.yml@v2.0.6
permissions:
contents: read
```
Expand Down
29 changes: 29 additions & 0 deletions docs/plans/2026-09-21-semgrep-coverage-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Narrow C++ parser compatibility exception

User approved a trial implementation, not a release or stable update.
Keep the tested production image. Do not skip files or suppress findings.
Enable only the reproduced C++ warning fingerprint through the central policy:
code 2, level warn, type Other syntax error, exact simple-var message with
matching source path and positive line number. Unknown/malformed errors,
missing reports and process failures remain invalid scans.

Return known compatibility warnings as advisory findings with locations and
an explicit partial scanner state. Do not drop coverage warnings during changed
line filtering. Unknown coverage failures take precedence; real blockers retain
their level. Summaries show partial coverage prominently even alongside blockers.

Validation: known fingerprint and each near miss; mixed known/unknown errors;
mixed real blocker and compatibility warning; missing/malformed report and
process failure; partial summary; production-image replay of pinned PR source.
No change to tag, stable, release, repository workflow or runner configuration.

## Trial validation

- 13 focused parser/executor/summary unit tests passed locally.
- Linux full unit regression: 139 tests passed.
- Production pinned Semgrep image replayed the original topology.cpp through the
patched executor: partial state, one located advisory at line 1136, zero blockers.
Rendered heading explicitly states incomplete coverage, not full scan success.
- Candidate Semgrep 1.177.0 was not adopted: it reproduces the same parser issue.
- This was an isolated executor replay, not a rerun of the GitHub PR's four Jobs.
Production remains unchanged until a separately approved rollout.
70 changes: 70 additions & 0 deletions docs/plans/2026-09-21-semgrep-cpp-reproduction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# rocSHMEM C++ parser incident verification

## Candidate 1.177.0 validation (2026-09-21)

Official latest release queried from semgrep/semgrep; candidate image pulled
without changing the production policy:
`semgrep/semgrep@sha256:acaac22ffc7b7cc5926de0751b223bce0b2491c33d18422fa72f632c78d81198`.

- Original pinned PR file: same line 1136 `single name expected for simple var`
warning, exit 0, zero findings. The initial expect-fixed assertion failed.
- Ordinary-loop substitution in full file: no errors, zero findings.
- Minimal structured binding and ordinary loop: C++17 compiler and scanner pass.
- Explicit C++ system(input) control: compiles, scanner exits 1, matches
hygon.c.system-call; no parse errors. Detection is not globally disabled.
- Four report-parser unit tests rerun and passed.

Conclusion: upgrading to this candidate does not resolve the incident. Do not
change the production image solely for this failure. Next design should narrowly
distinguish confirmed parser compatibility warnings from operational failure,
retain findings and visible incomplete-coverage information, and avoid globally
turning all parse errors into passing scans. No production behavior changed.
Candidate containers used offline execution with version checks disabled;
only the public pinned source download and image pull required network access.

- Incident: HYGON-AI/rocSHMEM-das run 35566088179, job 106228080177.
- Source pinned to a296ec7dad6aefa0103837eff1df98219ac759ad, src/gda/topology.cpp.
- Gate v2.0.5, production Semgrep 1.125.0 (digest in tests/semgrep_cpp_probe.py).
- Reproduced on quality-nmz1 using temporary fixtures, offline scanner containers,
no execution of PR code, no production runner or policy changes.

## Observed comparisons

1. Minimal map/vector range-for with a C++17 structured binding: g++ with
`-std=c++17 -pedantic-errors -fsyntax-only` passed; Semgrep also passed.
2. Pinned original PR file: Semgrep exit 0, zero findings, one `Other syntax error`
(code 2, level warn), line 1136: `single name expected for simple var`.
3. Identical file, replacing only the structured-binding range-for with an entry
variable and references to `.first` / `.second`: Semgrep exit 0, no errors,
zero findings. This is a diagnostic control, not a proposed business-code edit.
4. Minimal ordinary range-for: compiler and scanner both passed.
5. Malformed range-for missing a closing parenthesis: compiler rejected it,
but Semgrep returned exit 0, no errors and no findings. The probe now records
this existing limitation rather than assuming Semgrep validates C++ syntax.

Conclusion: the failure depends on the structured binding in this file's context;
it is not general lack of support for C++17 structured bindings. The full project
was not compiled by this probe. No claim that all project code is compiler-valid.
Semgrep success also cannot replace the repository's compiler/build checks.

The gate currently converts the coverage warning to a failed scan. Its policy
has not been changed. The probe and report-parser tests characterize the failure,
not a completed fix. Upgrading the scanner still requires separate validation.

## Reproduce

Validation: 130 unit tests passed on Linux/Python 3.9, including the four new
report-parser cases. Windows/Python 3.13 had four platform-related errors
(three os.getuid, one default-encoding read); Linux is the supported gate runtime.
Probe assumptions were corrected from real observations: minimal structured
bindings parse successfully, and the malformed sample is tolerated by Semgrep.

```sh
python3.9 tests/semgrep_cpp_probe.py --gate-root "$PWD"
python3.9 tests/semgrep_cpp_probe.py --gate-root "$PWD" --incident
PYTHONPATH=src python3.9 -m unittest discover -s tests -p test_semgrep_cpp_coverage.py -v
```

The incident mode downloads only the pinned public source; containers have no
network. The fixed-image incident assertion is intentionally a characterization
of the existing bug and must be revised when validating a corrected image.
11 changes: 11 additions & 0 deletions docs/releases/v2.0.6.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# v2.0.6 — C++ 解析兼容性告警精确降级

- 修复 rocSHMEM-das PR #33 因 Semgrep 已知 C++ 解析兼容性告警导致门禁阻断。
- 仅匹配已验证的错误类型、code 2、warn 级别、C++ 扩展名、匹配路径及精确 simple-var 文案;显示原始位置。
- 明确展示“覆盖不完整”,不宣称未解析部分已获安全验证,不跳过整个文件或删除其他安全发现。
- 未知解析异常、进程故障、缺失/损坏报告继续阻断;真实阻断项保持原级别。
- 未升级 Semgrep 镜像:候选 1.177.0 仍复现同一错误。
- 保留现有四个 Job 和调用接口;已有固定版本标签不变。

验证:Linux 139 项单元测试通过(含 13 项专项测试);生产镜像对原文件的候选执行器重放得到 0 阻断、1 项带位置的兼容性提示。
线上候选与发布后验证记录在完成后补充;本地重放不代表已完成 GitHub PR 验证。
1 change: 1 addition & 0 deletions policies/quality-security/hygon-quality-security-v1.1.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ scanners:
- '-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----'
semgrep:
enabled: true
cpp_simple_var_compatibility_advisory: true
block_rule_ids: []
advisory_rule_ids:
- hygon.python.tempfile-mktemp
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "hygon-quality-gate"
version = "2.0.5"
version = "2.0.6"
description = "Incremental quality, security, and open-source compliance gate for pull requests"
readme = "README.md"
license = { file = "LICENSE" }
Expand Down
11 changes: 10 additions & 1 deletion src/hygon_pr_gate/local_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,10 @@ def _semgrep(
findings, coverage = parse_semgrep(
report, self.quality["scanners"]["semgrep"]
)
findings = _filter_changed_lines(findings, scope["changed_lines"])
compatibility = [item for item in findings
if item['rule_id'] == 'SAST.SEMGREP.CPP_PARSER_COMPATIBILITY']
findings = compatibility + _filter_changed_lines(
[item for item in findings if item not in compatibility], scope['changed_lines'])
advisory_rules = {
str(value).upper()
for value in self.policy.get("advisory", {}).get("semgrep_rule_ids", [])
Expand All @@ -176,6 +179,12 @@ def _semgrep(
"semgrep", "failed", finding_count=len(findings),
detail="Semgrep 未完成扫描:" + "; ".join(coverage[:3]),
)
if compatibility:
return findings, scanner_status(
'semgrep', 'partial', finding_count=len(findings),
detail='扫描覆盖不完整:{} 项已确认的 C++ 解析兼容性提示;未跳过文件,其他发现仍保留。'.format(len(compatibility)),
image=str(self.images['semgrep']),
)
return findings, self._status(
"semgrep", findings, str(self.images["semgrep"]),
"本地规则、无网络;覆盖异常 {} 个".format(len(coverage)),
Expand Down
7 changes: 7 additions & 0 deletions src/hygon_pr_gate/render_summary.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,14 @@ def render_summary(data: Dict[str, Any], output: Path) -> None:
findings = data.get("findings", [])
blockers = [item for item in findings if item.get("level") == "blocker"]
advisories = [item for item in findings if item.get("level") != "blocker"]
coverage_partial = any(item.get('state') == 'partial' for item in data.get('statuses', []))
status_icon = {
"passed": "Passed / 通过",
"findings": "Findings / 有发现",
"failed": "Failed / 执行失败",
"disabled": "Skipped / 未执行",
"not-applicable": "无需检查",
"partial": "Partial / 覆盖不完整(兼容性提示)",
}
display_name = _escape(data.get("display_name") or "Unknown Check")
display_name_zh = _escape(data.get("display_name_zh") or "未知检查")
Expand All @@ -46,6 +48,7 @@ def render_summary(data: Dict[str, Any], output: Path) -> None:
else "⚠️ Invalid Scan / 扫描无效" if data.get('operational_error')
else "❌ Blocked / 本检查阻断" if blockers
else "内置预检通过,完整门禁未执行" if data.get('partial')
else "⚠️ 本检查无阻断项;扫描覆盖不完整(已知兼容性提示)" if coverage_partial
else "✅ Passed / 本检查通过"),
"",
"- Blockers / 阻断问题:{}".format(len(blockers)),
Expand Down Expand Up @@ -80,6 +83,8 @@ def render_summary(data: Dict[str, Any], output: Path) -> None:
)
)
lines.extend(['', '</details>'])
if coverage_partial:
lines.extend(['', '⚠️ 已知解析兼容性限制:部分内容未获安全验证;其他检查和发现仍有效。'])
if blockers:
lines.extend(["", "## Required Changes / 必须修改", ""])
locations = {}
Expand All @@ -99,6 +104,8 @@ def render_summary(data: Dict[str, Any], output: Path) -> None:
lines.append('- 修复:{}'.format(_escape(remedy)))
lines.append("")
if advisories:
if data.get('operational_error'):
lines.extend(['', '注意:以下提示不是失败原因;本次检查因扫描执行异常失败,原因见“扫描无效”。'])
lines.extend(["", "## Advisories / 提示项(不阻断)", ""])
lines.extend(['<details><summary>展开提示详情(不阻断合并)</summary>', ''])
for item in advisories:
Expand Down
2 changes: 1 addition & 1 deletion src/hygon_quality_security/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ def scanner_status(
image: str = "",
finding_count: int = 0,
) -> Dict[str, Any]:
if state not in {"passed", "findings", "disabled", "failed", "not-applicable"}:
if state not in {"passed", "findings", "disabled", "failed", "not-applicable", "partial"}:
raise ValueError("invalid scanner state: {}".format(state))
return {
"scanner": scanner,
Expand Down
35 changes: 32 additions & 3 deletions src/hygon_quality_security/scanner_parsers.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,11 +90,27 @@ def parse_gitleaks(
return findings, summary


def _known_cpp_simple_var_warning(error: Dict[str, Any]) -> Optional[int]:
"""Match only the production-reproduced C++ warning, never generic parse errors."""
path = error.get('path')
if (not isinstance(path, str) or Path(path).suffix.lower() not in
{'.cpp', '.cc', '.cxx', '.hpp', '.hh', '.hxx'} or
type(error.get('code')) is not int or error['code'] != 2 or
error.get('level') != 'warn' or error.get('type') != 'Other syntax error'):
return None
match = re.fullmatch(
r'Other syntax error at line ' + re.escape(path) +
r':([1-9][0-9]*):\s+single name expected for simple var\s*',
str(error.get('message') or ''),
)
return int(match.group(1)) if match else None


def parse_semgrep(
path: Path, policy: Optional[Dict[str, Any]] = None
) -> Tuple[List[Dict[str, Any]], List[str]]:
document = _load_json(path)
if not isinstance(document, dict) or not isinstance(document.get("results", []), list):
if not isinstance(document, dict) or not isinstance(document.get("results"), list):
raise ValueError("semgrep report has no results list")
policy = policy or {}
block_rule_ids = {
Expand All @@ -112,11 +128,24 @@ def parse_semgrep(
}
findings = []
coverage_errors = []
for error in document.get("errors") or []:
errors = document.get('errors', [])
if not isinstance(errors, list):
raise ValueError('semgrep errors must be a list')
for error in errors:
if not isinstance(error, dict):
continue
raise ValueError('semgrep error must be an object')
message = str(error.get("message") or error.get("type") or "Semgrep error")
path_value = _relative(str(error.get("path") or ""))
known_line = (_known_cpp_simple_var_warning(error)
if policy.get('cpp_simple_var_compatibility_advisory') is True else None)
if known_line is not None:
findings.append(finding(
'SAST.SEMGREP.CPP_PARSER_COMPATIBILITY', 'semgrep', path_value,
'Semgrep C++ 解析兼容性提示:扫描覆盖不完整', message,
'此提示不阻断;未解析部分未获安全验证,须保留项目编译和测试。其他安全发现仍有效。',
level='advisory', line=known_line,
))
continue
if path_value:
coverage_errors.append("{}: {}".format(path_value, message))
findings.append(
Expand Down
42 changes: 42 additions & 0 deletions tests/semgrep_cpp_gate_replay.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Copyright (c) 2026 Hygon Information Technology Co., Ltd.
# SPDX-License-Identifier: Apache-2.0
"""Run patched Semgrep executor on pinned incident source, without executing it."""
import json
from pathlib import Path
import tempfile
import urllib.request

from hygon_pr_gate.local_executor import LocalDockerExecutor
from hygon_pr_gate.policy import load_policy
from hygon_pr_gate.render_summary import render_summary


def main():
root = Path(__file__).resolve().parents[1]
url = 'https://raw.githubusercontent.com/HYGON-AI/rocSHMEM-das/a296ec7dad6aefa0103837eff1df98219ac759ad/src/gda/topology.cpp'
source = urllib.request.urlopen(url, timeout=30).read().decode('utf-8')
policy = load_policy(root / 'policies', 'HYGON-AI/rocSHMEM-das')
with tempfile.TemporaryDirectory(prefix='gate-cpp-replay-') as directory:
repo = Path(directory)
path = 'src/gda/topology.cpp'
(repo / path).parent.mkdir(parents=True)
(repo / path).write_text(source, encoding='utf-8')
reports = repo / 'reports'
reports.mkdir()
executor = LocalDockerExecutor(policy, root / 'policies')
executor.docker_cpus = '2'
executor.docker_memory = '4g'
findings, status = executor._semgrep(repo, {'changed_lines': {path: {1}}}, reports, [path])
assert status['state'] == 'partial', status
assert any(item['rule_id'] == 'SAST.SEMGREP.CPP_PARSER_COMPATIBILITY' and item['line'] == 1136 for item in findings)
assert not any(item['level'] == 'blocker' for item in findings)
summary = repo / 'summary.md'
render_summary(dict(repository='HYGON-AI/rocSHMEM-das', display_name='Secrets & SAST',
statuses=[status], findings=findings, gate_version='local candidate (unreleased)',
scope=dict(merge_base='unknown', head='a296ec7dad6aefa0103837eff1df98219ac759ad', changes=[path], commits=[])), summary)
print(json.dumps({'status': status, 'findings': findings}, ensure_ascii=False), flush=True)
print(summary.read_text(encoding='utf-8'), flush=True)


if __name__ == '__main__':
main()
Loading
Loading