venafi-connector-machine 2.0.1 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/bundle.mjs +365 -112
  2. package/package.json +1 -1
package/bundle.mjs CHANGED
@@ -29951,11 +29951,11 @@ var StdioServerTransport = class {
29951
29951
  var MACHINE_BLUEPRINT = `
29952
29952
  # Venafi Machine Connector Blueprint
29953
29953
 
29954
- This document captures the machine connector-specific architecture, patterns, and structure for building a Venafi TLS Protect Cloud machine connector. It is distilled from building multiple connectors: Splunk Enterprise (SSH), FortiGate (REST API), IBM API Connect (REST API), DataPower (REST API), and the Venafi Machine Connector Framework documentation.
29954
+ This document captures the machine connector-specific architecture, patterns, and structure for building a Venafi TLS Protect Cloud machine connector. It is distilled from building multiple connectors across SSH-based targets, REST API appliances, and API management platforms, as well as the Venafi Machine Connector Framework documentation.
29955
29955
 
29956
29956
  ## What Is a Machine Connector?
29957
29957
 
29958
- A machine connector is a containerized Go REST service that runs on a Venafi vSatellite. It acts as middleware between Venafi TLS Protect Cloud and a target system (e.g., Splunk, Apache, Nginx, HAProxy, F5). The connector discovers certificates on the target, reports them to Venafi, and provisions/renews certificates when instructed.
29958
+ A machine connector is a containerized Go REST service that runs on a Venafi vSatellite. It acts as middleware between Venafi TLS Protect Cloud and a target system (e.g., web servers, app servers, load balancers, network appliances). The connector discovers certificates on the target, reports them to Venafi, and provisions/renews certificates when instructed.
29959
29959
 
29960
29960
  - **pluginType**: "MACHINE"
29961
29961
  - **workTypes**: ["PROVISIONING", "DISCOVERY"]
@@ -30213,7 +30213,7 @@ Keystore and binding fields can be turned into dropdowns populated from the targ
30213
30213
  2. Implementing \`getTargetConfiguration\` to return actual values (not a stub)
30214
30214
  3. Adding \`x-targetConfigurationRef\` to the field in keystore/binding
30215
30215
 
30216
- **Example**: FortiGate VDOMs as a dropdown:
30216
+ **Example**: Virtual domains (partitions) as a dropdown:
30217
30217
 
30218
30218
  \`\`\`json
30219
30219
  "targetConfiguration": {
@@ -30236,7 +30236,7 @@ Keystore and binding fields can be turned into dropdowns populated from the targ
30236
30236
  }
30237
30237
  \`\`\`
30238
30238
 
30239
- The F5 BIG-IP connector uses this same pattern for partition dropdowns.
30239
+ Other reference connectors use this same pattern for partition dropdowns.
30240
30240
 
30241
30241
  ## Project Structure (Proven Pattern)
30242
30242
 
@@ -30266,7 +30266,7 @@ The F5 BIG-IP connector uses this same pattern for partition dropdowns.
30266
30266
  \u2502 \u251C\u2500\u2500 discovery/ # Certificate discovery logic
30267
30267
  \u2502 \u2502 \u251C\u2500\u2500 discovery.go # Main discovery orchestration
30268
30268
  \u2502 \u2502 \u2514\u2500\u2500 types.go # Request/response types
30269
- \u2502 \u2514\u2500\u2500 <target>/ # Target-specific logic (e.g., splunk/, apache/, nginx/)
30269
+ \u2502 \u2514\u2500\u2500 <target>/ # Target-specific logic (e.g., webserver/, appliance/)
30270
30270
  \u2502 \u251C\u2500\u2500 client.go # Connection client abstraction (SSH, API, etc.)
30271
30271
  \u2502 \u251C\u2500\u2500 detect.go # Target software detection
30272
30272
  \u2502 \u251C\u2500\u2500 install.go # Certificate file writing
@@ -30356,7 +30356,7 @@ func New() *fx.App {
30356
30356
  var LESSONS_LEARNED = `
30357
30357
  # Lessons Learned: Machine Connector Projects
30358
30358
 
30359
- This document captures everything that worked well, everything that was challenging, and every mistake or pitfall encountered while building machine connectors (Splunk SSH, FortiGate REST, IBM API Connect REST). Use this to avoid repeating mistakes and to replicate what worked.
30359
+ This document captures everything that worked well, everything that was challenging, and every mistake or pitfall encountered while building machine connectors across SSH-based targets and REST API appliances. Use this to avoid repeating mistakes and to replicate what worked.
30360
30360
 
30361
30361
  ---
30362
30362
 
@@ -30461,7 +30461,7 @@ This is critical for any values that come from user input (file paths, passwords
30461
30461
 
30462
30462
  **The reality**:
30463
30463
  - Venafi sends certificates in the \`CertificateBundle\` as **DER-encoded binary** (\`[]byte\`)
30464
- - Most target systems (Splunk, Apache, Nginx) expect **PEM format** (text with BEGIN/END headers)
30464
+ - Most target systems (web servers, app servers) expect **PEM format** (text with BEGIN/END headers)
30465
30465
  - Discovery must return certificates in **PEM format** (text strings)
30466
30466
 
30467
30467
  **The fix**: Always use \`pem.EncodeToMemory()\` when converting from Venafi's DER format to files:
@@ -30581,7 +30581,7 @@ return c.String(http.StatusBadRequest, fmt.Sprintf("SSH connection failed: %s",
30581
30581
 
30582
30582
  ### 8. Config Parsing is Inherently Fragile
30583
30583
 
30584
- **The issue**: Splunk's config files use an INI-like format, and our parser is basic:
30584
+ **The issue**: The target's config files use an INI-like format, and our parser is basic:
30585
30585
 
30586
30586
  \`\`\`go
30587
30587
  func parseINI(content string) map[string]map[string]string {
@@ -30590,34 +30590,34 @@ func parseINI(content string) map[string]map[string]string {
30590
30590
  \`\`\`
30591
30591
 
30592
30592
  **What it doesn't handle**:
30593
- - Multi-line values (rare in Splunk but possible)
30593
+ - Multi-line values (rare but possible)
30594
30594
  - Escaped characters
30595
30595
  - Include directives
30596
30596
  - App-level configs (only reads system/local and system/default)
30597
30597
 
30598
- **Why it worked anyway**: Splunk's SSL config is always simple key=value pairs. The basic parser handles 95%+ of real-world configs.
30598
+ **Why it worked anyway**: The target's SSL config is always simple key=value pairs. The basic parser handles 95%+ of real-world configs.
30599
30599
 
30600
30600
  **Recommendation for the next connector**: Match the parser complexity to the target's config format. If the target uses JSON/YAML, use Go's standard \`encoding/json\` or a YAML library. Don't build custom parsers for well-known formats.
30601
30601
 
30602
30602
  ### 9. Container Detection Based on Image Name
30603
30603
 
30604
- **The issue**: Container Splunk detection uses \`grep -i splunk\` on the image name:
30604
+ **The issue**: Container detection uses \`grep -i <product>\` on the image name:
30605
30605
 
30606
30606
  \`\`\`go
30607
- cs.RunCommand(client, fmt.Sprintf("%s ps --format '{{.Names}} {{.Image}}' | grep -i splunk", runtime))
30607
+ cs.RunCommand(client, fmt.Sprintf("%s ps --format '{{.Names}} {{.Image}}' | grep -i <product>", runtime))
30608
30608
  \`\`\`
30609
30609
 
30610
- **The problem**: This misses containers with custom image names that don't include "splunk" and may false-positive on unrelated containers that happen to have "splunk" in the name.
30610
+ **The problem**: This misses containers with custom image names that don't include the product name and may false-positive on unrelated containers that happen to match.
30611
30611
 
30612
30612
  **Recommendation for next connector**: Be specific about what you're detecting. If the target software has a known process name, binary path, or config file, check for those inside the container rather than relying on image name.
30613
30613
 
30614
30614
  ### 10. GetTargetConfiguration Can Power Dynamic Dropdowns
30615
30615
 
30616
- **The original issue**: The Splunk connector returned an empty response from \`getTargetConfiguration\`.
30616
+ **The original issue**: The SSH-based connector returned an empty response from \`getTargetConfiguration\`.
30617
30617
 
30618
- **Updated learning from FortiGate**: \`getTargetConfiguration\` is NOT just a stub. It can return dynamic values that populate dropdown fields in the Venafi UI. This is done via \`x-targetConfigurationRef\` in the manifest.
30618
+ **Updated learning from a REST API appliance connector**: \`getTargetConfiguration\` is NOT just a stub. It can return dynamic values that populate dropdown fields in the Venafi UI. This is done via \`x-targetConfigurationRef\` in the manifest.
30619
30619
 
30620
- **Pattern**: When the target has scopes/partitions/VDOMs, return them from \`getTargetConfiguration\`:
30620
+ **Pattern**: When the target has scopes/partitions/virtual domains, return them from \`getTargetConfiguration\`:
30621
30621
 
30622
30622
  \`\`\`go
30623
30623
  type GetTargetConfigurationResponse struct {
@@ -30646,7 +30646,7 @@ Then in the manifest, add a \`targetConfiguration\` domainSchema and reference t
30646
30646
  }
30647
30647
  \`\`\`
30648
30648
 
30649
- This creates a dropdown in the keystore form populated with actual values from the target system. The F5 BIG-IP connector uses this same pattern for partition dropdowns.
30649
+ This creates a dropdown in the keystore form populated with actual values from the target system. Other reference connectors use this same pattern for partition dropdowns.
30650
30650
 
30651
30651
  **Recommendation**: Use \`getTargetConfiguration\` whenever the target has enumerable scopes, profiles, or resource lists that users need to select from. Keep it as a stub only if there's nothing dynamic to populate.
30652
30652
 
@@ -30663,7 +30663,7 @@ This creates a dropdown in the keystore form populated with actual values from t
30663
30663
 
30664
30664
  **The problem**: The Venafi platform silently rejects this format. Discovery appears to succeed but returns: "The webhook invocation for discovering certificates failed because the discovery result is empty."
30665
30665
 
30666
- **The correct format** (confirmed by PAN Panorama, Citrix ADC, and FortiGate connectors):
30666
+ **The correct format** (confirmed by multiple reference connectors):
30667
30667
 
30668
30668
  \`\`\`json
30669
30669
  {
@@ -30688,9 +30688,9 @@ Key differences:
30688
30688
 
30689
30689
  **Why this is so dangerous**: The error message gives no clue about the wrong format. You'll spend hours debugging connectivity when the real issue is JSON structure. Always verify your response matches the types in the \`discovery-types.go\` template.
30690
30690
 
30691
- ### 12. Multi-Scope Discovery Pattern (VDOMs, Partitions, Tenants)
30691
+ ### 12. Multi-Scope Discovery Pattern (Virtual Domains, Partitions, Tenants)
30692
30692
 
30693
- **The pattern**: Many network appliances have scoping concepts (FortiGate VDOMs, F5 partitions, AVI tenants). Discovery should support both "discover all" and "discover specific scope."
30693
+ **The pattern**: Many network appliances have scoping concepts (virtual domains, partitions, tenants). Discovery should support both "discover all" and "discover specific scope."
30694
30694
 
30695
30695
  **Implementation**:
30696
30696
  1. The discovery request includes a scope field (e.g., \`vdom\`)
@@ -30737,7 +30737,7 @@ With localization: \`"vdomLabel": "VDOM (leave blank to discover all)"\`
30737
30737
  **What actually happens on network appliances**: Certificate data is split across APIs:
30738
30738
  - **Operational/Monitor APIs** return rich metadata (subject, issuer, serial number, fingerprint, validity dates) but NOT the PEM certificate content
30739
30739
  - **Configuration/CMDB APIs** return the actual PEM content, but only when fetching individual certificates \u2014 **batch/list endpoints strip heavy blob fields** to keep responses small
30740
- - This split is common across FortiGate (CMDB vs Monitor), F5 BIG-IP (mgmt vs stats), and similar REST API appliances
30740
+ - This split is common across REST API appliances (e.g., CMDB vs Monitor endpoints, mgmt vs stats endpoints)
30741
30741
 
30742
30742
  **The symptom in Venafi**: Discovery succeeds with no errors from the connector, but the Venafi Cloud activity log shows: **"Discovered certificate cannot be read"** for every certificate. This means the \`certificate\` field in the discovery response is empty or not valid PEM. The platform accepted the \`messages\` format but couldn't parse the cert data inside.
30743
30743
 
@@ -30773,7 +30773,7 @@ for _, cert := range certDetails {
30773
30773
 
30774
30774
  **Two approaches based on target type:**
30775
30775
 
30776
- **Approach A \u2014 Service-type filtering (FortiGate pattern):** For network appliances where certificates are assigned to different service types (admin HTTPS, SSL VPN, IPsec VPN, VIPs, SSL inspection), the \`discoveryTypes\` match the appliance's service categories:
30776
+ **Approach A \u2014 Service-type filtering (network appliance pattern):** For network appliances where certificates are assigned to different service types (admin HTTPS, SSL VPN, IPsec VPN, VIPs, SSL inspection), the \`discoveryTypes\` match the appliance's service categories:
30777
30777
 
30778
30778
  \`\`\`json
30779
30779
  "oneOf": [
@@ -30787,7 +30787,7 @@ for _, cert := range certDetails {
30787
30787
 
30788
30788
  These map 1:1 to the binding types. The connector queries only the selected service endpoints.
30789
30789
 
30790
- **Approach B \u2014 Usage-based filtering (APIC pattern):** For API management platforms where certificates serve different infrastructure roles, the \`discoveryTypes\` match certificate usage categories that the admin would recognize from their platform:
30790
+ **Approach B \u2014 Usage-based filtering (API management platform pattern):** For API management platforms where certificates serve different infrastructure roles, the \`discoveryTypes\` match certificate usage categories that the admin would recognize from their platform:
30791
30791
 
30792
30792
  \`\`\`json
30793
30793
  "oneOf": [
@@ -30800,7 +30800,7 @@ These map 1:1 to the binding types. The connector queries only the selected serv
30800
30800
 
30801
30801
  Here, the first three types all come from the same data source (keystores), but are classified after enumeration based on how they're used. The fourth comes from a separate data source (consumer apps). This approach replaces a less intuitive "profileBoundOnly" toggle \u2014 unchecking "Unassigned Keystores" achieves the same filtering in platform-native terms.
30802
30802
 
30803
- **Key: choosing the right categories.** Ask: "How would an admin of this platform describe their certificates?" For FortiGate, it's by service type (VPN, firewall, management). For APIC, it's by infrastructure role (gateway, backend, unassigned, consumer). For F5, it might be by partition or virtual server type. The checkboxes should match the admin's mental model.
30803
+ **Key: choosing the right categories.** Ask: "How would an admin of this platform describe their certificates?" For a network appliance, it's by service type (VPN, firewall, management). For an API management platform, it's by infrastructure role (gateway, backend, unassigned, consumer). For a load balancer, it might be by partition or virtual server type. The checkboxes should match the admin's mental model.
30804
30804
 
30805
30805
  **Implementation \u2014 binding metadata with per-type dropdown fields:**
30806
30806
 
@@ -30902,9 +30902,9 @@ Reporting configuration-level TLS settings (what the platform *says* it supports
30902
30902
  - Regular fields (no readOnly) \u2014 same result: stored but invisible in the UI
30903
30903
  - Fields added to \`x-primaryKey\` \u2014 **visible** in the UI immediately
30904
30904
 
30905
- **The rule**: The Venafi UI installation detail view renders ONLY the fields listed in the manifest's \`x-primaryKey\` arrays for both keystore and binding sections. No exceptions. No production connector (FortiGate, F5, Citrix ADC, A10) uses \`readOnly: true\` \u2014 all visible metadata goes through \`x-primaryKey\`.
30905
+ **The rule**: The Venafi UI installation detail view renders ONLY the fields listed in the manifest's \`x-primaryKey\` arrays for both keystore and binding sections. No exceptions. No production connector uses \`readOnly: true\` \u2014 all visible metadata goes through \`x-primaryKey\`.
30906
30906
 
30907
- **Verification**: FortiGate shows \`certificateName\`, \`vdom\`, \`bindingType\`, and \`targetName\` in the UI \u2014 every one of them is in \`x-primaryKey\`. Fields not in \`x-primaryKey\` are never shown regardless of any other manifest attributes.
30907
+ **Verification**: Reference connectors show \`certificateName\`, \`vdom\`, \`bindingType\`, and \`targetName\` in the UI \u2014 every one of them is in \`x-primaryKey\`. Fields not in \`x-primaryKey\` are never shown regardless of any other manifest attributes.
30908
30908
 
30909
30909
  **What administrators actually want**: When a platform admin looks at Venafi, they want to see the same context they see in their platform admin UI:
30910
30910
  - **Certificate usage**: "api-gateway" vs "outbound-mtls" vs "keystore-only"
@@ -30941,7 +30941,7 @@ Reporting configuration-level TLS settings (what the platform *says* it supports
30941
30941
  **What actually happens with readOnly fields**:
30942
30942
  - The Venafi platform **stores** the data correctly (confirmed via REST API queries)
30943
30943
  - The Venafi UI **does NOT display** readOnly fields in the installation detail view
30944
- - No production machine connector (FortiGate, F5, Citrix ADC, A10, PAN Panorama) uses \`readOnly: true\`
30944
+ - No production machine connector uses \`readOnly: true\`
30945
30945
  - The UI only renders fields listed in \`x-primaryKey\` (see section 16)
30946
30946
 
30947
30947
  **When readOnly might be useful**: Only for fields that are genuinely for internal use \u2014 like a platform UUID (\`keystoreId\`) used by the connector code during provisioning to target the correct resource, but not needed by human users in the UI. Even then, consider whether the field is worth including at all if users can't see it.
@@ -31042,7 +31042,7 @@ if binding.ApiNames == "" {
31042
31042
  4. Cross-reference with connector's discovery response \u2014 find MIs where a primaryKey field is empty
31043
31043
  5. Add a default value for the empty field, rebuild, and re-deploy
31044
31044
 
31045
- **Proven defaults from APIC connector**:
31045
+ **Proven defaults from an API management platform connector**:
31046
31046
  - \`catalogName\`: "org-level" for keystores/profiles not deployed to any catalog
31047
31047
  - \`productName\`: "N/A" for catalogs without published products
31048
31048
  - \`apiNames\`: "N/A" for catalogs without published APIs
@@ -31060,7 +31060,7 @@ if binding.ApiNames == "" {
31060
31060
 
31061
31061
  With catalog-level enrichment, BOTH entries show: Product = "Product Discovery, Customer Platform", APIs = "Product Catalog API, Customer API, Order API". This is misleading and unhelpful.
31062
31062
 
31063
- **The fix \u2014 Profile-level enrichment via title matching**: When the target platform doesn't provide a direct API-to-profile mapping (e.g., APIC's \`tls_client_profile_urls\` is empty on published APIs), use the TLS profile title as a heuristic to match against product names. Platform administrators typically follow naming conventions where profile titles encode the product/service context:
31063
+ **The fix \u2014 Profile-level enrichment via title matching**: When the target platform doesn't provide a direct API-to-profile mapping (e.g., the platform's \`tls_client_profile_urls\` is empty on published APIs), use the TLS profile title as a heuristic to match against product names. Platform administrators typically follow naming conventions where profile titles encode the product/service context:
31064
31064
 
31065
31065
  \`\`\`go
31066
31066
  // matchProfileToProduct matches a TLS profile to a specific product
@@ -31102,7 +31102,7 @@ func matchProfileToProduct(profileTitle string, cp catalogProductInfo) (string,
31102
31102
  - **\`discoveryTypes\` (array field with oneOf checkboxes)**: \`x-labelLocalizationKey\` is NOT resolved \u2014 the raw key text appears as the label in the UI (e.g., "discovery.typesLabel" shows literally). You MUST use the \`"title"\` property instead.
31103
31103
  - **Boolean discovery fields** (e.g., \`excludeExpiredCertificates\`): \`x-labelLocalizationKey\` DOES work, but ONLY with the nested \`"discovery.xxx"\` pattern.
31104
31104
 
31105
- **The correct pattern (confirmed by FortiGate, verified on APIC):**
31105
+ **The correct pattern (confirmed across multiple connectors):**
31106
31106
 
31107
31107
  \`\`\`json
31108
31108
  "discovery": {
@@ -31207,11 +31207,11 @@ for _, chainDER := range bundle.CertificateChain {
31207
31207
  2. Is the root CA ever included, or only intermediates?
31208
31208
  3. Should connectors assume the order and pass through, or re-sort based on issuer/subject matching?
31209
31209
 
31210
- This matters for targets like F5, Citrix ADC, and Java keystores where incorrect chain order causes TLS handshake failures. Until documented, connectors should pass through in the order received and note this assumption in their documentation.
31210
+ This matters for targets like load balancers, ADCs, and Java keystores where incorrect chain order causes TLS handshake failures. Until documented, connectors should pass through in the order received and note this assumption in their documentation.
31211
31211
 
31212
31212
  ### 25. DER-to-PEM Conversion for REST API Discovery
31213
31213
 
31214
- **The pattern**: When a REST API target returns certificates as base64-encoded DER (common for appliances like DataPower, FortiGate), the discovery response must convert to PEM. This conversion is easy to miss because the API response looks like a base64 string that could be passed through directly.
31214
+ **The pattern**: When a REST API target returns certificates as base64-encoded DER (common for REST API appliances), the discovery response must convert to PEM. This conversion is easy to miss because the API response looks like a base64 string that could be passed through directly.
31215
31215
 
31216
31216
  \`\`\`go
31217
31217
  // REST API returns base64 DER \u2014 MUST convert to PEM for discovery response
@@ -31309,7 +31309,7 @@ Getting these wrong can cause the target service to refuse to start.
31309
31309
 
31310
31310
  ### 1. Unit Tests
31311
31311
 
31312
- The Splunk connector has zero unit tests. This was acceptable for an MVP but should be addressed from the start:
31312
+ The SSH-based connector has zero unit tests. This was acceptable for an MVP but should be addressed from the start:
31313
31313
 
31314
31314
  - Mock \`ClientServices\` to test handlers without SSH
31315
31315
  - Test certificate parsing with known PEM files
@@ -31357,13 +31357,13 @@ Or return \`result: false\` with a helpful message if the target software isn't
31357
31357
 
31358
31358
  ---
31359
31359
 
31360
- ## REST API Appliance Patterns (FortiGate, APIC, DataPower)
31360
+ ## REST API Appliance Patterns
31361
31361
 
31362
31362
  These patterns apply to machine connectors that communicate via REST API rather than SSH.
31363
31363
 
31364
31364
  ### 20. PKCS12 Is the Dominant Provisioning Format for REST API Targets
31365
31365
 
31366
- **The pattern**: Most REST API appliances (FortiGate, IBM APIC, F5, Citrix ADC, PAN Panorama) accept certificate bundles as **PKCS12** files, not raw PEM files. The connector must build a PKCS12 bundle from Venafi's DER-encoded certificate bundle.
31366
+ **The pattern**: Most REST API appliances accept certificate bundles as **PKCS12** files, not raw PEM files. The connector must build a PKCS12 bundle from Venafi's DER-encoded certificate bundle.
31367
31367
 
31368
31368
  **Implementation** (using \`go-pkcs12\`):
31369
31369
 
@@ -31410,7 +31410,7 @@ func parsePrivateKey(keyDER []byte) (crypto.PrivateKey, error) {
31410
31410
  }
31411
31411
  \`\`\`
31412
31412
 
31413
- This sequence is proven across FortiGate, APIC, and DataPower connectors. Never assume RSA.
31413
+ This sequence is proven across multiple REST API appliance connectors. Never assume RSA.
31414
31414
 
31415
31415
  ### 22. CRITICAL: Binding Must Never Be Nil in Discovery Results
31416
31416
 
@@ -31446,7 +31446,7 @@ if hasBindings {
31446
31446
  2. **If yes**: Upload a new cert with a unique name (e.g., \`certName_YYMMDD_serial\`), update the binding to point to the new cert, then optionally delete the old cert
31447
31447
  3. **If no**: Simply upload the cert and create the binding reference
31448
31448
 
31449
- This is the proven pattern for FortiGate and PAN Panorama where certificates are immutable once created. APIC handles this differently \u2014 its PATCH API preserves UUIDs when updating keystore content in-place.
31449
+ This is the proven pattern for appliances where certificates are immutable once created. Some API management platforms handle this differently \u2014 their PATCH API preserves UUIDs when updating keystore content in-place.
31450
31450
 
31451
31451
  **Key**: The \`installCertificateBundle\` handler returns the updated keystore (with the new cert name), which is passed to \`configureInstallationEndpoint\`. Use the keystore fields to carry the cert name between activities.
31452
31452
 
@@ -31490,9 +31490,9 @@ if pageEnd < len(allCerts) {
31490
31490
  // page = nil means "done"
31491
31491
  \`\`\`
31492
31492
 
31493
- ### 25. OAuth2 Token Exchange (APIC Pattern)
31493
+ ### 25. OAuth2 Token Exchange (API Management Platform Pattern)
31494
31494
 
31495
- **The pattern**: Some REST API targets (IBM APIC) use OAuth2 token exchange instead of static API keys. The connector exchanges credentials for a bearer token at the start of each endpoint call.
31495
+ **The pattern**: Some REST API targets (API management platforms) use OAuth2 token exchange instead of static API keys. The connector exchanges credentials for a bearer token at the start of each endpoint call.
31496
31496
 
31497
31497
  \`\`\`go
31498
31498
  func (h *Handler) authenticate() error {
@@ -31516,34 +31516,34 @@ func (h *Handler) authenticate() error {
31516
31516
  \`\`\`
31517
31517
 
31518
31518
  **IMPORTANT \u2014 Token endpoint body format varies by target**:
31519
- - **IBM APIC**: Requires \`application/json\` body (NOT form-urlencoded). Sending form-urlencoded silently fails.
31519
+ - **Some API platforms**: Require \`application/json\` body (NOT form-urlencoded). Sending form-urlencoded silently fails.
31520
31520
  - **Standard OAuth2**: Uses \`application/x-www-form-urlencoded\` per RFC 6749 (use \`SetFormData()\`).
31521
31521
  - Always check the target API documentation for the expected content type.
31522
31522
 
31523
- **Key**: Token has a TTL (8 hours for APIC). For connectors where each endpoint call is stateless, exchange the token at the start of each call \u2014 don't cache across calls.
31523
+ **Key**: Token has a TTL (e.g., 8 hours). For connectors where each endpoint call is stateless, exchange the token at the start of each call \u2014 don't cache across calls.
31524
31524
 
31525
31525
  ### 26. REST API Targets May Use PATCH Instead of PUT
31526
31526
 
31527
- **The learning**: IBM APIC hosted environments return 405 (Method Not Allowed) for PUT on keystore updates. APIC requires PATCH. FortiGate requires PUT for most updates. DataPower uses PUT for config objects.
31527
+ **The learning**: Some API management platforms return 405 (Method Not Allowed) for PUT on keystore updates and require PATCH instead. Other REST API appliances require PUT for most updates.
31528
31528
 
31529
31529
  **The fix**: Don't assume PUT or PATCH \u2014 check the target API documentation and test both during development. Make the HTTP method part of your helper function:
31530
31530
 
31531
31531
  \`\`\`go
31532
31532
  func (h *Handler) UpdateResource(path string, body interface{}) (*resty.Response, error) {
31533
- // APIC: PATCH; FortiGate: PUT; DataPower: PUT
31533
+ // some targets require PATCH; others use PUT
31534
31534
  return h.client.R().SetBody(body).Patch(h.baseURL + path) // or .Put()
31535
31535
  }
31536
31536
  \`\`\`
31537
31537
 
31538
31538
  ### 27. Protocols Field Must Never Be Empty Array for Machine Identities
31539
31539
 
31540
- **The learning from APIC**: Venafi platform silently drops machine identities with \`protocols: []\` (empty array) in the discovery response. Omitting \`protocols\` entirely (with \`omitempty\`) causes hard discovery failure.
31540
+ **The learning**: Venafi platform silently drops machine identities with \`protocols: []\` (empty array) in the discovery response. Omitting \`protocols\` entirely (with \`omitempty\`) causes hard discovery failure.
31541
31541
 
31542
31542
  **The fix**: For onboard discovery, do NOT set protocols at all. Leave the field out of the response struct entirely (don't even define it). Protocols are for network discovery connectors that observe live TLS connections, not for onboard discovery that reads platform inventory.
31543
31543
 
31544
31544
  ### 28. Null-Safe JSON Arrays in Discovery Responses
31545
31545
 
31546
- **The learning from APIC**: Go's \`encoding/json\` marshals a nil slice as \`null\`, but the Venafi platform expects empty arrays to be \`[]\` (not \`null\`). A \`null\` value for \`certificateChain\` or \`machineIdentities\` can cause discovery failures or silent data loss.
31546
+ **The learning**: Go's \`encoding/json\` marshals a nil slice as \`null\`, but the Venafi platform expects empty arrays to be \`[]\` (not \`null\`). A \`null\` value for \`certificateChain\` or \`machineIdentities\` can cause discovery failures or silent data loss.
31547
31547
 
31548
31548
  **The symptom**: Discovery responses with \`"certificateChain": null\` or \`"machineIdentities": null\` are silently rejected or produce incomplete results.
31549
31549
 
@@ -31660,7 +31660,7 @@ func truncateMetadata(s string, totalCount int) string {
31660
31660
 
31661
31661
  **The mistake**: Using modern PKCS12 encoding (AES-256-CBC + SHA-256) when building PKCS12 bundles for certificate import to network appliances.
31662
31662
 
31663
- **The problem**: Many network appliances (FortiGate confirmed, likely F5 and others) cannot parse modern PKCS12 encoding. The import API returns success or a vague error, but the certificate is not usable. FortiGate specifically requires the legacy PKCS12 format (3DES + SHA1).
31663
+ **The problem**: Many network appliances cannot parse modern PKCS12 encoding. The import API returns success or a vague error, but the certificate is not usable. These appliances specifically require the legacy PKCS12 format (3DES + SHA1).
31664
31664
 
31665
31665
  **The fix**: When building PKCS12 bundles in Go, use the Legacy encoder:
31666
31666
 
@@ -31670,7 +31670,7 @@ import gopkcs12 "software.sslmate.com/src/go-pkcs12"
31670
31670
  // CORRECT: Legacy encoding (3DES/SHA1) \u2014 compatible with network appliances
31671
31671
  p12Data, err := gopkcs12.Legacy.Encode(key, cert, caCerts, password)
31672
31672
 
31673
- // WRONG: Modern encoding (AES-256/SHA-256) \u2014 rejected by FortiGate and similar appliances
31673
+ // WRONG: Modern encoding (AES-256/SHA-256) \u2014 rejected by many network appliances
31674
31674
  // p12Data, err := gopkcs12.Modern.Encode(key, cert, caCerts, password)
31675
31675
  \`\`\`
31676
31676
 
@@ -31911,7 +31911,7 @@ if _, err := x509.ParsePKCS8PrivateKey(keyDER); err == nil {
31911
31911
 
31912
31912
  **When to use which approach**:
31913
31913
  - **Target accepts PEM** (GCP, AWS, cloud APIs): Use the no-re-marshal approach above. Just detect the type for the PEM header and wrap the original DER bytes.
31914
- - **Target requires PKCS12** (FortiGate, network appliances): You MUST parse into Go types to pass to \`pkcs12.Encode()\`. Re-marshaling is unavoidable and acceptable here because the PKCS12 encoder produces its own encoding.
31914
+ - **Target requires PKCS12** (network appliances): You MUST parse into Go types to pass to \`pkcs12.Encode()\`. Re-marshaling is unavoidable and acceptable here because the PKCS12 encoder produces its own encoding.
31915
31915
 
31916
31916
  ### 42. CRITICAL: Handle Both DER and PEM Input in Certificate Bundle
31917
31917
 
@@ -32671,8 +32671,8 @@ var TEMPLATES = {
32671
32671
  "// File: internal/app/domain/binding.go",
32672
32672
  "// CUSTOMIZE: Replace fields with your target-specific service data.",
32673
32673
  "// Choose the pattern that fits your target:",
32674
- "// SSH-based targets: ServiceType + ServicePort (e.g., Splunk, Apache, Nginx)",
32675
- "// REST API appliances: BindingType + TargetName (e.g., FortiGate, F5, Citrix ADC)",
32674
+ "// SSH-based targets: ServiceType + ServicePort (e.g., web server, app server)",
32675
+ "// REST API appliances: BindingType + TargetName (e.g., load balancer, firewall, ADC)",
32676
32676
  "// ============================================================",
32677
32677
  "",
32678
32678
  "package domain",
@@ -32685,7 +32685,7 @@ var TEMPLATES = {
32685
32685
  '// ServicePort int `json:"servicePort"` // port the service listens on',
32686
32686
  "// }",
32687
32687
  "",
32688
- "// --- Option B: REST API appliance binding (FortiGate pattern) ---",
32688
+ "// --- Option B: REST API appliance binding (network appliance pattern) ---",
32689
32689
  "",
32690
32690
  "// Binding represents how a certificate is used by a target service.",
32691
32691
  '// BindingType identifies the service category (e.g., "admin-https", "ssl-vpn",',
@@ -32702,17 +32702,49 @@ var TEMPLATES = {
32702
32702
  "// ============================================================",
32703
32703
  "// File: internal/app/domain/certificate_bundle.go",
32704
32704
  "// DO NOT MODIFY: This is standard across all connectors.",
32705
+ "//",
32706
+ '// IMPORTANT: The manifest schema uses "contentEncoding": "base64" and',
32707
+ '// "x-encrypted-base64": true on the privateKey field.',
32708
+ "// Venafi Cloud sends certificateBundle fields as base64-encoded DER.",
32709
+ "//",
32710
+ "// Choose ONE option below:",
32711
+ "//",
32712
+ "// Option A (RECOMMENDED for REST API connectors):",
32713
+ "// Use string fields and explicitly base64.StdEncoding.DecodeString() each field.",
32714
+ "// This is the most explicit pattern and proven across multiple connector builds.",
32715
+ "// After decoding, you have DER bytes \u2014 convert to PEM with pem.EncodeToMemory().",
32716
+ "//",
32717
+ "// Option B (works for SSH connectors):",
32718
+ "// Use []byte fields \u2014 Go's JSON unmarshaler auto-decodes base64 into []byte.",
32719
+ "// After unmarshaling, fields already contain DER bytes.",
32720
+ "//",
32721
+ "// Both options produce the same result: DER-encoded binary data.",
32705
32722
  "// ============================================================",
32706
32723
  "",
32707
32724
  "package domain",
32708
32725
  "",
32709
32726
  "// CertificateBundle represents the certificate data sent by Venafi for provisioning.",
32727
+ "//",
32728
+ "// Option A: string fields (RECOMMENDED \u2014 explicit base64 decode)",
32729
+ "// certDER, _ := base64.StdEncoding.DecodeString(bundle.Certificate)",
32730
+ "// keyDER, _ := base64.StdEncoding.DecodeString(bundle.PrivateKey)",
32731
+ "// for _, chainEntry := range bundle.CertificateChain {",
32732
+ "// chainDER, _ := base64.StdEncoding.DecodeString(chainEntry)",
32733
+ '// chainPEM = append(chainPEM, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: chainDER})...)',
32734
+ "// }",
32710
32735
  "type CertificateBundle struct {",
32711
- ' Certificate []byte `json:"certificate"`',
32712
- ' PrivateKey []byte `json:"privateKey"`',
32713
- ' CertificateChain [][]byte `json:"certificateChain"`',
32736
+ ' Certificate string `json:"certificate"`',
32737
+ ' PrivateKey string `json:"privateKey"`',
32738
+ ' CertificateChain []string `json:"certificateChain"`',
32714
32739
  "}",
32715
32740
  "",
32741
+ "// Option B: []byte fields (Go auto-decodes base64 \u2014 fields contain DER bytes directly)",
32742
+ "// type CertificateBundle struct {",
32743
+ '// Certificate []byte `json:"certificate"`',
32744
+ '// PrivateKey []byte `json:"privateKey"`',
32745
+ '// CertificateChain [][]byte `json:"certificateChain"`',
32746
+ "// }",
32747
+ "",
32716
32748
  "// ============================================================",
32717
32749
  "// File: internal/app/domain/client.go",
32718
32750
  "// This is the same across all SSH-based connectors.",
@@ -32845,6 +32877,16 @@ var TEMPLATES = {
32845
32877
  content: [
32846
32878
  "// TEMPLATE: Discovery request/response types. Standard across all connectors.",
32847
32879
  "// File: internal/app/discovery/types.go",
32880
+ "//",
32881
+ "// CRITICAL RULES:",
32882
+ '// 1. The response top-level key MUST be "messages" \u2014 the platform silently rejects other names.',
32883
+ "// 2. Use VALUE types (not pointers) for Keystore, Binding, and slice elements.",
32884
+ "// Pointers serialize to null when nil, which the platform may reject.",
32885
+ "// 3. Initialize all slices with make() to ensure JSON [] not null.",
32886
+ "// 4. The certificate field must contain a PEM string (not DER, not base64).",
32887
+ "// Always normalize through pem.EncodeToMemory() before returning \u2014 even if",
32888
+ "// the source is already PEM \u2014 to guarantee trailing newline and line wrapping.",
32889
+ '// 5. discoveryPage: null in the response signals "done" to the platform.',
32848
32890
  "",
32849
32891
  "package discovery",
32850
32892
  "",
@@ -32860,11 +32902,10 @@ var TEMPLATES = {
32860
32902
  "type DiscoverCertificatesConfiguration struct {",
32861
32903
  " // DiscoveryTypes lets users toggle which service types to discover.",
32862
32904
  " // For REST API appliances, this filters which binding endpoints are queried.",
32863
- ' // Example values: "admin-https", "ssl-vpn", "ipsec-vpn", "vip", "ssl-inspection"',
32864
32905
  ' // An empty slice means "discover all types".',
32865
32906
  ' DiscoveryTypes []string `json:"discoveryTypes"`',
32866
32907
  ' ExcludeExpiredCertificates bool `json:"excludeExpiredCertificates"`',
32867
- " // Scope field for multi-tenant targets (e.g., VDOM, partition, tenant).",
32908
+ " // Scope field for multi-tenant targets (e.g., partition, tenant, virtual domain).",
32868
32909
  " // Leave blank to auto-discover all scopes.",
32869
32910
  ' Scope string `json:"scope"`',
32870
32911
  "}",
@@ -32878,29 +32919,65 @@ var TEMPLATES = {
32878
32919
  "}",
32879
32920
  "",
32880
32921
  "// DiscoveryPage represents the current pagination state.",
32922
+ '// Return nil to signal "discovery complete" to the platform.',
32923
+ "// Return a non-nil DiscoveryPage to request another page.",
32881
32924
  "type DiscoveryPage struct {",
32882
- ' DiscoveryType *string `json:"discoveryType,omitempty"`',
32883
- ' Paginator string `json:"paginator"`',
32925
+ ' DiscoveryType string `json:"discoveryType"`',
32926
+ ' Paginator string `json:"paginator"`',
32884
32927
  "}",
32885
32928
  "",
32886
32929
  "// DiscoverCertificatesResponse represents the response to a discovery request.",
32930
+ '// CRITICAL: The top-level JSON key MUST be "messages" \u2014 the platform ignores other keys.',
32887
32931
  "type DiscoverCertificatesResponse struct {",
32932
+ ' Messages []DiscoveredCertificate `json:"messages"`',
32888
32933
  ' Page *DiscoveryPage `json:"discoveryPage"`',
32889
- ' Messages []*DiscoveredCertificate `json:"messages"`',
32890
32934
  "}",
32891
32935
  "",
32892
32936
  "// DiscoveredCertificate represents a single certificate found during discovery.",
32937
+ "// CRITICAL: Use VALUE types (not pointers) \u2014 pointers serialize to null which the platform rejects.",
32893
32938
  "type DiscoveredCertificate struct {",
32894
- ' Certificate string `json:"certificate"`',
32895
- ' CertificateChain []string `json:"certificateChain"`',
32896
- ' MachineIdentities []*MachineIdentity `json:"machineIdentities"`',
32939
+ ' Certificate string `json:"certificate"` // PEM string (use pem.EncodeToMemory)',
32940
+ ' CertificateChain []string `json:"certificateChain"` // PEM strings, initialize with make()',
32941
+ ' MachineIdentities []MachineIdentity `json:"machineIdentities"` // VALUE type slice, initialize with make()',
32897
32942
  "}",
32898
32943
  "",
32899
32944
  "// MachineIdentity represents a certificate usage found during discovery.",
32945
+ "// CRITICAL: Use VALUE types for Keystore and Binding \u2014 never pointers.",
32946
+ '// Binding must NEVER be empty \u2014 use a default like "unbound" for certs with no bindings.',
32947
+ "// All x-primaryKey fields must have non-empty values or the identity is silently dropped.",
32900
32948
  "type MachineIdentity struct {",
32901
- ' Keystore *domain.Keystore `json:"keystore"`',
32902
- ' Binding *domain.Binding `json:"binding"`',
32903
- "}"
32949
+ ' Keystore domain.Keystore `json:"keystore"`',
32950
+ ' Binding domain.Binding `json:"binding"`',
32951
+ "}",
32952
+ "",
32953
+ "// ============================================================",
32954
+ "// Example: Building a discovery response",
32955
+ "// ============================================================",
32956
+ "//",
32957
+ "// messages := make([]DiscoveredCertificate, 0) // MUST use make() \u2014 nil serializes as null",
32958
+ "// chainPEMs := make([]string, 0) // same for chains",
32959
+ "//",
32960
+ "// // Always normalize PEM through pem.EncodeToMemory, even if source is already PEM:",
32961
+ "// block, _ := pem.Decode([]byte(rawPEM))",
32962
+ "// normalizedPEM := string(pem.EncodeToMemory(block))",
32963
+ "//",
32964
+ "// // For REST API targets returning base64 DER:",
32965
+ "// derBytes, _ := base64.StdEncoding.DecodeString(apiResponse.Base64)",
32966
+ '// certPEM := string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes}))',
32967
+ "//",
32968
+ "// messages = append(messages, DiscoveredCertificate{",
32969
+ "// Certificate: normalizedPEM,",
32970
+ "// CertificateChain: chainPEMs,",
32971
+ "// MachineIdentities: []MachineIdentity{{",
32972
+ '// Keystore: domain.Keystore{CertificateName: "my-cert"},',
32973
+ '// Binding: domain.Binding{BindingType: "unbound"}, // never empty',
32974
+ "// }},",
32975
+ "// })",
32976
+ "//",
32977
+ "// return c.JSON(http.StatusOK, DiscoverCertificatesResponse{",
32978
+ "// Messages: messages,",
32979
+ "// Page: nil, // nil = done; non-nil = call again",
32980
+ "// })"
32904
32981
  ].join("\n"),
32905
32982
  customizable: true
32906
32983
  },
@@ -32910,12 +32987,14 @@ var TEMPLATES = {
32910
32987
  targetPath: "internal/app/<target>/install_helpers.go",
32911
32988
  content: [
32912
32989
  "// TEMPLATE: Helper functions for certificate installation.",
32913
- "// These are proven patterns from the Splunk connector that work correctly.",
32990
+ "// These are proven patterns from multiple connector builds.",
32914
32991
  "// Include these in your install.go or a helpers.go file.",
32915
32992
  "",
32916
32993
  "package <target>",
32917
32994
  "",
32918
32995
  "import (",
32996
+ ' "crypto/ecdsa"',
32997
+ ' "crypto/rsa"',
32919
32998
  ' "crypto/x509"',
32920
32999
  ' "encoding/pem"',
32921
33000
  ' "fmt"',
@@ -32925,27 +33004,52 @@ var TEMPLATES = {
32925
33004
  ' "go.uber.org/zap"',
32926
33005
  ")",
32927
33006
  "",
32928
- "// privateKeyPEMType detects the private key type and returns the correct PEM header.",
32929
- '// IMPORTANT: Do NOT hardcode "RSA PRIVATE KEY" - Venafi may issue EC keys.',
32930
- "func privateKeyPEMType(keyBytes []byte) string {",
32931
- " // Try PKCS#1 RSA",
32932
- " if _, err := x509.ParsePKCS1PrivateKey(keyBytes); err == nil {",
32933
- ' return "RSA PRIVATE KEY"',
33007
+ "// encodeKeyToPEM detects the key type and encodes to PEM with the correct header.",
33008
+ "//",
33009
+ "// CRITICAL: When PKCS8 wraps a key, re-marshal to the native format.",
33010
+ "// Some targets validate that the DER encoding matches the PEM header.",
33011
+ '// PKCS8 DER with "RSA PRIVATE KEY" header = INVALID (DER is PKCS8, header says PKCS1).',
33012
+ '// PKCS1 DER with "RSA PRIVATE KEY" header = VALID (both agree on format).',
33013
+ "//",
33014
+ '// Do NOT hardcode "RSA PRIVATE KEY" \u2014 Venafi may issue EC or other key types.',
33015
+ "func encodeKeyToPEM(keyDER []byte) []byte {",
33016
+ " // Try PKCS8 first (most common from Venafi)",
33017
+ " key, err := x509.ParsePKCS8PrivateKey(keyDER)",
33018
+ " if err == nil {",
33019
+ " switch k := key.(type) {",
33020
+ " case *rsa.PrivateKey:",
33021
+ ' // Re-marshal to PKCS1 so DER matches "RSA PRIVATE KEY" header',
33022
+ " pkcs1DER := x509.MarshalPKCS1PrivateKey(k)",
33023
+ ' return pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: pkcs1DER})',
33024
+ " case *ecdsa.PrivateKey:",
33025
+ " ecDER, ecErr := x509.MarshalECPrivateKey(k)",
33026
+ " if ecErr == nil {",
33027
+ ' return pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: ecDER})',
33028
+ " }",
33029
+ ' return pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyDER})',
33030
+ " default:",
33031
+ ' return pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyDER})',
33032
+ " }",
33033
+ " }",
33034
+ "",
33035
+ " // Try RSA PKCS1",
33036
+ " if _, err := x509.ParsePKCS1PrivateKey(keyDER); err == nil {",
33037
+ ' return pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: keyDER})',
32934
33038
  " }",
33039
+ "",
32935
33040
  " // Try EC",
32936
- " if _, err := x509.ParseECPrivateKey(keyBytes); err == nil {",
32937
- ' return "EC PRIVATE KEY"',
32938
- " }",
32939
- " // Try PKCS#8 (wraps either RSA or EC)",
32940
- " if _, err := x509.ParsePKCS8PrivateKey(keyBytes); err == nil {",
32941
- ' return "PRIVATE KEY"',
33041
+ " if _, err := x509.ParseECPrivateKey(keyDER); err == nil {",
33042
+ ' return pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: keyDER})',
32942
33043
  " }",
32943
- " // Default to PKCS#8 header (most compatible)",
32944
- ' return "PRIVATE KEY"',
33044
+ "",
33045
+ " // Fallback \u2014 generic PRIVATE KEY header",
33046
+ ' return pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyDER})',
32945
33047
  "}",
32946
33048
  "",
32947
33049
  "// buildCombinedPEM creates a combined PEM file with cert + chain + key.",
32948
33050
  "// Used when the target expects all components in a single file.",
33051
+ "// NOTE: This assumes CertificateBundle uses []byte fields (Option B).",
33052
+ "// If using string fields (Option A), decode from base64 first.",
32949
33053
  "func buildCombinedPEM(bundle *domain.CertificateBundle) ([]byte, error) {",
32950
33054
  " var pemContent strings.Builder",
32951
33055
  "",
@@ -32963,12 +33067,8 @@ var TEMPLATES = {
32963
33067
  " })))",
32964
33068
  " }",
32965
33069
  "",
32966
- " // Private key (with correct type detection)",
32967
- " keyType := privateKeyPEMType(bundle.PrivateKey)",
32968
- " pemContent.WriteString(string(pem.EncodeToMemory(&pem.Block{",
32969
- " Type: keyType,",
32970
- " Bytes: bundle.PrivateKey,",
32971
- " })))",
33070
+ " // Private key (with PKCS8 re-marshaling)",
33071
+ " pemContent.WriteString(string(encodeKeyToPEM(bundle.PrivateKey)))",
32972
33072
  "",
32973
33073
  " return []byte(pemContent.String()), nil",
32974
33074
  "}",
@@ -32996,12 +33096,9 @@ var TEMPLATES = {
32996
33096
  "}",
32997
33097
  "",
32998
33098
  "// buildKeyPEM creates a PEM file with just the private key.",
33099
+ "// Re-marshals PKCS8 keys to native format for PEM header correctness.",
32999
33100
  "func buildKeyPEM(bundle *domain.CertificateBundle) []byte {",
33000
- " keyType := privateKeyPEMType(bundle.PrivateKey)",
33001
- " return pem.EncodeToMemory(&pem.Block{",
33002
- " Type: keyType,",
33003
- " Bytes: bundle.PrivateKey,",
33004
- " })",
33101
+ " return encodeKeyToPEM(bundle.PrivateKey)",
33005
33102
  "}",
33006
33103
  "",
33007
33104
  "// backupFile creates a backup of an existing file before overwriting.",
@@ -33247,7 +33344,7 @@ func configureLogger() (*zap.Logger, error) {
33247
33344
  },
33248
33345
  "rest-client.go": {
33249
33346
  name: "rest-client.go",
33250
- description: "REST API client (handler) pattern for network appliance connectors (FortiGate, F5, Citrix ADC). Uses go-resty with multi-auth support (API token, session, PKI).",
33347
+ description: "REST API client (handler) pattern for network appliance connectors (firewalls, load balancers, ADCs). Uses go-resty with multi-auth support (API token, session, PKI).",
33251
33348
  targetPath: "internal/app/<target>/handler.go",
33252
33349
  content: [
33253
33350
  "package <target>",
@@ -33384,7 +33481,7 @@ func configureLogger() (*zap.Logger, error) {
33384
33481
  },
33385
33482
  "rest-api-service-pattern.go": {
33386
33483
  name: "rest-api-service-pattern.go",
33387
- description: "3-service decomposition pattern for REST API connectors (PAN Panorama architecture). Connection, Provisioning, and Discovery as separate services.",
33484
+ description: "3-service decomposition pattern for REST API connectors. Connection, Provisioning, and Discovery as separate services.",
33388
33485
  targetPath: "internal/app/<target>/<target>.go",
33389
33486
  content: [
33390
33487
  "package <target>",
@@ -33557,14 +33654,17 @@ ${content}`;
33557
33654
  function getMachineEndpoints(args) {
33558
33655
  const webTemplate = TEMPLATES["machine-web.go"];
33559
33656
  const testConnTemplate = TEMPLATES["test-connection.go"];
33657
+ const discoveryTypesTemplate = TEMPLATES["discovery-types.go"];
33560
33658
  let webContent = webTemplate.content;
33561
33659
  let testConnContent = testConnTemplate.content;
33660
+ let discoveryTypesContent = discoveryTypesTemplate.content;
33562
33661
  if (args?.connectorName || args?.modulePath || args?.targetPackage) {
33563
33662
  const cn = args?.connectorName || "<CONNECTOR_NAME>";
33564
33663
  const mp = args?.modulePath || "<CONNECTOR_MODULE>";
33565
33664
  const tp = args?.targetPackage || "<target>";
33566
33665
  webContent = substituteTemplate(webContent, cn, mp, tp);
33567
33666
  testConnContent = substituteTemplate(testConnContent, cn, mp, tp);
33667
+ discoveryTypesContent = substituteTemplate(discoveryTypesContent, cn, mp, tp);
33568
33668
  }
33569
33669
  return `# Machine Connector Endpoint Templates
33570
33670
 
@@ -33576,9 +33676,9 @@ A machine connector implements 5 endpoints (plus /healthz). All are POST endpoin
33576
33676
  |---|---|---|
33577
33677
  | POST /v1/testconnection | HandleTestConnection | Validate connectivity, detect target software |
33578
33678
  | POST /v1/discovercertificates | HandleDiscoverCertificates | Find certificates, return with keystore/binding metadata |
33579
- | POST /v1/installcertificatebundle | HandleInstallCertificateBundle | Write cert+chain+key files to target |
33580
- | POST /v1/configureinstallationendpoint | HandleConfigureInstallationEndpoint | Restart/reload target service |
33581
- | POST /v1/gettargetconfiguration | HandleGetTargetConfiguration | Return target info (stub is OK) |
33679
+ | POST /v1/installcertificatebundle | HandleInstallCertificateBundle | Install cert+chain+key on target |
33680
+ | POST /v1/configureinstallationendpoint | HandleConfigureInstallationEndpoint | Configure service binding for the installed cert |
33681
+ | POST /v1/gettargetconfiguration | HandleGetTargetConfiguration | Return target info for UI dropdowns (stub is OK) |
33582
33682
  | GET /healthz | (inline) | Kubernetes liveness probe |
33583
33683
 
33584
33684
  ## WebhookService Interface (web.go)
@@ -33597,17 +33697,172 @@ This is a complete test connection handler with SSH validation. Customize step 4
33597
33697
  ${testConnContent}
33598
33698
  \`\`\`
33599
33699
 
33700
+ ## Discovery Response Types (discovery/types.go)
33701
+
33702
+ These types define the discovery request/response structures. The response format is critical \u2014 incorrect types cause silent failures.
33703
+
33704
+ \`\`\`go
33705
+ ${discoveryTypesContent}
33706
+ \`\`\`
33707
+
33708
+ ## Install Certificate Bundle Handler
33709
+
33710
+ CRITICAL: The response MUST wrap the updated keystore in \`{"keystore": {...}}\`.
33711
+ Returning the keystore directly (without the wrapper) causes: \`"missing json schema in plugin manifest for entity keystoreId"\`.
33712
+
33713
+ \`\`\`go
33714
+ // File: internal/app/<target>/install_certificate_bundle.go
33715
+
33716
+ // InstallCertificateBundleRequest contains the request for installing a certificate.
33717
+ type InstallCertificateBundleRequest struct {
33718
+ Connection *domain.Connection \`json:"connection"\`
33719
+ Keystore *domain.Keystore \`json:"keystore"\`
33720
+ CertificateBundle *domain.CertificateBundle \`json:"certificateBundle"\`
33721
+ }
33722
+
33723
+ // InstallCertificateBundleResponse wraps the updated keystore.
33724
+ // CRITICAL: Response MUST be {"keystore": {...}} \u2014 platform validates against manifest schema.
33725
+ type InstallCertificateBundleResponse struct {
33726
+ Keystore domain.Keystore \`json:"keystore"\`
33727
+ }
33728
+
33729
+ func (svc *WebhookServiceImpl) HandleInstallCertificateBundle(c echo.Context) error {
33730
+ zap.L().Info("installCertificateBundle workflow activity started")
33731
+
33732
+ // Step 1: Parse request
33733
+ req := InstallCertificateBundleRequest{}
33734
+ if err := c.Bind(&req); err != nil {
33735
+ zap.L().Error("install step 1 failed: invalid request", zap.Error(err))
33736
+ return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
33737
+ }
33738
+
33739
+ // Step 2: Connect to target
33740
+ client := svc.ClientServices.NewClient(req.Connection)
33741
+ if err := svc.ClientServices.Connect(client); err != nil {
33742
+ return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
33743
+ }
33744
+ defer svc.ClientServices.Close(client)
33745
+
33746
+ // Step 3: Install the certificate
33747
+ // CUSTOMIZE: Implement your target-specific installation logic here.
33748
+ // For SSH targets: write PEM files to disk, set permissions.
33749
+ // For REST API targets: upload via API (PKCS12, PEM, or target-specific format).
33750
+ //
33751
+ // Certificate bundle fields are base64-encoded DER (if using string fields):
33752
+ // certDER, _ := base64.StdEncoding.DecodeString(req.CertificateBundle.Certificate)
33753
+ // keyDER, _ := base64.StdEncoding.DecodeString(req.CertificateBundle.PrivateKey)
33754
+ // for _, entry := range req.CertificateBundle.CertificateChain {
33755
+ // chainDER, _ := base64.StdEncoding.DecodeString(entry)
33756
+ // }
33757
+ //
33758
+ // If using []byte fields, Go auto-decodes \u2014 fields already contain DER bytes.
33759
+
33760
+ updatedKeystore := *req.Keystore // Copy and update as needed
33761
+
33762
+ zap.L().Info("installCertificateBundle completed",
33763
+ zap.Any("keystore", updatedKeystore),
33764
+ )
33765
+
33766
+ // CRITICAL: Wrap in {"keystore": {...}} \u2014 do NOT return the keystore directly
33767
+ return c.JSON(http.StatusOK, InstallCertificateBundleResponse{Keystore: updatedKeystore})
33768
+ }
33769
+ \`\`\`
33770
+
33771
+ ## Configure Installation Endpoint Handler
33772
+
33773
+ CRITICAL: The response MUST wrap the binding in \`{"binding": {...}}\`.
33774
+
33775
+ \`\`\`go
33776
+ // File: internal/app/<target>/configure_installation_endpoint.go
33777
+
33778
+ // ConfigureInstallationEndpointRequest contains the request for configuring the binding.
33779
+ type ConfigureInstallationEndpointRequest struct {
33780
+ Connection *domain.Connection \`json:"connection"\`
33781
+ Keystore *domain.Keystore \`json:"keystore"\`
33782
+ Binding *domain.Binding \`json:"binding"\`
33783
+ }
33784
+
33785
+ // ConfigureInstallationEndpointResponse wraps the binding.
33786
+ // CRITICAL: Response MUST be {"binding": {...}} \u2014 platform validates against manifest schema.
33787
+ type ConfigureInstallationEndpointResponse struct {
33788
+ Binding domain.Binding \`json:"binding"\`
33789
+ }
33790
+
33791
+ func (svc *WebhookServiceImpl) HandleConfigureInstallationEndpoint(c echo.Context) error {
33792
+ zap.L().Info("configureInstallationEndpoint workflow activity started")
33793
+
33794
+ // Step 1: Parse request
33795
+ req := ConfigureInstallationEndpointRequest{}
33796
+ if err := c.Bind(&req); err != nil {
33797
+ zap.L().Error("configure step 1 failed: invalid request", zap.Error(err))
33798
+ return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
33799
+ }
33800
+
33801
+ // Step 2: Connect to target
33802
+ client := svc.ClientServices.NewClient(req.Connection)
33803
+ if err := svc.ClientServices.Connect(client); err != nil {
33804
+ return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
33805
+ }
33806
+ defer svc.ClientServices.Close(client)
33807
+
33808
+ // Step 3: Configure the binding
33809
+ // CUSTOMIZE: Implement your target-specific binding logic here.
33810
+ // For SSH targets: restart/reload the service (systemctl restart, service reload, etc.).
33811
+ // For REST API targets: update the binding via API (assign cert to VIP, profile, etc.).
33812
+
33813
+ zap.L().Info("configureInstallationEndpoint completed",
33814
+ zap.Any("binding", req.Binding),
33815
+ )
33816
+
33817
+ // CRITICAL: Wrap in {"binding": {...}} \u2014 do NOT return the binding directly
33818
+ return c.JSON(http.StatusOK, ConfigureInstallationEndpointResponse{Binding: *req.Binding})
33819
+ }
33820
+ \`\`\`
33821
+
33822
+ ## Get Target Configuration Handler
33823
+
33824
+ Returns target metadata for UI dropdowns. Can be a stub initially.
33825
+
33826
+ \`\`\`go
33827
+ // File: internal/app/<target>/get_target_configuration.go
33828
+
33829
+ type GetTargetConfigurationRequest struct {
33830
+ Connection *domain.Connection \`json:"connection"\`
33831
+ }
33832
+
33833
+ func (svc *WebhookServiceImpl) HandleGetTargetConfiguration(c echo.Context) error {
33834
+ zap.L().Info("getTargetConfiguration workflow activity started")
33835
+
33836
+ req := GetTargetConfigurationRequest{}
33837
+ if err := c.Bind(&req); err != nil {
33838
+ return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
33839
+ }
33840
+
33841
+ // CUSTOMIZE: Return target configuration for UI dropdowns.
33842
+ // Fields returned here populate x-targetConfigurationRef dropdowns in the manifest.
33843
+ // Return an empty object if no dynamic configuration is needed.
33844
+ return c.JSON(http.StatusOK, map[string]interface{}{})
33845
+ }
33846
+ \`\`\`
33847
+
33600
33848
  ## Handler Pattern (Use for ALL Endpoints)
33601
33849
 
33602
33850
  Every handler follows these steps:
33603
33851
  1. Parse and validate the request
33604
33852
  2. Establish connection (with defer Close)
33605
33853
  3. Do the actual work
33606
- 4. Return response
33854
+ 4. Return response as JSON
33855
+
33856
+ ### Critical Response Rules
33857
+ - **testConnection**: Return \`c.JSON(http.StatusOK, TestConnectionResponse{Result: true})\`
33858
+ - **installCertificateBundle**: Return \`c.JSON(http.StatusOK, InstallCertificateBundleResponse{Keystore: ...})\` \u2014 MUST wrap in \`{"keystore": ...}\`
33859
+ - **configureInstallationEndpoint**: Return \`c.JSON(http.StatusOK, ConfigureInstallationEndpointResponse{Binding: ...})\` \u2014 MUST wrap in \`{"binding": ...}\`
33860
+ - **discoverCertificates**: Return \`c.JSON(http.StatusOK, DiscoverCertificatesResponse{Messages: ...})\` \u2014 MUST use \`"messages"\` key
33861
+ - **ALL responses must be JSON** \u2014 the vSatellite unmarshals every webhook response as JSON. Returning plain text (\`c.String(200, "OK")\`) causes: \`"invalid character 'O' looking for beginning of value"\`
33862
+ - Return HTTP 400 for ALL errors (not 500)
33607
33863
 
33608
- Key rules:
33864
+ ### Other Rules
33609
33865
  - Always defer Close() immediately after Connect()
33610
- - Return HTTP 400 for ALL errors (not 500)
33611
33866
  - Log every step with structured fields
33612
33867
  - Each endpoint is stateless \u2014 connect, do work, disconnect
33613
33868
  `;
@@ -33615,7 +33870,7 @@ Key rules:
33615
33870
  function getMachineBestPractices() {
33616
33871
  return `# Machine Connector Best Practices
33617
33872
 
33618
- These best practices are distilled from building multiple machine connectors: Splunk Enterprise (SSH), FortiGate (REST API), IBM APIC (REST API), and DataPower (REST API). They cover what worked, what failed, and what to avoid.
33873
+ These best practices are distilled from building multiple machine connectors across SSH-based and REST API-based targets. They cover what worked, what failed, and what to avoid.
33619
33874
 
33620
33875
  ${LESSONS_LEARNED}
33621
33876
 
@@ -33641,7 +33896,7 @@ ${LESSONS_LEARNED}
33641
33896
  18. **Retired certificates in Venafi silently block discovery** \u2014 query certificateStatus for RETIRED records when MI count is low
33642
33897
  19. **Null-safe JSON arrays** \u2014 initialize slices to \`[]string{}\` not nil, as \`null\` in JSON causes silent discovery failures
33643
33898
  20. **discoveryTypes label MUST use "title"** \u2014 \`x-labelLocalizationKey\` does NOT work for array discovery fields (shows raw key text). Use \`"title": "Certificate Types"\` on the property and \`"title"\` on each oneOf item. Boolean discovery fields CAN use \`x-labelLocalizationKey\` with the \`"discovery.xxx"\` pattern
33644
- 21. **Discovery type checkboxes should match the target platform's certificate categories** \u2014 ask "how would an admin describe their certificates?" (FortiGate: by service type; APIC: by usage role). Don't use generic/internal labels
33899
+ 21. **Discovery type checkboxes should match the target platform's certificate categories** \u2014 ask "how would an admin describe their certificates?" (network appliance: by service type; API platform: by usage role). Don't use generic/internal labels
33645
33900
  22. **x-labelLocalizationKey must be two-level dot paths** \u2014 \`"fieldName.label"\` works, but \`"section.fieldName.label"\` (three levels) does NOT resolve and shows raw key text in the UI. Use flat prefixes like \`"dpDomain.label"\` instead of \`"keystore.domain.label"\`
33646
33901
  23. **certificateBundle.certificateChain is an ARRAY** \u2014 Venafi Cloud sends it as \`["base64cert1", "base64cert2"]\`, not a single string. Manifest must declare \`{ "type": "array", "items": { "contentEncoding": "base64", "type": "string" } }\`. Go struct uses \`[][]byte\` (auto base64 decode) or \`[]string\` (manual decode)
33647
33902
  24. **Certificate chain ordering is undocumented** \u2014 Venafi Cloud sends the chain array but the ordering guarantee is not documented. Pass through in received order and note this assumption. Targets with strict ordering requirements may need issuer/subject-based re-sorting
@@ -33666,7 +33921,7 @@ function getRESTClientPattern(args) {
33666
33921
  }
33667
33922
  return `# REST API Client Pattern for Machine Connectors
33668
33923
 
33669
- The REST API client pattern is for network appliance connectors (FortiGate, F5, Citrix ADC, PAN Panorama) that communicate via HTTPS REST API instead of SSH.
33924
+ The REST API client pattern is for network appliance connectors (firewalls, load balancers, ADCs, management platforms) that communicate via HTTPS REST API instead of SSH.
33670
33925
 
33671
33926
  ## When to Use This Pattern
33672
33927
  - Target is a network appliance (firewall, load balancer, ADC)
@@ -33681,7 +33936,7 @@ The REST API client pattern is for network appliance connectors (FortiGate, F5,
33681
33936
  - No sudo, no file system ops \u2014 everything is API calls
33682
33937
  - Each request creates a fresh handler (stateless per endpoint call)
33683
33938
 
33684
- ## Architecture: 3-Service Decomposition (PAN Panorama Pattern)
33939
+ ## Architecture: 3-Service Decomposition
33685
33940
 
33686
33941
  Instead of a single \`ClientServices\` interface, REST API connectors use three services:
33687
33942
  - **ConnectionService** \u2014 handles \`testConnection\`
@@ -33711,7 +33966,7 @@ ${appContent}
33711
33966
  ## Network Appliance Patterns
33712
33967
 
33713
33968
  ### Certificate Cannot Be Updated In-Place
33714
- Many appliances (FortiGate, PAN) cannot update a certificate's content in-place. The replacement pattern is:
33969
+ Many appliances cannot update a certificate's content in-place. The replacement pattern is:
33715
33970
  1. Upload new cert with a **different name** (e.g., \`name_YYMonDD_serial\`)
33716
33971
  2. Update all bindings to reference the new name
33717
33972
  3. Delete the old certificate (only if no bindings remain)
@@ -33741,10 +33996,8 @@ Changing the management interface certificate drops the active HTTPS session.
33741
33996
  Catch the connection error and treat it as success.
33742
33997
 
33743
33998
  ## Reference Connectors
33744
- - **PAN Panorama** \u2014 cleanest architecture, 3-service decomposition, config locking
33745
- - **Citrix ADC** \u2014 NITRO REST API, partition switching, SNI handling, comprehensive tests
33746
- - **F5** \u2014 simpler flat structure, go-bigip SDK, file upload pattern
33747
33999
  - **VMware AVI** (github.com/Venafi/vmware-avi-connector) \u2014 public REST API reference
34000
+ - Reference connectors using 3-service decomposition, partition switching, and SNI handling are available internally
33748
34001
  `;
33749
34002
  }
33750
34003
  function getSSHClientPattern(args) {
@@ -33857,7 +34110,7 @@ This means: no shared state between calls. Each call connects, does work, discon
33857
34110
  - \`get_machine_manifest\` \u2014 Get the machine connector manifest.json template with all sections explained
33858
34111
  - \`get_machine_domain_types\` \u2014 Get Go domain type templates (Connection, Keystore, Binding, CertificateBundle, Client)
33859
34112
  - \`get_machine_endpoints\` \u2014 Get handler/service interface templates for all 5 machine endpoints
33860
- - \`get_machine_best_practices\` \u2014 Get lessons learned and best practices from building the Splunk connector
34113
+ - \`get_machine_best_practices\` \u2014 Get lessons learned and best practices from building reference connectors
33861
34114
  - \`get_ssh_client_pattern\` \u2014 Get the SSH client abstraction code with sudo, file I/O, and command execution
33862
34115
 
33863
34116
  ## What I Need From You
@@ -33889,7 +34142,7 @@ server.resource("machine-blueprint", "venafi://connector-machine/blueprint", {
33889
34142
  ]
33890
34143
  }));
33891
34144
  server.resource("lessons-learned", "venafi://connector-machine/lessons-learned", {
33892
- description: "What worked, what failed, and mistakes to avoid \u2014 from building the Splunk SSH connector. Covers SSH patterns, discovery challenges, certificate format issues, and more.",
34145
+ description: "What worked, what failed, and mistakes to avoid \u2014 from building multiple machine connectors. Covers SSH and REST API patterns, discovery challenges, certificate format issues, and more.",
33893
34146
  mimeType: "text/markdown"
33894
34147
  }, async () => ({
33895
34148
  contents: [
@@ -33944,7 +34197,7 @@ server.tool("get_machine_endpoints", "Return handler and service interface templ
33944
34197
  }
33945
34198
  ]
33946
34199
  }));
33947
- server.tool("get_machine_best_practices", "Return lessons learned and best practices from building the Splunk SSH connector. Covers: what worked (Avi reference, logging, interfaces, DI), mistakes (DER vs PEM, key types, pagination, error handling), and things that need improvement (unit tests, integration tests, error context, timeouts).", {}, async () => ({
34200
+ server.tool("get_machine_best_practices", "Return lessons learned and best practices from building multiple machine connectors (SSH and REST API). Covers: what worked (Avi reference, logging, interfaces, DI), mistakes (DER vs PEM, key types, pagination, error handling, response format), and things that need improvement (unit tests, integration tests, error context, timeouts).", {}, async () => ({
33948
34201
  content: [
33949
34202
  {
33950
34203
  type: "text",
@@ -33964,7 +34217,7 @@ server.tool("get_ssh_client_pattern", "Return the SSH client abstraction code fo
33964
34217
  }
33965
34218
  ]
33966
34219
  }));
33967
- server.tool("get_rest_client_pattern", "Return the REST API client abstraction code for network appliance machine connectors (FortiGate, F5, Citrix ADC, PAN Panorama). Includes the Handler interface with multi-auth support (API token, username/password, PKI client cert), 3-service decomposition (ConnectionService, ProvisioningService, DiscoveryService), and uber/fx DI wiring. Use this instead of get_ssh_client_pattern when the target is a network appliance with a REST API.", {
34220
+ server.tool("get_rest_client_pattern", "Return the REST API client abstraction code for network appliance machine connectors (firewalls, load balancers, ADCs, management platforms). Includes the Handler interface with multi-auth support (API token, username/password, PKI client cert), 3-service decomposition (ConnectionService, ProvisioningService, DiscoveryService), and uber/fx DI wiring. Use this instead of get_ssh_client_pattern when the target is a network appliance with a REST API.", {
33968
34221
  connectorName: external_exports3.string().optional().describe("Connector name (e.g., 'fortigate-connector'). Replaces <CONNECTOR_NAME>."),
33969
34222
  modulePath: external_exports3.string().optional().describe("Go module path (e.g., 'github.com/venafi/fortigate-connector'). Replaces <CONNECTOR_MODULE>."),
33970
34223
  targetPackage: external_exports3.string().optional().describe("Target Go package name (e.g., 'fortigate'). Replaces <target>.")
@@ -33977,7 +34230,7 @@ server.tool("get_rest_client_pattern", "Return the REST API client abstraction c
33977
34230
  ]
33978
34231
  }));
33979
34232
  server.prompt("new_machine_connector", "Start building a new Venafi machine connector. Provides all context, tools, and instructions needed to begin building a machine connector for a specific target software.", {
33980
- targetSoftware: external_exports3.string().describe("The target software (e.g., 'Apache HTTP Server', 'Nginx', 'HAProxy', 'Splunk')"),
34233
+ targetSoftware: external_exports3.string().describe("The target software (e.g., 'Apache HTTP Server', 'Nginx', 'HAProxy', 'network appliance')"),
33981
34234
  connectionMethod: external_exports3.string().optional().default("ssh").describe("Connection method: 'ssh' or 'api'")
33982
34235
  }, async (args) => ({
33983
34236
  messages: [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "venafi-connector-machine",
3
- "version": "2.0.1",
3
+ "version": "2.2.0",
4
4
  "description": "MCP server providing machine connector-specific knowledge, templates, and tools for building Venafi machine connectors",
5
5
  "main": "bundle.mjs",
6
6
  "type": "module",