Skip to content

Commit 0e0afcd

Browse files
committed
feat: initial implementation of CERTInext AnyCA REST Gateway plugin
Complete plugin scaffold including: - IAnyCAPlugin implementation (enroll, revoke, sync, ping, validation) - HMAC-signed POST-only API client targeting CERTInext REST API - AccessKey and OAuth authentication modes - Unit test project (97 tests, 90%+ coverage) with WireMock + Moq - Integration test project (14 live tests, skippable without credentials) - SOX/SOC1/SOC2-compliant structured audit logging - Developer Makefile with build, test, coverage, and API smoke-test targets - docsource: configuration, development, and architecture docs with Mermaid diagrams - GitHub workflows: dotnet-ci, release-please, sync-issues-to-ado - Terraform for repository management
0 parents  commit 0e0afcd

52 files changed

Lines changed: 10701 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
---
2+
name: Bug report
3+
about: Something in the plugin is broken or behaving unexpectedly
4+
labels: bug
5+
---
6+
7+
## Describe the bug
8+
9+
<!-- What happened? What did you expect? -->
10+
11+
## Steps to reproduce
12+
13+
1.
14+
2.
15+
3.
16+
17+
## Environment
18+
19+
- Keyfactor Command version:
20+
- AnyCA Gateway REST framework version:
21+
- Plugin version:
22+
- CERTInext environment (sandbox / production India / production US):
23+
- Auth mode (AccessKey / OAuth):
24+
25+
## Relevant output
26+
27+
<!-- Paste the gateway log output, error message, or API response. Remove any credentials before pasting. -->
28+
29+
```
30+
```
31+
32+
## Troubleshooting already attempted
33+
34+
<!-- What have you tried? This saves duplicated effort. -->

.github/ISSUE_TEMPLATE/config.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
blank_issues_enabled: false
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
name: Feature request
3+
about: Suggest an improvement to the plugin, configuration, or documentation
4+
labels: enhancement
5+
---
6+
7+
## Problem
8+
9+
<!-- What limitation or gap are you running into? -->
10+
11+
## Proposed solution
12+
13+
<!-- What would you like to see added or changed? -->
14+
15+
## Alternatives considered
16+
17+
<!-- Any other approaches you've thought about? -->

.github/PULL_REQUEST_TEMPLATE.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
## What
2+
3+
<!-- One or two sentences: what does this PR do? -->
4+
5+
## Why
6+
7+
<!-- What problem does it solve, or what improvement does it make? -->
8+
9+
## Testing
10+
11+
<!-- How was this validated? Check all that apply. -->
12+
13+
- [ ] `make test` passes (unit tests)
14+
- [ ] `make integration-test` passes (requires `~/.env_certinext`)
15+
- [ ] `make coverage` shows no coverage regression
16+
- [ ] Terraform changes validated with `terraform plan`
17+
- [ ] Tested only docs/config — no runtime changes
18+
19+
## Checklist
20+
21+
- [ ] PR title follows [Conventional Commits](https://www.conventionalcommits.org/) (`feat:`, `fix:`, `chore:`, etc.) — this becomes the squash commit message on `main`
22+
- [ ] No secrets, credential files, or `~/.env_certinext` content committed
23+
- [ ] `docsource/` updated if behavior or configuration changed

.github/workflows/dotnet-ci.yml

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
name: .NET CI
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
pull_request:
8+
branches:
9+
- main
10+
paths:
11+
- "**/*.cs"
12+
- "**/*.csproj"
13+
- "**/*.sln"
14+
- ".github/workflows/dotnet-ci.yml"
15+
16+
concurrency:
17+
group: dotnet-ci-${{ github.ref }}
18+
cancel-in-progress: true
19+
20+
jobs:
21+
build-and-test:
22+
name: Build and Test
23+
runs-on: ubuntu-latest
24+
25+
steps:
26+
- name: Checkout
27+
uses: actions/checkout@v4
28+
29+
- name: Setup .NET
30+
uses: actions/setup-dotnet@v4
31+
with:
32+
dotnet-version: "8.0.x"
33+
34+
- name: Restore dependencies
35+
run: dotnet restore certinext-caplugin.sln
36+
37+
- name: Build
38+
run: dotnet build certinext-caplugin.sln --no-restore --configuration Release
39+
40+
- name: Run unit tests
41+
run: |
42+
dotnet test certinext-caplugin.sln \
43+
--no-build \
44+
--configuration Release \
45+
--verbosity normal \
46+
--collect:"XPlat Code Coverage" \
47+
--results-directory /tmp/coverage
48+
49+
- name: Generate coverage report
50+
run: |
51+
dotnet tool install -g dotnet-reportgenerator-globaltool 2>/dev/null || true
52+
reportgenerator \
53+
-reports:"/tmp/coverage/**/coverage.cobertura.xml" \
54+
-targetdir:/tmp/coverage-report \
55+
-reporttypes:"MarkdownSummaryGithub;Cobertura"
56+
57+
- name: Post coverage summary
58+
if: github.event_name == 'pull_request'
59+
uses: actions/github-script@v7
60+
with:
61+
script: |
62+
const fs = require('fs');
63+
const summary = fs.readFileSync('/tmp/coverage-report/SummaryGithub.md', 'utf8');
64+
github.rest.issues.createComment({
65+
issue_number: context.issue.number,
66+
owner: context.repo.owner,
67+
repo: context.repo.repo,
68+
body: summary,
69+
});
70+
71+
- name: Upload coverage artifact
72+
uses: actions/upload-artifact@v4
73+
with:
74+
name: coverage-report
75+
path: /tmp/coverage-report/
76+
retention-days: 14
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
name: Release Please
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
workflow_dispatch:
8+
9+
permissions:
10+
contents: write
11+
pull-requests: write
12+
13+
jobs:
14+
release-please:
15+
runs-on: ubuntu-latest
16+
steps:
17+
- uses: googleapis/release-please-action@v4
18+
with:
19+
token: ${{ secrets.GITHUB_TOKEN }}
20+
config-file: release-please-config.json
21+
manifest-file: .release-please-manifest.json
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
name: Sync Issues to Azure DevOps
2+
3+
# One-way sync: GitHub Issue → ADO work item.
4+
#
5+
# Work item type: bug label → Bug, enhancement label → Epic, unlabeled → Bug
6+
# ADO item ID: stored in a hidden comment on the issue (<!-- ado:<id> -->) so
7+
# updates and closes can find it without external state.
8+
#
9+
# Config: integration-manifest.json (.ado_config)
10+
# Secret required: ADO_PERSONAL_ACCESS_TOKEN (repo secret, not environment-scoped)
11+
# Scope: Work Items (Read & Write)
12+
13+
on:
14+
issues:
15+
types: [opened, edited, closed, reopened, deleted]
16+
17+
permissions:
18+
contents: read
19+
issues: write
20+
21+
jobs:
22+
sync:
23+
runs-on: ubuntu-latest
24+
25+
steps:
26+
- uses: actions/checkout@v4
27+
28+
- name: Load ADO config from manifest
29+
id: manifest
30+
run: |
31+
cfg=$(jq '.ado_config' integration-manifest.json)
32+
echo "org=$(echo "$cfg" | jq -r '.org')" >> "$GITHUB_OUTPUT"
33+
echo "project=$(echo "$cfg" | jq -r '.project')" >> "$GITHUB_OUTPUT"
34+
echo "area=$(echo "$cfg" | jq -r '.area')" >> "$GITHUB_OUTPUT"
35+
echo "tags=$(echo "$cfg" | jq -r '.tags | join(";")')" >> "$GITHUB_OUTPUT"
36+
echo "priority=$(echo "$cfg" | jq -r '.priority // 3')" >> "$GITHUB_OUTPUT"
37+
echo "iteration=$(echo "$cfg" | jq -r '.iteration_path // ""')" >> "$GITHUB_OUTPUT"
38+
echo "state_closed=$(echo "$cfg" | jq -r '.state_closed // "Done"')" >> "$GITHUB_OUTPUT"
39+
echo "state_reopened=$(echo "$cfg" | jq -r '.state_reopened // "New"')" >> "$GITHUB_OUTPUT"
40+
41+
- name: Determine work item type from issue labels
42+
id: wit
43+
env:
44+
LABELS_JSON: ${{ toJson(github.event.issue.labels) }}
45+
run: |
46+
NAMES=$(echo "$LABELS_JSON" | jq -r '.[].name')
47+
if echo "$NAMES" | grep -qx 'bug'; then
48+
echo "type=Bug" >> "$GITHUB_OUTPUT"
49+
elif echo "$NAMES" | grep -qx 'enhancement'; then
50+
echo "type=Epic" >> "$GITHUB_OUTPUT"
51+
else
52+
echo "type=Bug" >> "$GITHUB_OUTPUT"
53+
fi
54+
55+
- name: Find ADO work item ID from issue comments
56+
id: ado
57+
if: github.event.action != 'opened'
58+
env:
59+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
60+
run: |
61+
ID=$(gh api "repos/${{ github.repository }}/issues/${{ github.event.issue.number }}/comments" \
62+
| jq -r 'map(select(.body | contains("<!-- ado:"))) | last | .body' \
63+
| grep -oP '(?<=<!-- ado:)\d+(?= -->)' || true)
64+
echo "id=${ID}" >> "$GITHUB_OUTPUT"
65+
66+
- name: Acknowledge issue to reporter
67+
if: github.event.action == 'opened'
68+
env:
69+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
70+
run: |
71+
MSG=$(jq -r '.issue_acknowledgment // "Thanks for opening this issue! Our team has been notified and will follow up here."' integration-manifest.json)
72+
gh issue comment ${{ github.event.issue.number }} \
73+
--repo "${{ github.repository }}" \
74+
--body "$MSG"
75+
76+
- name: Create ADO work item
77+
id: create
78+
if: github.event.action == 'opened'
79+
env:
80+
ADO_PAT: ${{ secrets.ADO_PERSONAL_ACCESS_TOKEN }}
81+
ISSUE_TITLE: ${{ github.event.issue.title }}
82+
ISSUE_BODY: ${{ github.event.issue.body }}
83+
ISSUE_URL: ${{ github.event.issue.html_url }}
84+
run: |
85+
ORG="${{ steps.manifest.outputs.org }}"
86+
PROJECT="${{ steps.manifest.outputs.project }}"
87+
AREA="${{ steps.manifest.outputs.area }}"
88+
TAGS="${{ steps.manifest.outputs.tags }}"
89+
PRIORITY="${{ steps.manifest.outputs.priority }}"
90+
ITERATION="${{ steps.manifest.outputs.iteration }}"
91+
TYPE="${{ steps.wit.outputs.type }}"
92+
93+
DESCRIPTION=$(jq -rn \
94+
--arg url "$ISSUE_URL" \
95+
--arg body "$ISSUE_BODY" \
96+
'"<p>GitHub Issue: <a href=\"\($url)\">\($url)</a></p><pre>\($body)</pre>"')
97+
98+
FOUND_IN="GitHub #${{ github.event.issue.number }}"
99+
100+
PAYLOAD=$(jq -n \
101+
--arg title "$ISSUE_TITLE" \
102+
--arg desc "$DESCRIPTION" \
103+
--arg area "$AREA" \
104+
--arg tags "$TAGS" \
105+
--argjson prio "$PRIORITY" \
106+
--arg iteration "$ITERATION" \
107+
--arg found_in "$FOUND_IN" \
108+
'[
109+
{"op":"add","path":"/fields/System.Title","value":$title},
110+
{"op":"add","path":"/fields/System.Description","value":$desc},
111+
{"op":"add","path":"/fields/System.AreaPath","value":$area},
112+
{"op":"add","path":"/fields/System.Tags","value":$tags},
113+
{"op":"add","path":"/fields/Microsoft.VSTS.Common.Priority","value":$prio},
114+
{"op":"add","path":"/fields/Microsoft.VSTS.Build.FoundIn","value":$found_in}
115+
]
116+
+ if $iteration != "" then
117+
[{"op":"add","path":"/fields/System.IterationPath","value":$iteration}]
118+
else [] end')
119+
120+
AUTH=$(printf ':%s' "$ADO_PAT" | base64 -w 0)
121+
122+
RESPONSE=$(curl -s -w "\nHTTP_STATUS:%{http_code}" -X PATCH \
123+
"https://dev.azure.com/${ORG}/${PROJECT}/_apis/wit/workitems/\$${TYPE}?api-version=7.1" \
124+
-H "Authorization: Basic ${AUTH}" \
125+
-H "Content-Type: application/json-patch+json" \
126+
-d "$PAYLOAD")
127+
128+
HTTP_STATUS=$(echo "$RESPONSE" | grep -o "HTTP_STATUS:[0-9]*" | cut -d: -f2)
129+
BODY=$(echo "$RESPONSE" | sed '/HTTP_STATUS:/d')
130+
echo "ADO response status: $HTTP_STATUS"
131+
132+
if [[ "$HTTP_STATUS" -lt 200 || "$HTTP_STATUS" -ge 300 ]]; then
133+
echo "::error::ADO API returned $HTTP_STATUS"
134+
exit 1
135+
fi
136+
137+
echo "id=$(echo "$BODY" | jq -r '.id')" >> "$GITHUB_OUTPUT"
138+
echo "url=$(echo "$BODY" | jq -r '._links.html.href')" >> "$GITHUB_OUTPUT"
139+
140+
- name: Post ADO link comment on issue
141+
if: github.event.action == 'opened' && steps.create.outputs.id != ''
142+
env:
143+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
144+
run: |
145+
ID="${{ steps.create.outputs.id }}"
146+
URL="${{ steps.create.outputs.url }}"
147+
gh issue comment ${{ github.event.issue.number }} \
148+
--repo "${{ github.repository }}" \
149+
--body "<!-- ado:${ID} -->
150+
<details><summary>Internal: ADO work item</summary>
151+
152+
[#${ID}](${URL})
153+
</details>"
154+
155+
- name: Update ADO work item
156+
if: github.event.action != 'opened' && steps.ado.outputs.id != ''
157+
env:
158+
ADO_PAT: ${{ secrets.ADO_PERSONAL_ACCESS_TOKEN }}
159+
ISSUE_TITLE: ${{ github.event.issue.title }}
160+
ISSUE_BODY: ${{ github.event.issue.body }}
161+
ISSUE_URL: ${{ github.event.issue.html_url }}
162+
run: |
163+
ORG="${{ steps.manifest.outputs.org }}"
164+
PROJECT="${{ steps.manifest.outputs.project }}"
165+
ID="${{ steps.ado.outputs.id }}"
166+
ACTION="${{ github.event.action }}"
167+
168+
OPS='[]'
169+
170+
if [[ "$ACTION" == "edited" ]]; then
171+
DESCRIPTION=$(jq -rn \
172+
--arg url "$ISSUE_URL" \
173+
--arg body "$ISSUE_BODY" \
174+
'"<p>GitHub Issue: <a href=\"\($url)\">\($url)</a></p><pre>\($body)</pre>"')
175+
OPS=$(echo "$OPS" | jq \
176+
--arg title "$ISSUE_TITLE" \
177+
--arg desc "$DESCRIPTION" \
178+
'. + [
179+
{"op":"replace","path":"/fields/System.Title","value":$title},
180+
{"op":"replace","path":"/fields/System.Description","value":$desc}
181+
]')
182+
fi
183+
184+
if [[ "$ACTION" == "closed" || "$ACTION" == "deleted" ]]; then
185+
STATE="${{ steps.manifest.outputs.state_closed }}"
186+
OPS=$(echo "$OPS" | jq --arg s "$STATE" \
187+
'. + [{"op":"replace","path":"/fields/System.State","value":$s}]')
188+
fi
189+
190+
if [[ "$ACTION" == "reopened" ]]; then
191+
STATE="${{ steps.manifest.outputs.state_reopened }}"
192+
OPS=$(echo "$OPS" | jq --arg s "$STATE" \
193+
'. + [{"op":"replace","path":"/fields/System.State","value":$s}]')
194+
fi
195+
196+
if [[ "$OPS" == '[]' ]]; then
197+
echo "No ADO fields to update for action: $ACTION — skipping."
198+
exit 0
199+
fi
200+
201+
AUTH=$(printf ':%s' "$ADO_PAT" | base64 -w 0)
202+
203+
curl -sf -X PATCH \
204+
"https://dev.azure.com/${ORG}/${PROJECT}/_apis/wit/workitems/${ID}?api-version=7.1" \
205+
-H "Authorization: Basic ${AUTH}" \
206+
-H "Content-Type: application/json-patch+json" \
207+
-d "$OPS"

0 commit comments

Comments
 (0)