Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

203 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

πŸ” PCL - Policy-based Certificate Linter

CI codecov Go Report Card License: GPL-3.0

A flexible X.509 certificate linter that validates certificates against configurable YAML-based policies. Ensure compliance with RFC 5280, organizational standards, or industry best practices.

πŸš€ Quick Start

go install github.com/cavoq/PCL/cmd/pcl@latest
pcl --policy <path> [--policy <path>...] --cert <path> [--crl <path>] [--ocsp <path>] [--purpose <eku-name-or-oid>] [--output text|json|yaml]

Multiple policies can be specified with repeatable --policy flags. All rules from all policies will be applied.

By default, only failed rules are shown. Use -v to include passed rules and -vv to include skipped rules.

Use --purpose serverAuth (or another supported friendly EKU name or dotted OID) to activate RFC 5280 application-purpose evaluation. The shipped rule checks EKU across the supplied certificate chain and compatible end-entity Key Usage. The wildcard values any, anyExtendedKeyUsage, and 2.5.29.37.0 are not application purposes and are rejected. Private EKU OIDs are accepted as purpose identifiers, but fail closed when end-entity Key Usage is present because RFC 5280 defines no Key Usage mapping for them. Without --purpose, the conditional rule is skipped.

Auto-Validate Mode

Automatically fetch PKI resources from certificate extensions (OCSP, CRL, CA Issuers) and climb the certificate chain:

pcl --policy <path> --cert leaf.pem --auto-validate

This mode:

  • Chain Climbing: Recursively fetches issuer certificates via CA Issuers URLs
  • PKCS#7 Support: Parses .p7c certificate bundles per RFC 5280/5652
  • Auto OCSP/CRL: Fetches revocation information from AIA extensions
  • Issuer Matching: Handles multi-certificate bundles by matching Issuer DN and AKI-SKI

Options for granular control:

# Limit chain depth
pcl --policy <path> --cert leaf.pem --auto-validate --max-chain-depth 5

# Disable specific auto-fetch features
pcl --policy <path> --cert leaf.pem --auto-validate --no-auto-chain
pcl --policy <path> --cert leaf.pem --auto-validate --no-auto-crl
pcl --policy <path> --cert leaf.pem --auto-validate --no-auto-ocsp

# OCSP nonce configuration (RFC 9654)
pcl --policy <path> --cert leaf.pem --auto-validate --ocsp-nonce-length 32
pcl --policy <path> --cert leaf.pem --auto-validate --ocsp-nonce-value aabbcc...
pcl --policy <path> --cert leaf.pem --auto-validate --no-ocsp-nonce

# OCSP CertID hash algorithm (RFC 5019 vs modern)
pcl --policy <path> --cert leaf.pem --auto-validate --ocsp-hash sha1   # RFC 5019
pcl --policy <path> --cert leaf.pem --auto-validate --ocsp-hash sha256 # Modern (default)

Public Suffix List (PSL) Options

PCL uses the Public Suffix List to validate TLDs and domain names (BR 4.2.2, 3.2.2.6):

# Use default PSL location (./data/public_suffix_list.dat or ~/.pcl/data/)
pcl --policy <path> --cert cert.pem

# Specify custom PSL file
pcl --policy <path> --cert cert.pem --psl-file /path/to/public_suffix_list.dat

# Disable PSL loading (use regex fallback)
pcl --policy <path> --cert cert.pem --use-psl=false

# Update/download PSL to default location
pcl update-data

# Update PSL to custom directory
pcl update-data --data-dir /custom/data/path

Debug OCSP Requests

Use -vv to see detailed OCSP request/response information:

pcl --policy policies/RFC6960.yaml --cert leaf.pem --auto-validate -vv

Output includes:

  • Request nonce length and hex value
  • CertID hash algorithm (SHA1/SHA256)
  • Response nonce and match status
  • OCSP response timing details

Fetch TLS Certificates from URLs

Fetch certificate chains from HTTPS endpoints:

pcl --policy <path> --cert-url https://example.test --cert-url-timeout 10s --cert-url-save-dir ./downloads

πŸ“ Policy Configuration

Policies are YAML files defining validation rules with a simple declarative syntax.

id: rfc5280
version: 1.0

