Skip to content

Commit

Permalink
Add support for encoding/decoding cert payloads
Browse files Browse the repository at this point in the history
OVERVIEW

Add support for generating a certificate metadata payload in JSON
format from a specified metadata payload format version.

Add support for support for decoding a given (valid) certificate
metadata payload. The format is automatically detected from a list of
valid format versions.

The intent is to support all stable format versions indefinitely.

As of this commit / PR, format 0 is still under active development.
This format version is an "unstable" metadata format and is not
covered by this goal; format version 0 is subject to change often as
development continues. Format version 1 is implemented at this time as
a stub version for testing purposes; once stable the plan is to
promote version 0 content as the initial version 1.

CHANGES

Primary changes:

- add support for generating a JSON payload from a specified metadata
  payload format version
  - this can be generated by calling the `Encode` function from a
    specific format version or by calling the top-level `Encode`
    function and specifying a valid format version number (e.g., `0`
    or `1`)
- add support for decoding a given (valid) certificate metadata
  payload
  - the intent is to support decoding any given payload matching the
    set of supported format versions (e.g., `0`, `1`)
  - the caller provides an instance of a specific format version of
    the certificate metadata payload and the `Decode` function for
    that format version is used
  - once a format version is stable, the intent is to support creating
    and decoding it using this library indefinitely
    - this should allow the sysadmin using the `check_cert` plugin to
      specify what version of the payload format they wish to create
    - this should allow the sysadmin using a reporting tool to consume
      a certificate metadata payload generated by the `check_cert`
      plugin in the same fixed version as the one they asked the
      `check_cert` plugin to create
    - this process should continue to work as-is until the sysadmin
      decides to explicitly change the certificate metadata payload
      format version they're working with; updating this dependency
      should not break payload generation or consumption

Other changes:

- add identification of misordered certificate chains
- add example "test" to illustrate library usage
  - initial example uses format 0; the plan is to update the example
    once format 1 is released/stable
- documentation updates
- general refactoring work

REFERENCES

- #19
- #31
- #46
- atc0005/check-cert#1004
  • Loading branch information
atc0005 committed Nov 23, 2024
1 parent c4d4403 commit df9f195
Show file tree
Hide file tree
Showing 30 changed files with 3,288 additions and 256 deletions.
71 changes: 66 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<!-- omit in toc -->
# cert-payload

Support for managing embedded JSON payloads generated by a plugin from the
atc0005/check-cert project
Support for encoding and decoding certificate metadata payloads associated
with the `check_cert` plugin from the `atc0005/check-cert` project.

[![Latest Release](https://img.shields.io/github/release/atc0005/cert-payload.svg?style=flat-square)](https://github.com/atc0005/cert-payload/releases/latest)
[![Go Reference](https://pkg.go.dev/badge/github.com/atc0005/cert-payload.svg)](https://pkg.go.dev/github.com/atc0005/cert-payload)
Expand All @@ -15,17 +15,31 @@ atc0005/check-cert project

- [Overview](#overview)
- [Status](#status)
- [Contributions](#contributions)
- [Features](#features)
- [Changelog](#changelog)
- [Examples](#examples)
- [Imports](#imports)
- [Encoding a payload](#encoding-a-payload)
- [Decoding a payload](#decoding-a-payload)
- [License](#license)
- [Used by](#used-by)
- [References](#references)

## Overview

This package provides support and functionality for managing embedded JSON
payloads generated by a plugin from the `atc0005/check-cert` project.
This library provides support and functionality for encoding and decoding
certificate metadata payloads in JSON format.

Using this library, the `check_cert` plugin (from the `atc0005/check-cert`
project) creates fixed format version payloads. Those payloads (using a
different library), are embedded in monitoring plugin output where they can
later be extracted and decoded and then be unmarshalled back into a specific
format version of a native Go type provided by this project.

This library exists to allow the `check_cert` plugin to easily generate
certificate metadata payloads and various other tools to unpack them for
reporting purposes.

## Status

Expand All @@ -34,9 +48,42 @@ change without notice and may break client code that depends on it. You are
encouraged to [vendor](#references) this package if you find it useful until
such time that the API is considered stable.

The specific certificate metadata payload format versions provided by this
project are *intended* to be supported indefinitely once the format is
declared stable. Any breaking changes to a format would be provided by
releasing a new format version with those changes.

## Contributions

This library has a very narrow focus. While PRs may be accepted to resolve
typos, logic errors and enhance documentation, behavioral changes and feature
additions will likely be rejected as out of scope.

## Features

- placeholder
- support for generating a JSON payload from a specified metadata payload
format version
- this can be generated by calling the `Encode` function from a specific
format version or by calling the top-level `Encode` function and
specifying a valid format version number (e.g., `0` or `1`)
- support for decoding a given (valid) certificate metadata payload
- the intent is to support decoding any given payload matching the set of
supported format versions (e.g., `0`, `1`)
- the caller provides an instance of a specific format version of
the certificate metadata payload and the `Decode` function for that
format version is used
- once a format version is stable, the intent is to support creating and
decoding it using this library indefinitely
- this should allow the sysadmin using the `check_cert` plugin to specify
what version of the payload format they wish to create
- this should allow the sysadmin using a reporting tool to consume a
certificate metadata payload generated by the `check_cert` plugin in the
same fixed version as the one they asked the `check_cert` plugin to
create
- this process should continue to work as-is until the sysadmin decides to
explicitly change the certificate metadata payload format version
they're working with; updating this dependency should not break payload
generation or consumption

## Changelog

Expand All @@ -48,6 +95,8 @@ official release is also provided for further review.

## Examples

### Imports

Add this line to your imports like so:

```golang
Expand Down Expand Up @@ -79,6 +128,18 @@ See <https://pkg.go.dev/github.com/atc0005/cert-payload> for specific examples.
See <https://pkg.go.dev/github.com/atc0005/cert-payload?tab=importedby> for
projects that are using this library.

### Encoding a payload

See the `check_cert` monitoring plugin from the `atc0005/check-cert` project
for an example of how a certificate metadata payload is generated and embedded
within plugin output (for later retrieval and parsing).

### Decoding a payload

See the Nagios XI API example in this repo for how to combine using this
library and another library to extract, decode and unmarshal an embedded
payload to a specific format version of a certificate metadata payload.

## License

From the [LICENSE](LICENSE) file:
Expand Down
5 changes: 3 additions & 2 deletions doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
// Licensed under the MIT License. See LICENSE file in the project root for
// full license information.

// Package payload provides support for managing embedded JSON payloads
// generated by the `check_cert` plugin within this project.
// Package payload provides support for encoding and decoding certificate
// metadata payloads associated with the check_cert plugin from the
// atc0005/check-cert project.
//
// See our [GitHub repo]:
//
Expand Down
230 changes: 230 additions & 0 deletions example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
// Copyright 2024 Adam Chalkley
//
// https://github.com/atc0005/cert-payload
//
// Licensed under the MIT License. See LICENSE file in the project root for
// full license information.

package payload_test

import (
"bytes"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"time"

payload "github.com/atc0005/cert-payload"
format0 "github.com/atc0005/cert-payload/format/v0"
)

// Example of parsing a previously retrieved Nagios XI API response (saved to
// a JSON file) from the /nagiosxi/api/v1/objects/servicestatus endpoint,
// extracting and decoding an embedded certificate metadata payload from each
// status entry and then unmarshalling the result into a specific format
// version (in this case format 0).
//
// TODO: Update this example once format version 1 is released.
func Example_extractandDecodePayloadsFromNagiosXIAPI() {
if len(os.Args) < 2 {
fmt.Println("Missing input file")
os.Exit(1)
}

sampleInputFile := os.Args[1]

jsonInput, readErr := os.ReadFile(filepath.Clean(sampleInputFile))
if readErr != nil {
fmt.Println("Failed to read sample input file:", readErr)
os.Exit(1)
}

var serviceStatusResponse ServiceStatusResponse
decodeErr := json.Unmarshal(jsonInput, &serviceStatusResponse)
if decodeErr != nil {
fmt.Println("Failed to decode JSON input:", decodeErr)
os.Exit(1)
}

for i, serviceStatus := range serviceStatusResponse.ServiceStatuses {
fmt.Printf("\n\nProcess service check result %d ...", i)

longServiceOutput := serviceStatus.LongServiceOutput

// We use a pretend nagios.ExtractAndDecodePayload implementation.
unencodedPayload, payloadDecodeErr := ExtractAndDecodePayload(
longServiceOutput,
"",
DefaultASCII85EncodingDelimiterLeft,
DefaultASCII85EncodingDelimiterRight,
)

if payloadDecodeErr != nil {
fmt.Println(" WARNING: Failed to extract and decode payload from original plugin output:", payloadDecodeErr)
// os.Exit(1)
continue // we have some known cases of explicitly excluding payload generation
}

format0Payload := format0.CertChainPayload{}
jsonDecodeErr := payload.Decode(unencodedPayload, &format0Payload)
if jsonDecodeErr != nil {
fmt.Println("Failed to decode JSON payload from original plugin output:", jsonDecodeErr)
os.Exit(1)
}

if !format0Payload.Issues.Confirmed() {
fmt.Print(" Skipping (no cert chain issues detected)")
continue
}

fmt.Printf(
"\nJSON payload for %s (flagged as problematic):\n",
format0Payload.Server,
)

var prettyJSON bytes.Buffer
err := json.Indent(&prettyJSON, []byte(unencodedPayload), "", " ")
if err == nil {
_, _ = fmt.Fprintln(os.Stdout, prettyJSON.String())
}

}

}

// Pulled from go-nagios repo to remove external dependency.
const (
// DefaultASCII85EncodingDelimiterLeft is the left delimiter often used
// with ascii85-encoded data.
DefaultASCII85EncodingDelimiterLeft string = "<~"

// DefaultASCII85EncodingDelimiterRight is the right delimiter often used
// with ascii85-encoded data.
DefaultASCII85EncodingDelimiterRight string = "~>"
)

// ExtractAndDecodePayload is a mockup for nagios.ExtractAndDecodePayload.
func ExtractAndDecodePayload(text string, customRegex string, leftDelimiter string, rightDelimiter string) (string, error) {
_ = text
_ = customRegex
_ = leftDelimiter
_ = rightDelimiter

return "placeholder", nil
}

// BoolString is a boolean value that is represented in JSON API input as a
// string value ("1" or "0").
type BoolString bool

// MarshalJSON implements the json.Marshaler interface. This compliments the
// custom Unmarshaler implementation to handle conversion of Go boolean field
// to JSON API expectations of a "1" or "0" string value.
func (bs BoolString) MarshalJSON() ([]byte, error) {
switch bs {
case true:
return json.Marshal("1")
case false:
return json.Marshal("0")

}

return nil, nil
}

// UnmarshalJSON implements the json.Unmarshaler interface to handle
// converting a string value of "1" or "0" to a native boolean value.
func (bs *BoolString) UnmarshalJSON(data []byte) error {

// Per json.Unmarshaler convention we treat "null" value as a no-op.
str := string(data)
if str == "null" {
return nil
}

// The 1 or 0 value is double-quoted, so we remove those before attempting
// to parse as a boolean value.
str = strings.Trim(str, `"`)

boolValue, err := strconv.ParseBool(str)
if err != nil {
return err
}

*bs = BoolString(boolValue)

return nil
}

// credit: https://romangaranin.net/posts/2021-02-19-json-time-and-golang/

// DateTimeLayout is the time layout format as used by the JSON API.
const DateTimeLayout string = "2006-01-02 15:04:05"

// DateTime is time value as represented in the JSON API input. It uses the
// DateTimeLayout format.
type DateTime time.Time

// String implements the fmt.Stringer interface as a convenience method.
func (dt DateTime) String() string {
return dt.Format(DateTimeLayout)
}

// Format calls (time.Time).Format as a convenience for the caller.
func (dt DateTime) Format(layout string) string {
return time.Time(dt).Format(layout)
}

// MarshalJSON implements the json.Marshaler interface. This compliments the
// custom Unmarshaler implementation to handle conversion of a native Go
// time.Time format to the JSON API expectations of a time value in the
// DateTimeLayout format.
func (dt DateTime) MarshalJSON() ([]byte, error) {
return json.Marshal(time.Time(dt).Format(DateTimeLayout))
}

// UnmarshalJSON implements the json.Unmarshaler interface to handle
// converting a time string from the JSON API to a native Go time.Time value
// using the DateTimeLayout format.
func (dt *DateTime) UnmarshalJSON(data []byte) error {
value := strings.Trim(string(data), `"`) // get rid of "
if value == "" || value == "null" {

// Per json.Unmarshaler convention we treat "null" value as a no-op.
return nil
}

t, err := time.Parse(DateTimeLayout, value) // parse time
if err != nil {
return err
}

*dt = DateTime(t) // set result using the pointer

return nil
}

type ServiceStatus struct {
HostAddress string `json:"host_address"`
HostAlias string `json:"host_alias"`
HostName string `json:"host_name"`
ServiceDescription string `json:"service_description"`
ActiveChecksEnabled BoolString `json:"active_checks_enabled"`
NotificationsEnabled BoolString `json:"notifications_enabled"`
LongServiceOutput string `json:"long_output"`
Notes string `json:"notes"`
StatusUpdateTime DateTime `json:"status_update_time"`
LastCheck DateTime `json:"last_check"`
NextCheck DateTime `json:"next_check"`
LastNotification DateTime `json:"last_notification"`
NextNotification DateTime `json:"next_notification"`
RawPerfData string `json:"perfdata"`
}

type ServiceStatusResponse struct {
RecordCount int `json:"recordcount"`
ServiceStatuses []ServiceStatus `json:"servicestatus"`
}
2 changes: 1 addition & 1 deletion chain-export.go → format/internal/shared/chain-export.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
// Licensed under the MIT License. See LICENSE file in the project root for
// full license information.

package payload
package shared

import (
"bytes"
Expand Down
Loading

0 comments on commit df9f195

Please # to comment.