name: NYX Security Scan description: Runs NYX SAST scanner and posts findings as PR comment inputs: forgejo_push_token: description: Token with write:issue scope required: true repository: description: Repository in owner/name format required: true pr_number: description: PR number to comment on required: true sha: description: Commit SHA to scan required: true fail_on: description: Severity threshold (LOW, MEDIUM, HIGH, CRITICAL) required: false default: HIGH runs: using: composite steps: - name: Clone nyx from Forgejo mirror shell: bash run: | git clone --depth=1 --branch v0.7.0 \ "https://oauth2:${{ inputs.forgejo_push_token }}@bitfreedom.net/code/apunkt/nyx.git" \ .nyx-src - name: Install Rust shell: bash run: | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable echo "$HOME/.cargo/bin" >> $GITHUB_PATH - name: Build nyx from source shell: bash run: | cd .nyx-src cargo build --release - name: Run NYX scan id: nyx shell: bash run: | .nyx-src/target/release/nyx scan --format json > nyx-results-raw.json 2>&1 # Apply suppression rules from triage.json python3 -c " import json with open('nyx-results-raw.json') as f: findings = json.load(f) if isinstance(findings, dict): findings = findings.get('findings', []) # Load suppression rules try: with open('.nyx/triage.json') as f: triage = json.load(f) rules = triage.get('suppression_rules', []) except: rules = [] def is_suppressed(f): rule_id = f.get('id', '') for r in rules: by = r.get('by', '') value = r.get('value', '') if by == 'rule' and rule_id == value: return True if by == 'file' and f.get('path', '').endswith(value): return True if by == 'rule_in_file': parts = value.split(':', 1) if len(parts) == 2 and rule_id == parts[0] and f.get('path','').endswith(parts[1]): return True return False filtered = [f for f in findings if not is_suppressed(f)] print(f'Suppressed {len(findings) - len(filtered)} of {len(findings)} findings', flush=True) # Convert to minimal SARIF results = [] for f in filtered: results.append({ 'level': 'error' if f.get('severity','').lower() in ['high','critical'] else 'warning', 'message': {'text': f.get('message','')}, 'ruleId': f.get('id',''), 'locations': [{'physicalLocation': { 'artifactLocation': {'uri': f.get('path','').replace('/workspace/nomyo-ai/nomyo-router/','')}, 'region': {'startLine': f.get('line',0), 'startColumn': f.get('col',0)} }}] }) sarif = { 'version': '2.1.0', '\$schema': 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json', 'runs': [{'results': results, 'tool': {'driver': {'name': 'nyx', 'version': '0.7.0', 'rules': []}}}] } with open('nyx-results.sarif', 'w') as f: json.dump(sarif, f, indent=2) # Fail if any HIGH/CRITICAL remain high = [f for f in filtered if f.get('severity','').lower() in ['${{ inputs.fail_on }}'.lower(), 'critical']] exit(1 if high else 0) " continue-on-error: true - name: Post findings as PR comment if: steps.nyx.outcome == 'failure' shell: bash run: | FINDINGS=$(python3 -c " import json, sys with open('nyx-results.sarif') as f: data = json.load(f) results = data.get('runs', [{}])[0].get('results', []) lines = [f'## 🔴 NYX found {len(results)} issue(s)\n'] for r in results: level = r.get('level', '?') msg = r.get('message', {}).get('text', '?') rule = r.get('ruleId', '?') loc = r.get('locations', [{}])[0].get('physicalLocation', {}) path = loc.get('artifactLocation', {}).get('uri', '?') line = loc.get('region', {}).get('startLine', '?') col = loc.get('region', {}).get('startColumn', '?') lines.append(f'- **{level.upper()}** \`{path}:{line}:{col}\` [{rule}] — {msg}') print('\n'.join(lines)) ") curl -sf -X POST \ -H "Authorization: token ${{ inputs.forgejo_push_token }}" \ -H "Content-Type: application/json" \ "https://bitfreedom.net/code/api/v1/repos/${{ inputs.repository }}/issues/${{ inputs.pr_number }}/comments" \ -d "{\"body\": $(python3 -c 'import json,sys; print(json.dumps(sys.stdin.read()))' <<< "$FINDINGS")}" - name: Fail if findings found if: steps.nyx.outcome == 'failure' shell: bash run: exit 1