rules:
  # -------------------------------------------------
  # Certificate structure (Section 4.1)
  # -------------------------------------------------

  # Version MUST be 3 when extensions are present
  - id: version-v3-when-extensions
    reference: RFC5280 4.1.2.1
    when:
      target: certificate.extensions
      operator: present
    target: certificate.version
    operator: eq
    operands: [3]
    severity: error

  # Serial number MUST be positive integer
  - id: serial-number-positive
    reference: RFC5280 4.1.2.2
    target: certificate.serialNumber.value
    operator: positive
    severity: error

  # Local diagnostic only: this cannot establish CA-wide RFC 5280 uniqueness
  - id: serial-number-duplicate-in-chain
    reference: LOCAL-CHAIN-HEURISTIC
    target: certificate
    operator: serialNumberUnique
    severity: warning

  # -------------------------------------------------
  # Signature validation (Section 4.1.2.3)
  # -------------------------------------------------

  - id: signature-valid
    reference: RFC5280 4.1.2.3
    target: certificate
    operator: signatureValid
    severity: error

  - id: signature-algorithm-matches-tbs
    reference: RFC5280 4.1.2.3
    target: certificate
    operator: signatureAlgorithmMatchesTBS
    severity: error

  # -------------------------------------------------
  # Basic Constraints (Section 4.2.1.9)
  # -------------------------------------------------

  - id: ca-basic-constraints
    reference: RFC5280 4.2.1.9
    target: certificate.basicConstraints.cA
    operator: eq
    operands: [true]
    severity: error
    certType: [root, intermediate]

  # -------------------------------------------------
  # Key Usage (Section 4.2.1.3)
  # -------------------------------------------------

  - id: key-usage-ca
    reference: RFC5280 4.2.1.3
    when:
      target: certificate.extensions.keyUsage
      operator: present
    target: certificate.keyUsage.keyCertSign
    operator: eq
    operands: [true]
    severity: error
    certType: [root, intermediate]

  # -------------------------------------------------
  # AIA Extension - Best Practice (INFO severity)
  # -------------------------------------------------

  - id: leaf-ca-issuers-url-recommended
    reference: RFC5280 4.2.2.1
    target: certificate.extensions.authorityInfoAccess.containsCaIssuers
    operator: eq
    operands: [true]
    severity: info
    certType: [leaf]

  # -------------------------------------------------
  # Subject Alternative Name (Section 4.2.1.6)
  # -------------------------------------------------

  - id: san-required-if-empty-subject
    reference: RFC5280 4.2.1.6
    when:
      target: certificate.subject
      operator: isEmpty
    target: certificate.subjectAltName
    operator: present
    severity: error

  # -------------------------------------------------
  # Conditional RSA validation (RFC 4055)
  # -------------------------------------------------

  - id: rsa-params-null
    reference: RFC4055 Section 5
    when:
      target: certificate.signatureAlgorithm.oid
      operator: in
      operands:
        - "1.2.840.113549.1.1.11"  # sha256WithRSAEncryption
        - "1.2.840.113549.1.1.12"  # sha384WithRSAEncryption
        - "1.2.840.113549.1.1.13"  # sha512WithRSAEncryption
    target: certificate.signatureAlgorithm.parameters.null
    operator: eq
    operands: [true]
    severity: error

πŸ›οΈ Supported Policies

  • policies/RFC5280.yaml β€” RFC 5280 certificate and CRL profile checks
  • policies/RFC4055.yaml β€” RSA algorithm-identifier checks from RFC 4055
  • policies/RFC6960.yaml β€” fail-closed OCSP evidence checks for leaf certificates
  • policies/LOCAL-PROFILE.yaml β€” explicitly supplemental local/WebPKI checks

These are lint policies, not claims that PCL implements every state machine in the named standard. See the coverage matrices and conformance roadmap for the exact scope.

RFC 5280 P2 certificate-profile semantics are complete: strict extension parsing, dependency checks, explicit application purpose, bounded path-length and supported-name constraints, and fail-closed processed-extension handling. CRL scope/reasons/deltas remain P3, while full Sections 6 and 7 path/policy/name processing remains P4. See the coverage matrix and roadmap.

Load independent bundles together by repeating --policy, for example --policy policies/RFC5280.yaml --policy policies/RFC6960.yaml.

The RFC 6960 bundle runs during leaf-certificate evaluation because it binds a response to the certificate and issuer chain. Loading it opts into requiring valid OCSP evidence; a missing, unrelated, stale, or non-good response fails.

Related profiles used by selected rules and acquisition features include:

  • RFC 9549 - Internationalization Updates to RFC 5280 (IDN, DNS labels)
  • RFC 9598 - Internationalized Email Addresses in X.509 (rfc822Name)
  • RFC 9654 - OCSP Nonce Extension
  • CA/Browser Forum Baseline Requirements (BR)
  • CA/Browser Forum Extended Validation Guidelines (EVG)
  • CA/Browser Forum S/MIME Baseline Requirements (SMIME BR)
  • CA/Browser Forum Code Signing Baseline Requirements (CS BR)

βž• Supported Operators

Comparison Operators

Operator Description
eq Equality check
neq Not equal check
gt, gte Greater than (or equal)
lt, lte Less than (or equal)
in Value in allowed list
notIn Value not in disallowed list
contains String/array contains value
matches Compare two field paths for equality

Presence & Value Operators

Operator Description
present Field existence check
absent Field does not exist
isEmpty, notEmpty Value emptiness check
isNull Explicit ASN.1 NULL marker check
positive Value is a positive number
odd Value is an odd number (for RSA exponent validation)
maxLength, minLength String/array length constraints
regex, notRegex Regular expression pattern matching
componentMaxLength, componentMinLength Per-component length validation (DNS labels, path segments)
componentRegex, componentNotRegex Per-component regex validation
anyComponentMatches, noComponentMatches ANY/NO component matches regex (for wildcard detection)
componentInCIDR, componentNotInCIDR Per-component CIDR range validation (for IP address checking)
utf8NoBom, containsBom UTF-8 BOM detection
derEqualsHex Compare DER bytes with an expected hexadecimal value

Date Operators

Operator Description
before Date is before current time
after Date is after current time
onOrBefore Date is before or equal to the comparison time
onOrAfter Date is after or equal to the comparison time
validityOrderCorrect Validates notBefore < notAfter
validityDays Certificate validity period check
dateDiff Date difference validation with maxDays/maxMonths limits (for CRL nextUpdate)

Extension Operators

Operator Description
isCritical, notCritical Extension criticality check
noUnknownCriticalExtensions Extension OIDs must be unique; critical certificate extensions must be in the conservative processed set and not malformed; critical non-certificate extensions fail closed

Certificate Chain Operators

Operator Description
signatureValid Cryptographic signature verification
signatureAlgorithmMatchesTBS Signature algorithm matches TBS certificate
issuedBy Issuer DN matches issuer's subject DN
akiMatchesSki Authority Key ID matches issuer's Subject Key ID
pathLenValid Counts non-self-issued intermediates in the supplied ordered chain; not a Section 6 path engine
serialNumberUnique Duplicate issuer/serial diagnostic within the supplied chain; not CA-wide uniqueness

Key Usage & Constraints Operators

Operator Description
ekuContains, ekuNotContains Extended key usage checks
ekuServerAuth, ekuClientAuth TLS authentication EKU checks
applicationPurposeValid Explicit --purpose compatibility with EKU and end-entity Key Usage
anyExtendedKeyUsageNotCritical Rejects critical EKU when it asserts anyExtendedKeyUsage
basicConstraintsDependenciesValid, keyUsageDependenciesValid Basic Constraints/Key Usage profile dependencies, including keyCertSign => CA
nameConstraintsDependenciesValid, nameConstraintsDistancesValid CA-only and GeneralSubtree distance requirements
crlDistributionPointsDependenciesValid CRLDP profile dependencies without P3 CRL scope processing
policyMappingsDependenciesValid, policyMappingsIssuerPoliciesPresent Policy Mapping profile restrictions
policyConstraintsDependenciesValid, inhibitAnyPolicyDependenciesValid CA-only policy-extension restrictions

Collection/Array Operators

Operator Description
every Apply one scalar operator to every canonical collection element
uniqueValues All children have unique values (for CRL DP, AIA URLs)
uniqueChildren All children have unique string values
noDuplicateAttributes CABF single-instance subject attributes do not repeat

CRL Operators

Operator Description
crlValid CRL is within thisUpdate/nextUpdate window
crlNotExpired Compatibility check: nextUpdate is present and has not passed
crlSignedBy Cryptographic CRL signature verification against resolved issuers
notRevoked Certificate absent from an applicable CRL; missing/unrelated CRLs return false (unknown)

OCSP Operators

Operator Description
ocspValid At least one response is bound to the certificate and issuer, current, and signed by the issuer or an authorized responder
notRevokedOCSP Accepted aggregate status is Good; absent, unknown, stale, unrelated, invalidly signed, and revoked evidence fail
ocspGood At least one accepted response explicitly reports Good

Path Validation Operators

Operator Description
nameConstraintsValid Applies supported DNS, email, URI, and IP constraints; partial Sections 6/7 behavior
certificatePolicyValid Compatibility operator; the Section 6 policy state machine is not active in RFC5280.yaml

ASN.1 time details are projected as ordinary fields (encoding, rawValue, hasSeconds, hasFraction, and hasZulu) and are checked with generic comparison operators.

Public Suffix List (PSL) / TLD Operators

Operator Description
tldRegistered TLD is registered in IANA Root Zone Database (via PSL ICANN section)
tldNotRegistered TLD is NOT registered in IANA Root Zone Database
isPublicSuffix Domain is a public suffix (ICANN or private)
isNotPublicSuffix Domain is NOT a public suffix
componentTLDNotRegistered At least one domain has an unregistered TLD
componentIsPublicSuffix FQDN portion of wildcard is a public suffix (BR 3.2.2.6)
componentNotPublicSuffix FQDN portion of wildcard is NOT a public suffix

These operators use the Public Suffix List (PSL) from publicsuffix.org. The PSL ICANN section contains all IANA-registered TLDs, enabling validation of:

  • BR 4.2.2: Internal Names - certificates must not contain domains with unregistered TLDs
  • BR 3.2.2.6: Wildcard certificates - FQDN portion must not be a public suffix

ASN.1 Encoding Operators

Operator Description
isIA5String Value uses IA5String encoding (ASCII)
isPrintableString Value uses PrintableString encoding
isUTF8String Value uses UTF8String encoding
validIA5String All characters valid for IA5String (ASCII)
validPrintableString All characters valid for PrintableString

validIA5String and componentRegex validate one scalar value. Wrap them in every when the target is a SAN, IAN, DN, or other collection.

πŸ”€ Conditional Rules

Rules can include a when clause to apply only when certain conditions are met:

# Only validate RSA key size when certificate uses RSA algorithm
- id: rsa-key-size-minimum
  reference: RFC4055
  when:
    target: certificate.subjectPublicKeyInfo.algorithm.algorithm
    operator: eq
    operands: [RSA]
  target: certificate.subjectPublicKeyInfo.publicKey.keySize
  operator: gte
  operands: [2048]
  severity: error

# Only check SAN when subject is empty (RFC 5280 4.2.1.6)
- id: san-required-if-empty-subject
  reference: RFC5280 4.2.1.6
  when:
    target: certificate.subject
    operator: isEmpty
  target: certificate.subjectAltName
  operator: present
  severity: error

# EV certificate MUST have SCT (only applies to EV certs)
- id: ev-sct-required
  reference: CA/Browser Forum BR Appendix B
  when:
    target: certificate.extensions.certificatePolicies.2.23.140.1.1
    operator: present
  target: certificate.signedCertificateTimestamps
  operator: present
  severity: warning
  certType: [leaf]

When the when condition is not met, the rule status is SKIP (not displayed by default, use -vv to see).

⚠️ Severity Levels

PCL supports three severity levels:

Level Color Description
error Red Mandatory compliance requirement
warning Yellow Important best practice or conditional requirement
info Blue Recommended best practice (non-blocking)

Example: EV certificates should have SCT embedded (warning), while AIA extension presence is recommended (info) for interoperability.

Certificate Chain Support

PCL automatically builds and validates certificate chains, applying rules based on certificate position and BasicConstraints:

  • leaf: End-entity certificates (position 0, no BasicConstraints or IsCA=false)
  • intermediate: Subordinate CA certificates (position 0+ with IsCA=true, not self-signed)
  • root: Self-signed root CA certificates (IsCA=true, Subject==Issuer)

Enhanced Detection: At position 0, PCL checks BasicConstraints to correctly identify CA certificates even when linted directly (without a subscriber certificate chain). This allows linting intermediate CA certificates standalone.

Use certType on individual rules to target certificate roles (leaf, intermediate, root, ocspSigning). Rules without certType run on every role (others are SKIP when the role does not match).

πŸ“₯ Policy and input filtering

Policy-level (top of the YAML file, next to id / version):

  • appliesTo: which input object the policy is for β€” cert, crl, ocsp, tst, sct, or attrCert (for example, appliesTo: [ocsp] for an OCSP-object policy).
  • certType: optional filter so the whole policy runs only on matching certificate roles (used with certificate linting).

An explicit appliesTo is authoritative. If it is omitted, PCL infers an order-independent set of inputs from every rule's primary target namespace (certificate, crl, ocsp, tst, sct, or attrCert). A when target is a dependency only and does not change the execution input; an unqualified primary target is input-agnostic. The current CLI has certificate, CRL, and OCSP loaders; the other values are reserved by the policy schema. Role filtering remains per-rule certType.

🌳 Node Tree Structure

Rules target fields using a dot-separated path notation. The node tree structure mirrors the certificate/CRL/OCSP structure:

Certificate Node Tree

certificate
β”œβ”€β”€ version                 # Integer (1, 2, 3)
β”œβ”€β”€ serialNumber
β”‚   β”œβ”€β”€ value              # String representation
β”‚   └── ...                # Raw bytes
β”œβ”€β”€ signatureAlgorithm
β”‚   β”œβ”€β”€ algorithm          # String name (e.g., "SHA256-RSA")
β”‚   β”œβ”€β”€ oid                # OID string
β”‚   └── parameters
β”‚       β”œβ”€β”€ null           # Boolean (true if NULL)
β”‚       β”œβ”€β”€ absent         # Boolean (true if omitted)
β”‚       └── pss/oaep       # PSS/OAEP parameters (if present)
β”œβ”€β”€ tbsSignatureAlgorithm  # Same structure as signatureAlgorithm
β”œβ”€β”€ issuer / subject
β”‚   β”œβ”€β”€ countryName
β”‚   β”œβ”€β”€ organizationName
β”‚   β”œβ”€β”€ commonName
β”‚   └── ...
β”œβ”€β”€ validity
β”‚   β”œβ”€β”€ notBefore          # time.Time
β”‚   β”œβ”€β”€ notAfter           # time.Time
β”œβ”€β”€ subjectPublicKeyInfo
β”‚   β”œβ”€β”€ algorithm
β”‚   β”‚   β”œβ”€β”€ algorithm      # String (RSA, ECDSA)
β”‚   β”‚   β”œβ”€β”€ oid            # OID string
β”‚   └── publicKey
β”‚       β”œβ”€β”€ keySize        # Integer
β”‚       β”œβ”€β”€ exponent       # RSA exponent
β”‚       β”œβ”€β”€ curve          # ECDSA curve name
β”œβ”€β”€ extensions
β”‚   β”œβ”€β”€ <oid>              # Each extension keyed by OID
β”‚   β”‚   β”œβ”€β”€ oid
β”‚   β”‚   β”œβ”€β”€ critical       # Boolean
β”‚   β”‚   β”œβ”€β”€ value          # Raw bytes
β”‚   β”‚   └── malformed      # Present when strict decoding fails
β”œβ”€β”€ basicConstraints
β”‚   β”œβ”€β”€ cA                 # Boolean
β”‚   β”œβ”€β”€ cAPresent          # Encoded DEFAULT-field presence
β”‚   β”œβ”€β”€ pathLenConstraint  # Integer (if present)
β”‚   └── pathLenConstraintPresent
β”œβ”€β”€ keyUsage               # Integer bitmask
β”‚   β”œβ”€β”€ digitalSignature   # Boolean (per bit)
β”‚   β”œβ”€β”€ keyCertSign        # Boolean
β”‚   └── ...
β”œβ”€β”€ extKeyUsage
β”‚   β”œβ”€β”€ serverAuth         # Boolean
β”‚   β”œβ”€β”€ clientAuth         # Boolean
β”‚   β”œβ”€β”€ usages.0.oid       # Ordered known or private KeyPurposeId
β”‚   └── ...
β”œβ”€β”€ applicationPurpose     # Present only with --purpose
β”œβ”€β”€ subjectKeyIdentifier   # Bytes
β”œβ”€β”€ authorityKeyIdentifier # Bytes
β”œβ”€β”€ subjectAltName
β”‚   β”œβ”€β”€ dNSName
β”‚   β”œβ”€β”€ iPAddress
β”‚   └── ...
β”œβ”€β”€ signedCertificateTimestamps  # SCT list
└── ...                    # AIA, CRLDP, policies, and constraints decode under extensions

P2 adds presence-preserving schemas under certificate.extensions.policyMappings (mappings.*.{issuerDomainPolicy,subjectDomainPolicy}), policyConstraints (the two counters and *Present facts), inhibitAnyPolicy.skipCerts, and nameConstraints.{permittedSubtrees,excludedSubtrees}. Name Constraints keep each GeneralSubtree base, tag, raw DER, minimum/maximum values, and independent presence facts. Counter nodes retain exact decimal text and a fitsInt fact, so a valid (0..MAX) value larger than the native integer range is not misreported as malformed. CRLDP supports both full GeneralNames and a lossless nameRelativeToCRLIssuer RDN. See the policy-writing guide for canonical paths.

The extension identity catalog and processed-extension registry are separate. The processed set is conservatively certificate-only. Known-but-unprocessed, non-certificate, P3-only, or malformed critical extensions fail noUnknownCriticalExtensions. Duplicate extension OIDs also fail before criticality is considered; non-critical unique extensions remain available to ordinary profile rules.

CRL Node Tree

crl
β”œβ”€β”€ signatureAlgorithm     # Same as certificate
β”œβ”€β”€ issuer                 # Same as certificate
β”œβ”€β”€ thisUpdate             # time.Time
β”œβ”€β”€ nextUpdate             # time.Time
β”œβ”€β”€ isCACRL                # Boolean: CA signer or >10-day profile inference
β”œβ”€β”€ revokedCertificates
β”‚   β”œβ”€β”€ <serial>           # Each revoked cert
β”‚   β”‚   β”œβ”€β”€ serialNumber
β”‚   β”‚   β”œβ”€β”€ revocationDate
β”‚   β”‚   └── extensions
β”‚   β”‚       └── 2.5.29.21  # reasonCode; decoded integer is under value
└── extensions
    └── ...

CRL type detection (isCACRL): True when a verified or identity-matched signer from the available chain is a CA. If no CA signer is available, a CRL window longer than 10 days selects the CA/other-CRL profile as a classification fallback; signature and revocation decisions still require verified evidence. With --auto-validate, PCL can also resolve a signer through CA Issuers URLs.

OCSP Node Tree

ocsp
β”œβ”€β”€ status                 # String (Good, Revoked, Unknown)
β”œβ”€β”€ producedAt             # time.Time
β”œβ”€β”€ thisUpdate             # time.Time
β”œβ”€β”€ nextUpdate             # time.Time
β”œβ”€β”€ revocationReason       # Integer (if revoked)
β”œβ”€β”€ signatureAlgorithm     # Same as certificate
β”œβ”€β”€ nonce                  # RFC 9654 nonce extension
β”‚   β”œβ”€β”€ present            # Boolean
β”‚   β”œβ”€β”€ value              # []byte (raw nonce)
β”‚   β”œβ”€β”€ length             # Integer (bytes)
β”‚   └── hexValue           # String (hex representation)
└── responderID            # Responder identification

P2 Migration Notes

  • Custom policies relying on catalog membership for critical-extension acceptance may now fail closed; only the conservative certificate processed set authorizes a critical extension.
  • Test optional/default extension fields through their *Present nodes rather than treating zero as absence, and check the concrete extension's malformed child before consuming decoded values.
  • pathLenValid uses the supplied leaf-to-root chain and excludes self-issued intermediates. It does not build or establish trust in a path.
  • Add --purpose only when application-specific EKU enforcement is desired; omitting it preserves conditional-skip behavior.

πŸ”§ Development

go build -o pcl ./cmd/pcl
go test -v -race ./...
golangci-lint run ./...