Compare commits
3 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 16497f2ec1 | |||
| 538b968651 | |||
| 231d6f1873 |
5 changed files with 334 additions and 55 deletions
22
.drone-manifest.yml
Normal file
22
.drone-manifest.yml
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
image: happydomain/checker-legacy-records:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}latest{{/if}}
|
||||
{{#if build.tags}}
|
||||
tags:
|
||||
{{#each build.tags}}
|
||||
- {{this}}
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
manifests:
|
||||
- image: happydomain/checker-legacy-records:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-amd64
|
||||
platform:
|
||||
architecture: amd64
|
||||
os: linux
|
||||
- image: happydomain/checker-legacy-records:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm64
|
||||
platform:
|
||||
architecture: arm64
|
||||
os: linux
|
||||
variant: v8
|
||||
- image: happydomain/checker-legacy-records:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm
|
||||
platform:
|
||||
architecture: arm
|
||||
os: linux
|
||||
variant: v7
|
||||
187
.drone.yml
Normal file
187
.drone.yml
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
---
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: build-amd64
|
||||
|
||||
platform:
|
||||
os: linux
|
||||
arch: amd64
|
||||
|
||||
steps:
|
||||
- name: checker build
|
||||
image: golang:1-alpine
|
||||
commands:
|
||||
- apk add --no-cache git make
|
||||
- make
|
||||
environment:
|
||||
CHECKER_VERSION: "${DRONE_BRANCH}-${DRONE_COMMIT}"
|
||||
CGO_ENABLED: 0
|
||||
when:
|
||||
event:
|
||||
exclude:
|
||||
- tag
|
||||
|
||||
- name: checker build tag
|
||||
image: golang:1-alpine
|
||||
commands:
|
||||
- apk add --no-cache git make
|
||||
- make
|
||||
environment:
|
||||
CHECKER_VERSION: "${DRONE_SEMVER}"
|
||||
CGO_ENABLED: 0
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
|
||||
- name: publish on Docker Hub
|
||||
image: plugins/docker
|
||||
settings:
|
||||
repo: happydomain/checker-legacy-records
|
||||
auto_tag: true
|
||||
auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
|
||||
dockerfile: Dockerfile
|
||||
build_args:
|
||||
- CHECKER_VERSION=${DRONE_BRANCH}-${DRONE_COMMIT}
|
||||
username:
|
||||
from_secret: docker_username
|
||||
password:
|
||||
from_secret: docker_password
|
||||
when:
|
||||
event:
|
||||
exclude:
|
||||
- tag
|
||||
|
||||
- name: publish on Docker Hub (tag)
|
||||
image: plugins/docker
|
||||
settings:
|
||||
repo: happydomain/checker-legacy-records
|
||||
auto_tag: true
|
||||
auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
|
||||
dockerfile: Dockerfile
|
||||
build_args:
|
||||
- CHECKER_VERSION=${DRONE_SEMVER}
|
||||
username:
|
||||
from_secret: docker_username
|
||||
password:
|
||||
from_secret: docker_password
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
|
||||
trigger:
|
||||
branch:
|
||||
exclude:
|
||||
- renovate/*
|
||||
event:
|
||||
- cron
|
||||
- push
|
||||
- tag
|
||||
|
||||
---
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: build-arm64
|
||||
|
||||
platform:
|
||||
os: linux
|
||||
arch: arm64
|
||||
|
||||
steps:
|
||||
- name: checker build
|
||||
image: golang:1-alpine
|
||||
commands:
|
||||
- apk add --no-cache git make
|
||||
- make
|
||||
environment:
|
||||
CHECKER_VERSION: "${DRONE_BRANCH}-${DRONE_COMMIT}"
|
||||
CGO_ENABLED: 0
|
||||
when:
|
||||
event:
|
||||
exclude:
|
||||
- tag
|
||||
|
||||
- name: checker build tag
|
||||
image: golang:1-alpine
|
||||
commands:
|
||||
- apk add --no-cache git make
|
||||
- make
|
||||
environment:
|
||||
CHECKER_VERSION: "${DRONE_SEMVER}"
|
||||
CGO_ENABLED: 0
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
|
||||
- name: publish on Docker Hub
|
||||
image: plugins/docker
|
||||
settings:
|
||||
repo: happydomain/checker-legacy-records
|
||||
auto_tag: true
|
||||
auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
|
||||
dockerfile: Dockerfile
|
||||
build_args:
|
||||
- CHECKER_VERSION=${DRONE_BRANCH}-${DRONE_COMMIT}
|
||||
username:
|
||||
from_secret: docker_username
|
||||
password:
|
||||
from_secret: docker_password
|
||||
when:
|
||||
event:
|
||||
exclude:
|
||||
- tag
|
||||
|
||||
- name: publish on Docker Hub (tag)
|
||||
image: plugins/docker
|
||||
settings:
|
||||
repo: happydomain/checker-legacy-records
|
||||
auto_tag: true
|
||||
auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
|
||||
dockerfile: Dockerfile
|
||||
build_args:
|
||||
- CHECKER_VERSION=${DRONE_SEMVER}
|
||||
username:
|
||||
from_secret: docker_username
|
||||
password:
|
||||
from_secret: docker_password
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
|
||||
trigger:
|
||||
event:
|
||||
- cron
|
||||
- push
|
||||
- tag
|
||||
|
||||
---
|
||||
kind: pipeline
|
||||
name: docker-manifest
|
||||
|
||||
platform:
|
||||
os: linux
|
||||
arch: arm64
|
||||
|
||||
steps:
|
||||
- name: publish on Docker Hub
|
||||
image: plugins/manifest
|
||||
settings:
|
||||
auto_tag: true
|
||||
ignore_missing: true
|
||||
spec: .drone-manifest.yml
|
||||
username:
|
||||
from_secret: docker_username
|
||||
password:
|
||||
from_secret: docker_password
|
||||
|
||||
trigger:
|
||||
branch:
|
||||
exclude:
|
||||
- renovate/*
|
||||
event:
|
||||
- cron
|
||||
- push
|
||||
- tag
|
||||
|
||||
depends_on:
|
||||
- build-amd64
|
||||
- build-arm64
|
||||
|
|
@ -20,6 +20,12 @@ finding.
|
|||
| Warning | `SPF`, `A6`, `MD`, `MF` | RFC 7208 / RFC 6563 / RFC 973: replaced by TXT, AAAA, MX. |
|
||||
| Informational| `WKS`, `MB`, `MG`, `MR`, `MINFO`, `NULL`, `GPOS`, `NSAP`, `NSAP-PTR`, `X25`, `ISDN`, `RT`, `ATMA`, `EID`, `NIMLOC`, `SINK`, `NINFO`, `RKEY` | Experimental or historical (RFC 1035, 1183, 1706, 1712, ...); safe to delete. |
|
||||
|
||||
## Rules
|
||||
|
||||
| Code | Description | Severity |
|
||||
|-------------------|---------------------------------------------------------------------------------------------------|---------------------|
|
||||
| `legacy_records` | Detects DNS record types deprecated by the IETF and reports each occurrence with RFC references. | CRITICAL |
|
||||
|
||||
## Tests
|
||||
|
||||
`go test ./...` covers:
|
||||
|
|
|
|||
|
|
@ -12,11 +12,10 @@ import (
|
|||
// GetHTMLReport renders the legacy-records observation as a self-contained
|
||||
// HTML page suitable for iframe embedding.
|
||||
//
|
||||
// The "fix this first" card is driven by the most-severe finding (no fixed
|
||||
// rule wins by name): SeverityCrit > SeverityWarn > SeverityInfo, with the
|
||||
// alphabetically-first type name as a stable tie-break. This matches what
|
||||
// the rule sorter produces, so the top card and the rule output never
|
||||
// disagree on which finding is "the" priority.
|
||||
// Cards are built from ctx.States(): each finding state carries the full
|
||||
// metadata (reason, replacement, how-to-fix, locations) in CheckState.Meta.
|
||||
// The rule already emits states in descending severity order, so the first
|
||||
// card is always the top priority without re-sorting here.
|
||||
func (p *legacyProvider) GetHTMLReport(ctx sdk.ReportContext) (string, error) {
|
||||
var data LegacyData
|
||||
if raw := ctx.Data(); len(raw) > 0 {
|
||||
|
|
@ -65,48 +64,131 @@ func buildReportView(data *LegacyData, states []sdk.CheckState) *reportView {
|
|||
CollectErrors: data.CollectErrors,
|
||||
}
|
||||
|
||||
groups := groupFindings(data.Findings)
|
||||
cards := make([]findingCard, 0, len(groups))
|
||||
worst := SeverityInfo
|
||||
for _, g := range groups {
|
||||
info := deprecatedTypes[g.Rrtype]
|
||||
if info.Severity > worst {
|
||||
worst = info.Severity
|
||||
if len(states) == 0 {
|
||||
// No rule output yet: data-only rendering with a neutral headline.
|
||||
v.OverallStatus = "ok"
|
||||
v.OverallText = fmt.Sprintf("Data collected — %d service(s) scanned.", data.ServicesScanned)
|
||||
v.OverallClass = "status-ok"
|
||||
return v
|
||||
}
|
||||
cards = append(cards, findingCard{
|
||||
TypeName: g.TypeName,
|
||||
Reason: info.Reason,
|
||||
Replacement: info.Replacement,
|
||||
HowToFix: info.HowToFix,
|
||||
Severity: severityLabel(info.Severity),
|
||||
SeverityCSS: info.Severity.String(),
|
||||
Count: len(g.Locations),
|
||||
Locations: g.Locations,
|
||||
})
|
||||
|
||||
var cards []findingCard
|
||||
for _, st := range states {
|
||||
c, ok := cardFromState(st)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
cards = append(cards, c)
|
||||
}
|
||||
|
||||
if len(cards) > 0 {
|
||||
v.Top = &cards[0]
|
||||
v.Others = cards[1:]
|
||||
v.OverallStatus = worst.String()
|
||||
v.OverallText, v.OverallClass = overallLabel(worst)
|
||||
} else {
|
||||
// Honour the rule's status when present: an Error from the rule
|
||||
// (e.g. observation load failure) must not be masked as "OK".
|
||||
if errState, ok := firstErrorState(states); ok {
|
||||
worst := worstFindingStatus(states)
|
||||
v.OverallStatus, v.OverallText, v.OverallClass = overallFromStatus(worst)
|
||||
} else if errState, ok := firstErrorState(states); ok {
|
||||
v.OverallStatus = "error"
|
||||
v.OverallText = errState.Message
|
||||
v.OverallClass = "status-crit"
|
||||
} else {
|
||||
v.OverallStatus = "ok"
|
||||
v.OverallText = fmt.Sprintf("No legacy record types found across %d service(s).", data.ServicesScanned)
|
||||
v.OverallClass = "status-ok"
|
||||
v.OverallText = fmt.Sprintf("No legacy record types found across %d service(s).", data.ServicesScanned)
|
||||
for _, st := range states {
|
||||
if st.Code == "legacy_records_clean" {
|
||||
v.OverallText = st.Message
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return v
|
||||
}
|
||||
|
||||
// cardFromState builds a findingCard from a finding CheckState. States that
|
||||
// carry no "locations" metadata (clean / error / skip states) return ok=false.
|
||||
func cardFromState(st sdk.CheckState) (findingCard, bool) {
|
||||
if st.Meta == nil {
|
||||
return findingCard{}, false
|
||||
}
|
||||
rawLocs, ok := st.Meta["locations"]
|
||||
if !ok {
|
||||
return findingCard{}, false
|
||||
}
|
||||
locations := decodeLocations(rawLocs)
|
||||
|
||||
typeName, _ := st.Meta["type"].(string)
|
||||
if typeName == "" {
|
||||
typeName = st.Subject
|
||||
}
|
||||
reason, _ := st.Meta["reason"].(string)
|
||||
replacement, _ := st.Meta["replacement"].(string)
|
||||
howToFix, _ := st.Meta["how_to_fix"].(string)
|
||||
sevLabel, sevCSS := severityFromStatus(st.Status)
|
||||
|
||||
return findingCard{
|
||||
TypeName: typeName,
|
||||
Reason: reason,
|
||||
Replacement: replacement,
|
||||
HowToFix: howToFix,
|
||||
Severity: sevLabel,
|
||||
SeverityCSS: sevCSS,
|
||||
Count: len(locations),
|
||||
Locations: locations,
|
||||
}, true
|
||||
}
|
||||
|
||||
// decodeLocations handles the JSON round-trip: Meta values are stored as any
|
||||
// but pass through json.Marshal/Unmarshal when transmitted, so []FindingLocation
|
||||
// arrives as []interface{} of map[string]interface{}. Re-marshaling restores
|
||||
// the typed slice.
|
||||
func decodeLocations(v any) []FindingLocation {
|
||||
b, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
var locs []FindingLocation
|
||||
if err := json.Unmarshal(b, &locs); err != nil {
|
||||
return nil
|
||||
}
|
||||
return locs
|
||||
}
|
||||
|
||||
func severityFromStatus(s sdk.Status) (label, css string) {
|
||||
switch s {
|
||||
case sdk.StatusCrit, sdk.StatusError:
|
||||
return "Critical", "crit"
|
||||
case sdk.StatusWarn:
|
||||
return "Warning", "warn"
|
||||
default:
|
||||
return "Informational", "info"
|
||||
}
|
||||
}
|
||||
|
||||
func worstFindingStatus(states []sdk.CheckState) sdk.Status {
|
||||
worst := sdk.StatusInfo
|
||||
for _, st := range states {
|
||||
switch st.Status {
|
||||
case sdk.StatusCrit, sdk.StatusError:
|
||||
return sdk.StatusCrit
|
||||
case sdk.StatusWarn:
|
||||
worst = sdk.StatusWarn
|
||||
}
|
||||
}
|
||||
return worst
|
||||
}
|
||||
|
||||
func overallFromStatus(s sdk.Status) (status, text, css string) {
|
||||
switch s {
|
||||
case sdk.StatusCrit, sdk.StatusError:
|
||||
return "crit", "Legacy records require urgent migration", "status-crit"
|
||||
case sdk.StatusWarn:
|
||||
return "warn", "Legacy records should be migrated", "status-warn"
|
||||
default:
|
||||
return "info", "Only informational legacy records found", "status-info"
|
||||
}
|
||||
}
|
||||
|
||||
func firstErrorState(states []sdk.CheckState) (sdk.CheckState, bool) {
|
||||
for i := range states {
|
||||
if states[i].Status == sdk.StatusError {
|
||||
|
|
@ -116,28 +198,6 @@ func firstErrorState(states []sdk.CheckState) (sdk.CheckState, bool) {
|
|||
return sdk.CheckState{}, false
|
||||
}
|
||||
|
||||
func severityLabel(s DeprecatedSeverity) string {
|
||||
switch s {
|
||||
case SeverityCrit:
|
||||
return "Critical"
|
||||
case SeverityWarn:
|
||||
return "Warning"
|
||||
default:
|
||||
return "Informational"
|
||||
}
|
||||
}
|
||||
|
||||
func overallLabel(s DeprecatedSeverity) (text, css string) {
|
||||
switch s {
|
||||
case SeverityCrit:
|
||||
return "Legacy records require urgent migration", "status-crit"
|
||||
case SeverityWarn:
|
||||
return "Legacy records should be migrated", "status-warn"
|
||||
default:
|
||||
return "Only informational legacy records found", "status-info"
|
||||
}
|
||||
}
|
||||
|
||||
var reportTmpl = template.Must(template.New("legacy-records-report").Funcs(template.FuncMap{
|
||||
"display": func(s string) string {
|
||||
if s == "" || s == "@" {
|
||||
|
|
|
|||
|
|
@ -184,8 +184,11 @@ func TestReport_TopCardMatchesWorstSeverity(t *testing.T) {
|
|||
},
|
||||
}
|
||||
data := runCollect(t, z)
|
||||
raw := mustMarshal(t, data)
|
||||
states := (&legacyRecordsRule{}).Evaluate(context.Background(),
|
||||
staticObs{key: ObservationKeyLegacy, payload: raw}, sdk.CheckerOptions{})
|
||||
|
||||
html, err := (&legacyProvider{}).GetHTMLReport(staticReportCtx{data: mustMarshal(t, data)})
|
||||
html, err := (&legacyProvider{}).GetHTMLReport(staticReportCtx{data: raw, states: states})
|
||||
if err != nil {
|
||||
t.Fatalf("GetHTMLReport: %v", err)
|
||||
}
|
||||
|
|
@ -239,8 +242,9 @@ func (s staticObs) GetRelated(_ context.Context, _ sdk.ObservationKey) ([]sdk.Re
|
|||
|
||||
type staticReportCtx struct {
|
||||
data []byte
|
||||
states []sdk.CheckState
|
||||
}
|
||||
|
||||
func (s staticReportCtx) Data() json.RawMessage { return s.data }
|
||||
func (s staticReportCtx) Related(_ sdk.ObservationKey) []sdk.RelatedObservation { return nil }
|
||||
func (s staticReportCtx) States() []sdk.CheckState { return nil }
|
||||
func (s staticReportCtx) States() []sdk.CheckState { return s.states }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue