package lints

/*
 * ZLint Copyright 2017 Regents of the University of Michigan
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy
 * of the License at http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
 * implied. See the License for the specific language governing
 * permissions and limitations under the License.
 */

import (
	"time"

	"github.com/zmap/zcrypto/x509"
	"github.com/zmap/zlint/util"
)

var (
	// Lints is a map of all known lints by name. Add a Lint to the map by calling
	// RegisterLint.
	Lints = make(map[string]*Lint)
)

// LintInterface is implemented by each Lint.
type LintInterface interface {
	// Initialize runs once per-lint. It is called during RegisterLint().
	Initialize() error

	// CheckApplies runs once per certificate. It returns true if the Lint should
	// run on the given certificate. If CheckApplies returns false, the Lint
	// result is automatically set to NA without calling CheckEffective() or
	// Run().
	CheckApplies(c *x509.Certificate) bool

	// Execute() is the body of the lint. It is called for every certificate for
	// which CheckApplies() returns true.
	Execute(c *x509.Certificate) *LintResult
}

// An Enum to programmatically represent the source of a lint
type LintSource int

const (
	UnknownLintSource LintSource = iota
	CABFBaselineRequirements
	RFC5280
	RFC5480
	RFC5891
	ZLint
	AWSLabs
	EtsiEsi // ETSI - Electronic Signatures and Infrastructures (ESI)
	CABFEVGuidelines
	AppleCTPolicy // https://support.apple.com/en-us/HT205280
)

// A Lint struct represents a single lint, e.g.
// "e_basic_constraints_not_critical". It contains an implementation of LintInterface.
type Lint struct {

	// Name is a lowercase underscore-separated string describing what a given
	// Lint checks. If Name beings with "w", the lint MUST NOT return Error, only
	// Warn. If Name beings with "e", the Lint MUST NOT return Warn, only Error.
	Name string `json:"name,omitempty"`

	// A human-readable description of what the Lint checks. Usually copied
	// directly from the CA/B Baseline Requirements or RFC 5280.
	Description string `json:"description,omitempty"`

	// The source of the check, e.g. "BRs: 6.1.6" or "RFC 5280: 4.1.2.6".
	Citation string `json:"citation,omitempty"`

	// Programmatic source of the check, BRs, RFC5280, or ZLint
	Source LintSource `json:"-"`

	// Lints automatically returns NE for all certificates where CheckApplies() is
	// true but with NotBefore < EffectiveDate. This check is bypassed if
	// EffectiveDate is zero.
	EffectiveDate time.Time `json:"-"`

	// The implementation of the lint logic.
	Lint LintInterface `json:"-"`
}

// CheckEffective returns true if c was issued on or after the EffectiveDate. If
// EffectiveDate is zero, CheckEffective always returns true.
func (l *Lint) CheckEffective(c *x509.Certificate) bool {
	if l.EffectiveDate.IsZero() || !l.EffectiveDate.After(c.NotBefore) {
		return true
	}
	return false
}

// Execute runs the lint against a certificate. For lints that are
// sourced from the CA/B Forum Baseline Requirements, we first determine
// if they are within the purview of the BRs. See LintInterface for details
// about the other methods called. The ordering is as follows:
//
// CheckApplies()
// CheckEffective()
// Execute()
func (l *Lint) Execute(cert *x509.Certificate) *LintResult {
	if l.Source == CABFBaselineRequirements && !util.IsServerAuthCert(cert) {
		return &LintResult{Status: NA}
	}
	if !l.Lint.CheckApplies(cert) {
		return &LintResult{Status: NA}
	} else if !l.CheckEffective(cert) {
		return &LintResult{Status: NE}
	}
	res := l.Lint.Execute(cert)
	return res
}

// RegisterLint must be called once for each lint to be excuted. Duplicate lint
// names are squashed. Normally, RegisterLint is called during init().
func RegisterLint(l *Lint) {
	if err := l.Lint.Initialize(); err != nil {
		panic("could not initialize lint: " + l.Name + ": " + err.Error())
	}
	Lints[l.Name] = l
}
