cap

package module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 11, 2026 License: Apache-2.0 Imports: 25 Imported by: 2

README

cap

OASIS Common Alerting Protocol 1.2 in pure Go: the alert model, reading and validation, XML signatures, area geometry and alert feeds.

Go Reference test Go

It reads CAP 1.2 and also takes CAP 1.1 and 1.0, lists every validation problem instead of stopping at the first, checks national profiles (Canada, the US, India) and the application's own, checks documents against the CAP 1.2 schema's content model, signs alerts and verifies enveloped XML signatures against pinned certificates or CA chains with revocation, converts areas to GeoJSON, and reads and writes the Atom and RSS feeds and EDXL-DE envelopes that carry or link alerts. It was written for SigmaM2M, a oneM2M platform, as the protocol half of its CAP feed ingest, publishing and public warning integrations.

Features

Feature Status
CAP 1.2 read and write yes; XML, and JSON with CAP element names (MarshalJSONCAP, ParseJSON)
CAP 1.1, 1.0, no namespace read as 1.2; Alert.Version records the original
Validation required elements, enumerations, date-time form (strict or lenient), references, identifier characters, scope conditions, polygon, circle, altitude and ceiling; Problem.Essential tells which problems leave an alert unusable; Check adds warnings
Schema check ValidateSchema: element order, occurrence, unknown elements, attributes, simple types and signature placement, from the CAP 1.2 schema's content model; a full XSD engine plugs in as a SchemaValidator
Profiles Canada CAP-CP 0.4 and 1.0 (CAPCP), US IPAWS 1.0 (IPAWS), India's SACHET conventions (India, warnings only, with RepairIndia); your own through the Profile interface; DetectProfiles finds the ones an alert declares
XML signatures enveloped, exclusive C14N, SHA-256, RSA or ECDSA; verification against pinned certificates or a chain to trusted roots (intermediates from KeyInfo or settings, system roots optional), key usage checks, a revocation hook with CRL and OCSP checks built in
Message history History: whether an Update or Cancel references every message of its chain still in effect
Feeds and envelopes Atom (RFC 4287) with embedded or linked alerts, RSS 2.0, EDXL-DE 1.0 and 2.0 (embedded, base64 or linked alerts), a bare alert in; Atom and EDXL-DE 1.0 out
GeoJSON (RFC 7946) areas as a FeatureCollection; circles as 32-segment polygons

Limits, stated plainly:

  • Profiles check one message at a time. The rule that needs the message history (CAP-CP and IPAWS: an Update or Cancel must reference every message of its chain still in effect) is checked by History, which knows only the messages it has been given. CAP-CP location codes are checked for form; CAPCPWithLocations checks them against a list the application loads (Statistics Canada's codes are not included).
  • India has no published CAP profile (NDMA, C-DOT, TEC and TSDSI were searched on 2026-10-11; India adopted CAP 1.2 itself through ITU-T X.1303). India checks the conventions seen in 50 alerts of NDMA's SACHET feed, as warnings, and says so in every finding's rule.
  • The OASIS schema file (cap-v1.2.xsd) is not shipped. OASIS allows it to be copied unmodified with its notice, but no published pure-Go XSD engine is used here; ValidateSchema encodes the schema's content model as facts instead, and an application with an XSD engine can load the OASIS file itself and pass it as a SchemaValidator.
  • WriteEDXLDE writes EDXL-DE 1.0 only; 2.0 is read, not written.
  • OCSPCheck soft-fails by default (an unreachable responder passes); OCSPOptions.Strict makes every certificate's answer required.

Choosing a Go CAP library

Compared from each library's source at the versions named, in October 2026. Check the projects themselves before relying on a row.

this module (v0.2.0) tannerryan/cap v1.1.1 IBM/cap (2019 commit) alerting/go-cap (2018 commit)
Maintained yes yes (tagged 2026-10-03) archived archived
Licence Apache-2.0 BSD-2-Clause Apache-2.0 MIT
Reads CAP 1.2 yes, leniently (Parse) or refusing non-conforming documents (ParseStrict) yes, strictly (refuses unknown elements at parse) yes yes, without namespace checks
Reads CAP 1.1 and 1.0 yes, as 1.2 no, refuses other namespaces 1.1 as a separate type yes, without namespace checks
Validation every problem listed, errors and warnings, profile and rule named; strict or lenient date-times; derefUri, digest, size and ceiling warnings; schema content-model check errors and warnings, each with its clause; derefUri and digest checks no no
Enumerations strings, with the value lists exported typed values strings strings
Canadian profile (CAP-CP) 0.4 (what Canadian feeds carry) and 1.0; event list as a warning 1.0 only no no
US IPAWS profile, India yes; India as SACHET's observed conventions no no no
Own profiles Profile interface and registry no no no
Verifies XML signatures yes, pinned certificates or a chain to trusted roots; CRL and OCSP checks, revocation hook yes, a chain to trusted roots, with a certificate check hook no no
Signs alerts yes (RSA, ECDSA) no no no
Reference chains across messages History no no no
Feeds, envelopes reads Atom, RSS and EDXL-DE 1.0/2.0, writes Atom and EDXL-DE 1.0 no reads the US NWS Atom feed no
JSON CAP element names, through MarshalJSONCAP/ParseJSON typed enums with JSON tags on the types no no
Areas as GeoJSON yes polygon and circle parsing only no polygon and circle types

Both maintained libraries sign or verify through github.com/russellhaering/goxmldsig and github.com/beevik/etree. Our interop tests cross-check the two (see Testing).

Pick this module for most uses: it covers what the others check and adds older CAP versions, feeds, envelopes, the IPAWS and Indian profiles, CAP-CP 0.4, your own profiles, reference chains, revocation and signing. Pick tannerryan/cap when you prefer typed enumeration values (urgency, severity and so on as Go types with their own JSON encoding) to strings; that is a matter of API style, not of what is checked.

Install

go get github.com/cB-Sigma/cap@latest

Requires Go 1.27.1 or later.

Quick start

import "github.com/cB-Sigma/cap"

a, err := cap.Parse(doc) // a CAP 1.1 alert is read as 1.2
if err != nil {
	return err
}
fmt.Println(a.Version, a.Identifier, a.MostSevere())
if err := a.Validate(cap.Options{}); err != nil {
	fmt.Println(err, "- essential:", err.(cap.ValidationError).Essential())
}
fmt.Println(a.Validate(cap.Options{LenientTime: true}) == nil)
// 1.1 DEMO-1 Severe
// the CAP alert is not valid: sent: "2026-09-24T10:00:00Z" is not in the form 2026-09-24T10:00:00+05:30 - essential: true
// true

This is the runnable Example in example_test.go, which holds the CAP 1.1 document it reads.

Profiles: the profiles an alert declares, errors and warnings (ExampleAlert_Check in example_profiles_test.go, which holds the CAP-CP 0.4 alert it reads):

a, _ := cap.Parse(doc)
o := cap.Options{Profiles: cap.DetectProfiles(a)}
for _, p := range a.Check(o) {
	fmt.Println(p)
}
fmt.Println("valid:", a.Validate(o) == nil)
// info[0].area[0].geocode: a profile:CAP-CP:Location geocode is required (CAP-CP 0.4 rule 9)
// warning: info[0].senderName: should be given (CAP-CP 0.4 rule 14)
// valid: false

Signatures from a CA hierarchy, with a CRL:

v, err := cap.NewVerifier(cap.VerifySettings{Roots: []string{rootPEM}, Require: true})
if err != nil {
	return err
}
crls, err := cap.ParseCRLs(crlPEM)
if err != nil {
	return err
}
v.Revoked = cap.CRLCheck(crls...)
verified, state, chain, err := v.VerifyChain(doc) // parse verified, never doc

ExampleNewProfile, ExampleValidateSchema, ExampleParseEDXLDE and ExampleRepairIndia show the rest.

Documentation

API reference: https://pkg.go.dev/github.com/cB-Sigma/cap

Package What it does
cap (module root) Alert and its parts, Parse (CAP 1.2, 1.1, 1.0, no namespace), ParseStrict, Marshal, MarshalJSONCAP/ParseJSON, Validate and Check with Options.LenientTime, Options.Profiles and Problem.Essential; Profile, NewProfile, RegisterProfile, ProfileByName, DetectProfiles, CAPCP, CAPCPWithLocations, IPAWS, India, RepairIndia; History; ValidateSchema and SchemaValidator; references, date-times, MostSevere, Expires; ParsePolygon, ParseCircle, CirclePolygon, Alert.GeoJSON; ParseFeed, ParseEDXLDE, WriteAtom and WriteEDXLDE; Signer, Verifier (VerifyChain, Revoked, KeyUsages), CRLCheck, ParseCRLs, OCSPCheck
interop (nested module, tests only) cross-checks against github.com/tannerryan/cap

Testing

go test -race ./...
go test -run XXX -fuzz FuzzReaders -fuzztime 1m   # every reader on arbitrary input

Tests carry // Covers: SIG-... comments: these are requirement IDs of the SigmaM2M project's compliance report and have no meaning outside it.

interop/ is a nested module with its own go.mod, so its dependency stays out of this one. It runs with go test ./... inside it:

cd interop && go test ./...
Interop tested against
  • github.com/tannerryan/cap v1.1.1 (interop/): our alerts parse and validate there without errors, its output parses and validates here, our RSA and ECDSA signatures verify there (self-signed, and chained through an intermediate to a root), and its CAP-CP 1.0 check agrees with ours on a complying and a failing alert.
  • NDMA's SACHET feed, by hand on 2026-10-11 (not an automated test): 50 live alerts were read with Parse; all passed ValidateSchema and Validate; the India profile reported their regional language codes (31) and areas given as a point in altitude and ceiling (21) or without geometry (23), and every alert still validated after RepairIndia. The live alerts are not kept in this repository.
  • Real Canadian and US alerts, in interop/: the 33 sample alerts tannerryan/cap v1.1.1 ships in its testing directory (17 CAP-CP 0.4 alerts from Environment Canada and the NAADS aggregator, 12 IPAWS alerts from the US National Weather Service, 4 OASIS examples) parse, pass ValidateSchema and have no errors under the profiles they declare. The profiles warn on some (NWS alerts without EAS-ORG; a NAADS alert carrying MinorChange on an Alert; a Spanish wireless text of 91 characters). No live Canadian or US feed has been read.
  • cap-sim: a simulated CAP feed server and receiver, as a Go package and a command.

Status

v0.x: the API may change before v1.

Contributing

See CONTRIBUTING.md.

Security

See SECURITY.md to report a vulnerability privately.

Licence

Apache-2.0 (see LICENSE and NOTICE). Copyright 2026 Abhinav Gautam. This module is not affiliated with or endorsed by OASIS; see NOTICE.

Documentation

Overview

Package cap reads, checks, writes and signs OASIS Common Alerting Protocol 1.2 alert messages, and reads and writes the Atom and RSS feeds that carry them.

CAP 1.1 and 1.0 alerts (and alerts without a namespace) are read as CAP 1.2, and Validate lists every problem rather than stopping at the first, so an application can store an alert that strays from the schema with its problems noted (Problem.Essential tells which problems leave an alert unusable). Profiles add a community's rules to validation (Options.Profiles): the Canadian profile CAP-CP (CAPCP), the US IPAWS profile (IPAWS), the conventions of India's SACHET feed (India), or the application's own (Profile, NewProfile); Check lists warnings too. ValidateSchema checks a document's structure against the CAP 1.2 schema's content model (schema.go). Signatures are enveloped XML signatures over the whole alert, verified against pinned certificates or chains to trusted roots, with a revocation hook (sign.go, chain.go). Areas convert to GeoJSON (geo.go). ParseEDXLDE reads alerts in EDXL-DE envelopes (edxl.go).

The message model follows the CAP 1.2 schema; no schema or specification text is copied here.

Example

A CAP 1.1 alert is read as 1.2; Validate lists every problem, and Essential tells whether the alert can still be kept.

package main

import (
	"fmt"

	"github.com/cB-Sigma/cap"
)

func main() {
	doc := []byte(`<alert xmlns="urn:oasis:names:tc:emergency:cap:1.1">
  <identifier>DEMO-1</identifier><sender>demo@example.test</sender>
  <sent>2026-09-24T10:00:00Z</sent><status>Actual</status><msgType>Alert</msgType><scope>Public</scope>
  <info><category>Met</category><event>Heavy Rainfall</event><urgency>Expected</urgency>
    <severity>Severe</severity><certainty>Likely</certainty>
    <area><areaDesc>Demo district</areaDesc><circle>18.52,73.85 5</circle></area></info>
</alert>`)
	a, err := cap.Parse(doc)
	if err != nil {
		panic(err)
	}
	fmt.Println(a.Version, a.Identifier, a.MostSevere())
	if err := a.Validate(cap.Options{}); err != nil {
		fmt.Println(err, "- essential:", err.(cap.ValidationError).Essential())
	}
	fmt.Println(a.Validate(cap.Options{LenientTime: true}) == nil)
}
Output:
1.1 DEMO-1 Severe
the CAP alert is not valid: sent: "2026-09-24T10:00:00Z" is not in the form 2026-09-24T10:00:00+05:30 - essential: true
true

Index

Examples

Constants

View Source
const (
	Namespace11 = "urn:oasis:names:tc:emergency:cap:1.1"
	Namespace10 = "http://www.incident.com/cap/1.0"
)

Namespaces of the earlier CAP versions, read like CAP 1.2 (Parse).

View Source
const (
	EDXLDE10Namespace = "urn:oasis:names:tc:emergency:EDXL:DE:1.0"
	EDXLDE20Namespace = "urn:oasis:names:tc:emergency:EDXL:DE:2.0"
)

EDXL-DE namespaces.

View Source
const (
	SignatureValid = "valid"
	SignatureNone  = "none"
)

Signature states.

View Source
const AtomNamespace = "http://www.w3.org/2005/Atom"

AtomNamespace is the Atom (RFC 4287) namespace.

View Source
const LGDDistrictCode = "LGD District Code"

LGD geocode valueName of SACHET alerts.

View Source
const MediaType = "application/cap+xml"

MediaType is the media type of a CAP document.

View Source
const Namespace = "urn:oasis:names:tc:emergency:cap:1.2"

Namespace is the CAP 1.2 XML namespace.

View Source
const XMLDSigNamespace = "http://www.w3.org/2000/09/xmldsig#"

XMLDSigNamespace is the XML Signature namespace.

Variables

View Source
var (
	Statuses     = []string{"Actual", "Exercise", "System", "Test", "Draft"}
	MsgTypes     = []string{"Alert", "Update", "Cancel", "Ack", "Error"}
	Scopes       = []string{"Public", "Restricted", "Private"}
	Categories   = []string{"Geo", "Met", "Safety", "Security", "Rescue", "Fire", "Health", "Env", "Transport", "Infra", "CBRNE", "Other"}
	Responses    = []string{"Shelter", "Evacuate", "Prepare", "Execute", "Avoid", "Monitor", "Assess", "AllClear", "None"}
	Urgencies    = []string{"Immediate", "Expected", "Future", "Past", "Unknown"}
	SeverityList = []string{"Extreme", "Severe", "Moderate", "Minor", "Unknown"}
	Certainties  = []string{"Observed", "Likely", "Possible", "Unlikely", "Unknown"}
)

Enumerations of the CAP 1.2 schema.

View Source
var CAPCPEvents = []string{}/* 147 elements not displayed */

CAPCPEvents are the event codes of the CAP-CP event lists 0.4 and 1.0 (a few are misspelt in the list itself and must be used as listed).

View Source
var ErrRevoked = errors.New("certificate revoked")

ErrRevoked is wrapped by the error of a revocation check that found a certificate of the chain revoked.

View Source
var ErrUnsigned = errors.New("the alert is not signed and signatures are required")

ErrUnsigned is an unsigned alert where signatures are required.

View Source
var IPAWSMediaTypes = []string{"audio/x-ipaws-audio", "audio/x-ipaws-streaming-audio", "video/x-ipaws-video", "video/x-ipaws-streaming-video"}

IPAWSMediaTypes are the mimeType values of the profile for resources of broadcast content.

Functions

func CRLCheck added in v0.2.0

func CRLCheck(crls ...*x509.RevocationList) func(chain []*x509.Certificate) error

CRLCheck returns a revocation check for Verifier.Revoked that refuses a chain when one of its certificates is listed on one of the CRLs. A CRL counts for a certificate only when it names the certificate's issuer and is signed by the next certificate of the chain; other CRLs are ignored. A CRL past its next update is still used (its entries do not expire); refresh the list and the check to keep it current.

func CirclePolygon

func CirclePolygon(center []float64, radiusKm float64) [][]float64

CirclePolygon approximates a circle as a closed GeoJSON ring (spherical Earth, 6371 km).

func FormatTime

func FormatTime(t time.Time) string

FormatTime writes t in CAP's date-time form: seconds precision with an explicit offset ("2026-09-24T10:00:00+05:30"); UTC is written "-00:00".

func IndianLanguageTag added in v0.2.0

func IndianLanguageTag(code string) (tag string, ok bool)

IndianLanguageTag turns a SACHET language code into a language tag ("HI" is hi-IN; "PN" is pa-IN, Punjabi; "TL" is te-IN, Telugu, as the script of SACHET's TL blocks shows). ok is false for a code it does not know; a well-formed tag is returned unchanged with ok true.

func OCSPCheck added in v0.2.0

func OCSPCheck(o OCSPOptions) func(chain []*x509.Certificate) error

OCSPCheck returns a revocation check for Verifier.Revoked that asks the OCSP responder each certificate of the chain names (its Authority Information Access extension) about it, signed by or for its issuer, the next certificate of the chain (RFC 6960, via golang.org/x/crypto/ocsp). A "revoked" answer refuses the chain with ErrRevoked; see OCSPOptions.Strict for the other cases. Combine it with CRLCheck in a function of your own to use both.

func ParseCRLs added in v0.2.0

func ParseCRLs(inputs ...[]byte) ([]*x509.RevocationList, error)

ParseCRLs reads certificate revocation lists given as PEM ("X509 CRL" blocks, several per input allowed) or DER.

func ParseCircle

func ParseCircle(s string) (center []float64, radiusKm float64, err error)

ParseCircle reads a CAP circle "lat,lon radius" (radius in km).

func ParsePolygon

func ParsePolygon(s string) ([][]float64, error)

ParsePolygon reads a CAP polygon: at least four space-separated "lat,lon" pairs whose first and last are the same point. It returns the ring as GeoJSON positions.

func ParseTime

func ParseTime(s string) (time.Time, error)

ParseTime reads a CAP date-time. Strict CAP has seconds precision and a numeric offset; feeds in the field also use "Z" and fractions, which are accepted too.

func PolygonURL added in v0.2.0

func PolygonURL(in Info) string

PolygonURL returns the "Polygon URL" parameter of a SACHET info block ("" when there is none): the address of the polygon its areas leave out.

func ProfileNames added in v0.2.0

func ProfileNames() []string

ProfileNames lists the registered profiles, sorted.

func RegisterProfile added in v0.2.0

func RegisterProfile(name string, make func() Profile)

RegisterProfile makes a profile available to ProfileByName (for configurations that name profiles). The built-in profiles are registered as "CAP-CP", "IPAWS" and "India". Registering a name again replaces it.

func RepairIndia added in v0.2.0

func RepairIndia(a *Alert) []string

RepairIndia fixes the departures from CAP 1.2 seen in SACHET alerts, in place, and lists what it changed: SACHET language codes become language tags, and a point given as latitude in altitude and longitude in ceiling becomes a circle of radius 0 (altitude and ceiling are cleared). Run it before Validate and GeoJSON.

Example

SACHET alerts use their own language codes and sometimes put a point in altitude and ceiling; RepairIndia turns them into CAP 1.2.

package main

import (
	"fmt"

	"github.com/cB-Sigma/cap"
)

func main() {
	a := &cap.Alert{Info: []cap.Info{{Language: "TL", Area: []cap.Area{{AreaDesc: "River gauge", Altitude: "17.0", Ceiling: "81.8"}}}}}
	for _, c := range cap.RepairIndia(a) {
		fmt.Println(c)
	}
}
Output:
info[0].language: TL -> te-IN
info[0].area[0]: altitude 17.0 and ceiling 81.8 -> circle 17,81.8 0

func ValidateSchema added in v0.2.0

func ValidateSchema(doc []byte, extra ...SchemaValidator) error

ValidateSchema checks the document against the CAP 1.2 schema's content model and, when given, each extra validator (a full XSD engine, say), and returns every problem found as a ValidationError (nil when there is none). It checks the document as written: unlike Parse, it does not read CAP 1.1 or 1.0 as 1.2, and reports their namespace instead.

Example

ValidateSchema checks a document's structure against the CAP 1.2 schema's content model, which Parse does not.

package main

import (
	"fmt"

	"github.com/cB-Sigma/cap"
)

func main() {
	doc := []byte(`<alert xmlns="urn:oasis:names:tc:emergency:cap:1.2">
  <sender>ops@example.test</sender><identifier>X-1</identifier>
  <sent>2026-09-24T10:00:00+05:30</sent><status>Exercise</status><msgType>Alert</msgType><scope>Public</scope>
</alert>`)
	_, err := cap.Parse(doc)
	fmt.Println("parse:", err)
	fmt.Println(cap.ValidateSchema(doc))
}
Output:
parse: <nil>
the CAP alert is not valid: alert/identifier: is required before sender; alert/identifier: is out of order

func Version

func Version(space string) (string, bool)

Version is the CAP version of an alert element's namespace, and whether it is one.

func WriteAtom

func WriteAtom(m FeedMeta, alerts []*Alert, now time.Time) ([]byte, error)

WriteAtom renders alerts (newest first) as an Atom feed whose entries embed each CAP message and link to its own document.

func WriteEDXLDE added in v0.2.0

func WriteEDXLDE(d Distribution, docs ...[]byte) ([]byte, error)

WriteEDXLDE writes an EDXL-DE 1.0 envelope with one content object per CAP document, embedded as XML. The documents are embedded byte for byte (their XML declarations dropped), so signed alerts stay verifiable. d's ID, SenderID, Sent, Status and Type are required (Type is Report, Update, Cancel and so on, as EDXL-DE 1.0 lists them); Version and Entries are ignored.

Types

type Alert

type Alert struct {
	XMLName     xml.Name `xml:"urn:oasis:names:tc:emergency:cap:1.2 alert"`
	Identifier  string   `xml:"identifier"`
	Sender      string   `xml:"sender"`
	Sent        string   `xml:"sent"`
	Status      string   `xml:"status"`
	MsgType     string   `xml:"msgType"`
	Source      string   `xml:"source,omitempty"`
	Scope       string   `xml:"scope"`
	Restriction string   `xml:"restriction,omitempty"`
	Addresses   string   `xml:"addresses,omitempty"`
	Code        []string `xml:"code,omitempty"`
	Note        string   `xml:"note,omitempty"`
	References  string   `xml:"references,omitempty"`
	Incidents   string   `xml:"incidents,omitempty"`
	Info        []Info   `xml:"info,omitempty"`
	// Version is the CAP version the document was written in ("1.2",
	// "1.1", "1.0", or "none" without a namespace); it is always
	// marshalled as 1.2.
	Version string `xml:"-"`
}

Alert is a CAP 1.2 alert message.

func Parse

func Parse(b []byte) (*Alert, error)

Parse decodes a CAP document: CAP 1.2, and also CAP 1.1, 1.0 or an alert without a namespace, read as 1.2 (Alert.Version says which). It checks the root element but not the content; call Validate for that.

func ParseJSON added in v0.2.0

func ParseJSON(b []byte) (*Alert, error)

ParseJSON reads an alert written by MarshalJSONCAP (or any JSON with CAP element names). Like Parse it does not validate; Version is "1.2".

func ParseStrict added in v0.2.0

func ParseStrict(doc []byte, extra ...SchemaValidator) (*Alert, error)

ParseStrict reads a CAP 1.2 document only when it passes ValidateSchema (and the extra validators): for receivers that refuse a non-conforming document outright instead of reading it and listing its problems.

func (*Alert) Check added in v0.2.0

func (a *Alert) Check(o Options) []Problem

Check is Validate that returns every problem, warnings included (nil when there is none).

Example

Profiles add a community's rules: the profiles an alert declares are found with DetectProfiles, Check lists errors and warnings, and Validate fails only on errors.

package main

import (
	"fmt"

	"github.com/cB-Sigma/cap"
)

func main() {
	doc := []byte(`<alert xmlns="urn:oasis:names:tc:emergency:cap:1.2">
  <identifier>CA-DEMO-1</identifier><sender>alerts.example.test</sender>
  <sent>2026-09-24T10:00:00-04:00</sent><status>Actual</status><msgType>Alert</msgType><scope>Public</scope>
  <code>profile:CAP-CP:0.4</code>
  <info><language>en-CA</language><category>Met</category><event>rainfall</event>
    <urgency>Future</urgency><severity>Moderate</severity><certainty>Likely</certainty>
    <eventCode><valueName>profile:CAP-CP:Event:0.4</valueName><value>rainfall</value></eventCode>
    <expires>2026-09-25T10:00:00-04:00</expires>
    <area><areaDesc>Demo county</areaDesc></area></info>
</alert>`)
	a, _ := cap.Parse(doc)
	o := cap.Options{Profiles: cap.DetectProfiles(a)}
	for _, p := range a.Check(o) {
		fmt.Println(p)
	}
	fmt.Println("valid:", a.Validate(o) == nil)
}
Output:
info[0].area[0].geocode: a profile:CAP-CP:Location geocode is required (CAP-CP 0.4 rule 9)
warning: info[0].senderName: should be given (CAP-CP 0.4 rule 14)
valid: false

func (*Alert) Expires

func (a *Alert) Expires() time.Time

Expires is the latest expiry of the alert's info blocks, or zero when one of them has none (it stays in effect).

func (*Alert) GeoJSON

func (a *Alert) GeoJSON() map[string]any

GeoJSON renders every area of the alert as a GeoJSON FeatureCollection: one Feature per area, with a Polygon, a MultiPolygon, or no geometry when the area is described by geocodes only. Circles become 32-sided polygons and keep their centre and radius in the properties.

func (*Alert) Key

func (a *Alert) Key() string

Key identifies a message: sender, identifier and sent together are unique (the same triple a references entry uses).

func (*Alert) Marshal

func (a *Alert) Marshal() ([]byte, error)

Marshal encodes the alert as a CAP 1.2 document with an XML declaration.

func (*Alert) MarshalJSONCAP added in v0.2.0

func (a *Alert) MarshalJSONCAP() ([]byte, error)

MarshalJSONCAP encodes the alert as JSON with CAP element names.

func (*Alert) MostSevere

func (a *Alert) MostSevere() string

MostSevere is the highest severity among the info blocks ("Unknown" when none says).

func (*Alert) Validate

func (a *Alert) Validate(o Options) error

Validate checks the alert against the CAP 1.2 rules: required elements, enumerations, date-time form, references, identifier characters, the restriction and addresses conditions of the scope, and area geometry; then against each of o.Profiles. It returns the problems that are not warnings as a ValidationError, or nil.

type Area

type Area struct {
	AreaDesc string       `xml:"areaDesc" json:"areaDesc"`
	Polygon  []string     `xml:"polygon,omitempty" json:"polygon,omitempty"`
	Circle   []string     `xml:"circle,omitempty" json:"circle,omitempty"`
	Geocode  []NamedValue `xml:"geocode,omitempty" json:"geocode,omitempty"`
	Altitude string       `xml:"altitude,omitempty" json:"altitude,omitempty"`
	Ceiling  string       `xml:"ceiling,omitempty" json:"ceiling,omitempty"`
}

Area is an affected area. The JSON names are the CAP element names, so an area can be given in a JSON configuration.

type Distribution added in v0.2.0

type Distribution struct {
	// Version is "1.0" or "2.0".
	Version  string
	ID       string // distributionID
	SenderID string
	Sent     string // dateTimeSent
	Status   string // distributionStatus
	Type     string // distributionType
	// Confidentiality is combinedConfidentiality (1.0); WriteEDXLDE writes
	// "UNCLASSIFIED AND NOT SENSITIVE" when it is empty.
	Confidentiality string
	// Entries are the CAP alerts the content objects carry, in document
	// order: embedded or base64 alerts with Alert and Raw set, alerts given
	// by URI with Link set. Each entry's ID is the distribution ID with the
	// content object's position ("ID#1" for the first).
	Entries []Entry
}

Distribution is an EDXL-DE envelope as far as a CAP reader needs it.

func ParseEDXLDE added in v0.2.0

func ParseEDXLDE(b []byte) (*Distribution, error)

ParseEDXLDE reads an EDXL-DE 1.0 or 2.0 envelope and the CAP alerts in it. Content that is not CAP is skipped.

Example

CAP alerts in an EDXL-DE envelope are read with their routing facts.

package main

import (
	"fmt"

	"github.com/cB-Sigma/cap"
)

func main() {
	doc := []byte(`<EDXLDistribution xmlns="urn:oasis:names:tc:emergency:EDXL:DE:1.0">
  <distributionID>DIST-1</distributionID><senderID>hub@example.test</senderID>
  <dateTimeSent>2026-09-24T10:01:00+05:30</dateTimeSent>
  <distributionStatus>Exercise</distributionStatus><distributionType>Report</distributionType>
  <contentObject><xmlContent><embeddedXMLContent>
    <alert xmlns="urn:oasis:names:tc:emergency:cap:1.2"><identifier>X-1</identifier><sender>ops@example.test</sender>
      <sent>2026-09-24T10:00:00+05:30</sent><status>Exercise</status><msgType>Alert</msgType><scope>Public</scope></alert>
  </embeddedXMLContent></xmlContent></contentObject>
</EDXLDistribution>`)
	d, err := cap.ParseEDXLDE(doc)
	if err != nil {
		panic(err)
	}
	fmt.Println(d.Version, d.ID, d.SenderID, len(d.Entries), d.Entries[0].Alert.Identifier)
}
Output:
1.0 DIST-1 hub@example.test 1 X-1

type Entry

type Entry struct {
	ID      string
	Updated string
	Title   string
	Link    string // the CAP document's URL when it is not embedded
	Alert   *Alert // embedded alert, if any
	// Raw is the embedded alert element's text (for signature checks); a
	// bare CAP document's Raw is the whole document.
	Raw []byte
}

Entry is one feed entry: an embedded CAP alert, or a link to one.

func ParseFeed

func ParseFeed(b []byte) ([]Entry, error)

ParseFeed reads an Atom feed, an RSS 2.0 feed or a bare CAP document (one entry). Atom entries may embed the alert in their content or link to it; RSS items link to it.

func (Entry) Key

func (e Entry) Key() string

Key identifies an entry version: a changed entry is fetched again.

type FeedMeta

type FeedMeta struct {
	ID, Title, Author, Self string
	// AlertURL gives the URL a CAP message is also served at.
	AlertURL func(identifier string) string
}

FeedMeta describes a published Atom feed.

type History added in v0.2.0

type History struct {
	// contains filtered or unexported fields
}

History remembers the messages a receiver has seen, to check a rule that one message cannot show on its own: CAP-CP (0.4 rule 12, 1.0 rule 9) and the IPAWS profile want an Update or Cancel to reference every message of its chain that is still in effect. A chain is the set of messages joined by references; a message is in effect until the latest expiry of its info blocks, and for good when one has none.

Add every message received (valid or not, in any order); Missing tells what a new message leaves out; Profile wraps that as a profile for Options.Profiles. A History is safe for concurrent use.

func NewHistory added in v0.2.0

func NewHistory() *History

NewHistory returns an empty history.

func (*History) Add added in v0.2.0

func (h *History) Add(a *Alert)

Add records a message and joins it to the chains it references.

func (*History) Forget added in v0.2.0

func (h *History) Forget(t time.Time)

Forget drops messages that expired before t (messages without expiry stay), to bound the history's size.

func (*History) Len added in v0.2.0

func (h *History) Len() int

Len is the number of messages recorded.

func (*History) Missing added in v0.2.0

func (h *History) Missing(a *Alert, at time.Time) []Reference

Missing returns the messages in effect at the time at, in the chains a references, that a does not reference itself, sorted by key. Messages the history has not seen cannot be reported.

func (*History) Profile added in v0.2.0

func (h *History) Profile(now func() time.Time) Profile

Profile returns a profile named "References" that reports, for an Update or Cancel, each message of its chains still in effect at now() (nil is time.Now) that it does not reference. It does not add the message to the history; call Add for that.

type Info

type Info struct {
	Language     string       `xml:"language,omitempty"`
	Category     []string     `xml:"category"`
	Event        string       `xml:"event"`
	ResponseType []string     `xml:"responseType,omitempty"`
	Urgency      string       `xml:"urgency"`
	Severity     string       `xml:"severity"`
	Certainty    string       `xml:"certainty"`
	Audience     string       `xml:"audience,omitempty"`
	EventCode    []NamedValue `xml:"eventCode,omitempty"`
	Effective    string       `xml:"effective,omitempty"`
	Onset        string       `xml:"onset,omitempty"`
	Expires      string       `xml:"expires,omitempty"`
	SenderName   string       `xml:"senderName,omitempty"`
	Headline     string       `xml:"headline,omitempty"`
	Description  string       `xml:"description,omitempty"`
	Instruction  string       `xml:"instruction,omitempty"`
	Web          string       `xml:"web,omitempty"`
	Contact      string       `xml:"contact,omitempty"`
	Parameter    []NamedValue `xml:"parameter,omitempty"`
	Resource     []Resource   `xml:"resource,omitempty"`
	Area         []Area       `xml:"area,omitempty"`
}

Info is one info block (one language, one event).

type NamedValue

type NamedValue struct {
	ValueName string `xml:"valueName" json:"valueName"`
	Value     string `xml:"value" json:"value"`
}

NamedValue is an eventCode, parameter or geocode.

type OCSPOptions added in v0.2.0

type OCSPOptions struct {
	// Client sends the requests; nil is a client with a 10 second timeout.
	Client *http.Client
	// Strict fails a chain when a certificate names no responder, the
	// responder cannot be reached, its answer is "unknown" or stale.
	// Without it those cases pass (soft fail) and only a "revoked" answer
	// refuses the chain.
	Strict bool
	// LeafOnly asks only about the signing certificate, not about the
	// intermediate CAs above it.
	LeafOnly bool
	// Now is the clock for freshness (tests); nil is time.Now.
	Now func() time.Time
}

OCSPOptions configures OCSPCheck.

type Options

type Options struct {
	// LenientTime accepts "Z" and fractional seconds in date-times.
	LenientTime bool
	// Profiles are checked after CAP 1.2 itself (CAPCP, IPAWS, India, or
	// one of the application's own); their problems carry the profile's
	// name.
	Profiles []Profile
}

Options relax validation for feeds that stray from the schema, and add profiles.

type Problem

type Problem struct {
	Field string `json:"field"`
	Issue string `json:"issue"`
	// Profile names the profile whose rule this is ("" for CAP 1.2
	// itself).
	Profile string `json:"profile,omitempty"`
	// Rule is the rule's reference in the profile or standard, such as a
	// clause or rule number, where one is known ("XSD" for the schema
	// check).
	Rule string `json:"rule,omitempty"`
	// Warning marks a recommendation rather than a requirement: Check
	// lists it, Validate does not fail on it.
	Warning bool `json:"warning,omitempty"`
}

Problem is one validation finding.

func (Problem) Essential

func (p Problem) Essential() bool

Essential tells whether a validation problem leaves the alert unusable: without its identifier, sender or a readable sent time it cannot be told apart from others or referenced. Profile problems are never essential.

func (Problem) String

func (p Problem) String() string

type Profile added in v0.2.0

type Profile interface {
	// Name identifies the profile in problems, such as "CAP-CP".
	Name() string
	// Check returns the alert's problems under the profile's rules (nil
	// when it complies). The alert has already been read; it may not be
	// valid CAP 1.2.
	Check(a *Alert) []Problem
}

Profile is a set of rules a community adds to CAP 1.2, such as a national alerting authority's profile. Pass profiles in Options.Profiles; Check and Validate run them after the CAP 1.2 rules. The profiles this module provides (CAPCP, IPAWS, India) are written from each profile's public document in our own words; an application adds its own by implementing Profile or with NewProfile.

func CAPCP added in v0.2.0

func CAPCP() Profile

CAPCP returns the Canadian profile. It follows the version the alert declares in its code (profile:CAP-CP:0.4 or profile:CAP-CP:1.0); an alert that declares neither is checked as 1.0 and told to declare it.

func CAPCPWithLocations added in v0.2.0

func CAPCPWithLocations(known func(code string) bool) Profile

CAPCPWithLocations is CAPCP that also checks each CAP-CP location code against a list the application loads (Statistics Canada's SGC codes and the profile's location annex, which are published separately and not included here): known reports whether a code is on it. "00" and "none" are always accepted.

func DetectProfiles added in v0.2.0

func DetectProfiles(a *Alert) []Profile

DetectProfiles returns the built-in profiles an alert declares in its code elements (CAP-CP and IPAWS name themselves there). India's profile is not declared in alerts and is never detected.

func IPAWS added in v0.2.0

func IPAWS() Profile

IPAWS returns the US IPAWS profile 1.0.

func India added in v0.2.0

func India() Profile

India returns the profile of the conventions SACHET alerts follow (see above; all findings are warnings).

func NewProfile added in v0.2.0

func NewProfile(name string, check func(a *Alert) []Problem) Profile

NewProfile makes a profile from a name and a check function.

Example

An application adds its own profile with NewProfile (or by implementing Profile) and can register it for configurations that name profiles.

package main

import (
	"fmt"

	"github.com/cB-Sigma/cap"
)

func main() {
	control := cap.NewProfile("Control room", func(a *cap.Alert) []cap.Problem {
		if a.Source == "" {
			return []cap.Problem{{Field: "source", Issue: "must name the duty officer", Rule: "CR-1"}}
		}
		return nil
	})
	cap.RegisterProfile("control-room", func() cap.Profile { return control })
	p, _ := cap.ProfileByName("Control-Room")
	a := &cap.Alert{Identifier: "X-1", Sender: "ops@example.test", Sent: "2026-09-24T10:00:00+05:30",
		Status: "Exercise", MsgType: "Alert", Scope: "Public"}
	fmt.Println(a.Validate(cap.Options{Profiles: []cap.Profile{p}}))
}
Output:
the CAP alert is not valid: source: must name the duty officer (Control room CR-1)

func ProfileByName added in v0.2.0

func ProfileByName(name string) (Profile, bool)

ProfileByName returns a registered profile; the name's case is ignored.

type Reference

type Reference struct {
	Sender, Identifier, Sent string
}

Reference is one entry of an alert's references.

func ParseReferences

func ParseReferences(s string) ([]Reference, error)

ParseReferences splits a references value into its entries.

func (Reference) Key

func (r Reference) Key() string

Key is the reference in the "sender,identifier,sent" form.

type Resource

type Resource struct {
	ResourceDesc string `xml:"resourceDesc"`
	MimeType     string `xml:"mimeType"`
	Size         string `xml:"size,omitempty"`
	URI          string `xml:"uri,omitempty"`
	DerefURI     string `xml:"derefUri,omitempty"`
	Digest       string `xml:"digest,omitempty"`
}

Resource is a file attached to an info block.

type SchemaValidator added in v0.2.0

type SchemaValidator interface {
	ValidateSchema(doc []byte) error
}

SchemaValidator validates a whole document, for example an XSD engine loaded with the OASIS cap-v1.2.xsd file the application provides.

type SchemaValidatorFunc added in v0.2.0

type SchemaValidatorFunc func(doc []byte) error

SchemaValidatorFunc adapts a function to SchemaValidator.

func (SchemaValidatorFunc) ValidateSchema added in v0.2.0

func (f SchemaValidatorFunc) ValidateSchema(doc []byte) error

ValidateSchema calls f.

type SignSettings

type SignSettings struct {
	// Key is the PEM private key (PKCS #8, PKCS #1 RSA or SEC 1 EC).
	Key string `json:"key"`
	// Certificate is the PEM certificate of the key, followed by any
	// intermediates; the first goes in each signature's KeyInfo.
	Certificate string `json:"certificate"`
}

SignSettings configures signing of published messages.

type Signer

type Signer struct {
	// contains filtered or unexported fields
}

Signer signs CAP documents.

func NewSigner

func NewSigner(s SignSettings) (*Signer, error)

NewSigner reads a key and certificate chain.

func (*Signer) Certificates

func (s *Signer) Certificates() []*x509.Certificate

Certificates is the signing certificate followed by its intermediates.

func (*Signer) Sign

func (s *Signer) Sign(doc []byte) ([]byte, error)

Sign returns the document with an enveloped signature appended to its alert element.

type ValidationError

type ValidationError []Problem

ValidationError lists every problem of an alert.

func (ValidationError) Error

func (v ValidationError) Error() string

func (ValidationError) Essential

func (v ValidationError) Essential() bool

Essential tells whether any of the problems is essential.

type Verifier

type Verifier struct {

	// Now is the clock for certificate validity (tests); nil is time.Now.
	Now func() time.Time
	// Revoked, when set, is asked about every chain a signature is
	// accepted on (the signing certificate first, then its issuers; a
	// pinned certificate is a chain of one). An error refuses the
	// signature. CRLCheck builds one from certificate revocation lists;
	// an OCSP or other check can be plugged in the same way.
	Revoked func(chain []*x509.Certificate) error
	// KeyUsages are the extended key usages a chained signing certificate
	// must allow; empty accepts any. Pinned certificates are not checked.
	KeyUsages []x509.ExtKeyUsage
	// contains filtered or unexported fields
}

Verifier checks CAP signatures against trusted certificates.

func NewVerifier

func NewVerifier(s VerifySettings) (*Verifier, error)

NewVerifier reads the trusted certificates.

func NewVerifierSkipping

func NewVerifierSkipping(s VerifySettings, skip func(i int, err error)) *Verifier

NewVerifierSkipping reads the trusted certificates, roots and intermediates, passing each entry that cannot be used to skip (i is its index in its own list, which the error names) and leaving it out. With none left, every signed alert fails verification (it stays refused).

func (*Verifier) Required

func (v *Verifier) Required() bool

Required tells whether unsigned alerts are refused.

func (*Verifier) Roots added in v0.2.0

func (v *Verifier) Roots() *x509.CertPool

Roots is the pool of CA certificates chains are built to; nil when the verifier has none and accepts only pinned certificates.

func (*Verifier) Trusted

func (v *Verifier) Trusted() []*x509.Certificate

Trusted is the certificates whose signatures are accepted.

func (*Verifier) Verify

func (v *Verifier) Verify(doc []byte) (verified []byte, state string, err error)

Verify checks the document's signature. It returns the verified alert element as a document of its own (signature removed): callers parse that, never the original bytes, so content outside the signed element cannot be slipped in. An unsigned document is returned as it is with state "none" unless signatures are required.

func (*Verifier) VerifyChain added in v0.2.0

func (v *Verifier) VerifyChain(doc []byte) (verified []byte, state string, chain []*x509.Certificate, err error)

VerifyChain is Verify that also returns the chain the signature was accepted on: the signing certificate first, then its issuers up to a root (only the signing certificate when it is pinned). The chain is nil for an unsigned alert.

type VerifySettings

type VerifySettings struct {
	// Trusted are PEM certificates whose signatures are accepted; a
	// signature is valid only when its KeyInfo certificate is one of them
	// (and within its validity period).
	Trusted []string `json:"trusted"`
	// Roots are PEM CA certificates. A signature whose KeyInfo certificate
	// is not pinned in Trusted is accepted when that certificate chains to
	// one of them (and to nothing else), through intermediates carried in
	// KeyInfo or listed in Intermediates, with every certificate within
	// its validity period.
	Roots []string `json:"roots,omitempty"`
	// Intermediates are PEM CA certificates used to build chains to Roots
	// for signers that leave them out of KeyInfo. They are not trusted on
	// their own.
	Intermediates []string `json:"intermediates,omitempty"`
	// SystemRoots adds the operating system's trusted CAs to Roots.
	SystemRoots bool `json:"systemRoots,omitempty"`
	// Require refuses alerts without a valid signature. Without it an
	// unsigned alert is accepted and marked "signature": "none"; a
	// signature that does not verify is refused unless acceptInvalid.
	Require bool `json:"require,omitempty"`
	// AcceptInvalid stores an alert whose signature does not verify,
	// marked "signature": "invalid", instead of refusing it (default
	// false). Risk: a forged or tampered alert is stored and can raise
	// alarms. Every such alert is also reported as a warning.
	AcceptInvalid bool `json:"acceptInvalid,omitempty"`
}

VerifySettings configures checking of received signatures.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL