Initial import
This commit is contained in:
42
policy/input/net.go
Normal file
42
policy/input/net.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package input
|
||||
|
||||
import (
|
||||
"net"
|
||||
)
|
||||
|
||||
// Addr represents a [net.Addr].
|
||||
type Addr struct {
|
||||
Network string `json:"network"` // Type of address.
|
||||
IP string `json:"ip"` // IP address.
|
||||
Port int `json:"port,omitempty"` // Port (if any).
|
||||
}
|
||||
|
||||
func NewAddr(addr net.Addr) *Addr {
|
||||
if addr == nil {
|
||||
return nil
|
||||
}
|
||||
switch addr := addr.(type) {
|
||||
case *net.IPAddr:
|
||||
return &Addr{
|
||||
Network: addr.Network(),
|
||||
IP: addr.IP.String(),
|
||||
}
|
||||
case *net.TCPAddr:
|
||||
return &Addr{
|
||||
Network: addr.Network(),
|
||||
IP: addr.IP.String(),
|
||||
Port: addr.Port,
|
||||
}
|
||||
case *net.UDPAddr:
|
||||
return &Addr{
|
||||
Network: addr.Network(),
|
||||
IP: addr.IP.String(),
|
||||
Port: addr.Port,
|
||||
}
|
||||
default:
|
||||
return &Addr{
|
||||
Network: addr.Network(),
|
||||
IP: addr.String(),
|
||||
}
|
||||
}
|
||||
}
|
98
policy/input/ssh.go
Normal file
98
policy/input/ssh.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package input
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
|
||||
"git.maze.io/maze/conduit/ssh/sshutil"
|
||||
)
|
||||
|
||||
// Certificate represents a [ssh.Certificate].
|
||||
type Certificate struct {
|
||||
Nonce []byte `json:"nonce"`
|
||||
Key *PublicKey `json:"key"`
|
||||
Serial uint64 `json:"serial"`
|
||||
CertType uint32 `json:"type"`
|
||||
KeyId string `json:"key_id"`
|
||||
ValidPrincipals []string `json:"valid_principals"`
|
||||
ValidAfter *time.Time `json:"valid_after"`
|
||||
ValidBefore *time.Time `json:"valid_before"`
|
||||
SignatureKey *PublicKey `json:"signature_key"`
|
||||
Signature []byte `json:"signature"`
|
||||
SignatureFormat string `json:"signature_format"`
|
||||
}
|
||||
|
||||
// NewCertificate converts an [ssh.Certificate] to [Certificate] input.
|
||||
func NewCertificate(cert *ssh.Certificate) *Certificate {
|
||||
if cert == nil {
|
||||
return nil
|
||||
}
|
||||
c := &Certificate{
|
||||
Nonce: cert.Nonce,
|
||||
Key: NewPublicKey(cert.Key),
|
||||
Serial: cert.Serial,
|
||||
CertType: cert.CertType,
|
||||
KeyId: cert.KeyId,
|
||||
ValidPrincipals: cert.ValidPrincipals,
|
||||
SignatureKey: NewPublicKey(cert.SignatureKey),
|
||||
}
|
||||
if cert.ValidAfter > 0 {
|
||||
t := time.Unix(int64(cert.ValidAfter), 0)
|
||||
c.ValidAfter = &t
|
||||
}
|
||||
if cert.ValidBefore > 0 {
|
||||
t := time.Unix(int64(cert.ValidBefore), 0)
|
||||
c.ValidBefore = &t
|
||||
}
|
||||
|
||||
if cert.Signature != nil {
|
||||
c.Signature = cert.Signature.Blob
|
||||
c.SignatureFormat = cert.Signature.Format
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// ConnMetadata is a Rego input that represents a [ssh.ConnMetadata].
|
||||
type ConnMetadata struct {
|
||||
User string `json:"user"` // User is the user ID for this connection.
|
||||
SessionID []byte `json:"session_id"` // SessionID is the session hash, also denoted by H.
|
||||
ClientVersion string `json:"client_version"` // ClientVersion is the client's version.
|
||||
ServerVersion string `json:"server_version"` // ServerVersion is the server's version
|
||||
RemoteAddr *Addr `json:"remote_addr"` // RemoteAddr is the remote address for this connection.
|
||||
LocalAddr *Addr `json:"local_addr"` // LocalAddr is the local address for this connection.
|
||||
}
|
||||
|
||||
// NewConnMetadata converts an [ssh.ConnMetadata] to [ConnMetadata] input.
|
||||
func NewConnMetadata(meta ssh.ConnMetadata) *ConnMetadata {
|
||||
if meta == nil {
|
||||
return nil
|
||||
}
|
||||
return &ConnMetadata{
|
||||
User: meta.User(),
|
||||
SessionID: meta.SessionID(),
|
||||
ClientVersion: string(meta.ClientVersion()),
|
||||
ServerVersion: string(meta.ServerVersion()),
|
||||
RemoteAddr: NewAddr(meta.RemoteAddr()),
|
||||
LocalAddr: NewAddr(meta.LocalAddr()),
|
||||
}
|
||||
}
|
||||
|
||||
// PublicKey represents a [ssh.PublicKey].
|
||||
type PublicKey struct {
|
||||
Type string `json:"type"`
|
||||
Bits int `json:"bits"`
|
||||
Fingerprint string `json:"fingerprint"`
|
||||
}
|
||||
|
||||
// NewPublicKey converts an [ssh.PublicKey] to [PublicKey] input.
|
||||
func NewPublicKey(key ssh.PublicKey) *PublicKey {
|
||||
if key == nil {
|
||||
return nil
|
||||
}
|
||||
return &PublicKey{
|
||||
Type: sshutil.KeyType(key),
|
||||
Bits: sshutil.KeyBits(key),
|
||||
Fingerprint: ssh.FingerprintSHA256(key),
|
||||
}
|
||||
}
|
157
policy/policy.go
Normal file
157
policy/policy.go
Normal file
@@ -0,0 +1,157 @@
|
||||
package policy
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/go-viper/mapstructure/v2"
|
||||
"github.com/open-policy-agent/opa/v1/rego"
|
||||
|
||||
"git.maze.io/maze/conduit/logger"
|
||||
)
|
||||
|
||||
type Policy interface {
|
||||
// Package name.
|
||||
Package() string
|
||||
|
||||
// Query is the policy query.
|
||||
Query(query string, input, result any) error
|
||||
|
||||
// Verify the policy definition.
|
||||
Verify() error
|
||||
}
|
||||
|
||||
type Loader interface {
|
||||
LoadPolicy(name string) (Policy, error)
|
||||
}
|
||||
|
||||
type FileSystemLoader struct {
|
||||
Root string
|
||||
}
|
||||
|
||||
func (l FileSystemLoader) LoadPolicy(name string) (Policy, error) {
|
||||
path := name
|
||||
if !strings.ContainsRune(filepath.Base(name), '.') || filepath.Ext(path) == "" {
|
||||
path += ".rego"
|
||||
}
|
||||
|
||||
log := logger.StandardLog.Value("policy", path)
|
||||
if !filepath.IsAbs(path) {
|
||||
var err error
|
||||
if path, err = filepath.Abs(filepath.Join(l.Root, path)); err != nil {
|
||||
log.Err(err).Warn("Error resolving absolute policy path")
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
b, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
log.Err(err).Value("path", path).Warn("Error reading policy file")
|
||||
return nil, err
|
||||
}
|
||||
pkg, err := decodePackage(b)
|
||||
if err != nil {
|
||||
log.Err(err).Warn("Error decoding policy package")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &policyFile{
|
||||
path: []string{path},
|
||||
pkg: pkg,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func decodePackage(b []byte) (name string, err error) {
|
||||
s := bufio.NewScanner(bytes.NewReader(b))
|
||||
for s.Scan() {
|
||||
line := s.Text()
|
||||
if strings.HasPrefix(line, "package ") {
|
||||
return strings.TrimSpace(line[len("package "):]), nil
|
||||
}
|
||||
}
|
||||
if err = s.Err(); err != nil {
|
||||
return
|
||||
}
|
||||
return "", errors.New("policy: no package name found")
|
||||
}
|
||||
|
||||
type policyFile struct {
|
||||
path []string
|
||||
pkg string
|
||||
}
|
||||
|
||||
func (p policyFile) Package() string {
|
||||
return p.pkg
|
||||
}
|
||||
|
||||
func (p policyFile) Query(query string, input, value any) error {
|
||||
log := logger.StandardLog.Values(logger.Values{
|
||||
"policy": p.path[0],
|
||||
"package": p.pkg,
|
||||
"query": query,
|
||||
})
|
||||
log.Trace("Policy query evaluating")
|
||||
|
||||
options := []func(*rego.Rego){
|
||||
rego.Dump(os.Stderr),
|
||||
rego.Query(query),
|
||||
rego.Load(p.path, nil),
|
||||
rego.Strict(true),
|
||||
}
|
||||
|
||||
if input != nil {
|
||||
debug := json.NewEncoder(os.Stdout)
|
||||
debug.SetIndent("", " ")
|
||||
debug.Encode(input)
|
||||
options = append(options, rego.Input(input))
|
||||
}
|
||||
|
||||
ctx := context.TODO()
|
||||
q, err := rego.New(options...).PrepareForEval(ctx)
|
||||
if err != nil {
|
||||
log.Err(err).Warn("Policy query prepare failed")
|
||||
return err
|
||||
}
|
||||
results, err := q.Eval(ctx)
|
||||
if err != nil {
|
||||
log.Err(err).Warn("Policy evaluation failed")
|
||||
return err
|
||||
}
|
||||
|
||||
log = log.Value("results", len(results))
|
||||
if value == nil {
|
||||
log.Debug("Policy query results processing ended, nil return value")
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Debug("Policy query results processing")
|
||||
mapped := make(map[string]any)
|
||||
for _, result := range results {
|
||||
for _, expr := range result.Expressions {
|
||||
if value, ok := expr.Value.(map[string]any); ok {
|
||||
for k, v := range value {
|
||||
mapped[k] = v
|
||||
}
|
||||
} else {
|
||||
log.Debugf("Unhandled expression value %T", expr)
|
||||
}
|
||||
}
|
||||
}
|
||||
log.Value("mapped", mapped).Debug("Policy query results done")
|
||||
|
||||
return mapstructure.Decode(mapped, value)
|
||||
}
|
||||
|
||||
func (p policyFile) Verify() error {
|
||||
_, err := rego.New(
|
||||
rego.Strict(true),
|
||||
rego.Load(p.path, nil),
|
||||
).PrepareForEval(context.TODO())
|
||||
return err
|
||||
}
|
Reference in New Issue
Block a user