1package models
2
3import (
4	"bytes"
5	"fmt"
6	"regexp"
7	"strings"
8
9	"golang.org/x/xerrors"
10)
11
12// Packages is Map of Package
13// { "package-name": Package }
14type Packages map[string]Package
15
16// NewPackages create Packages
17func NewPackages(packs ...Package) Packages {
18	m := Packages{}
19	for _, pack := range packs {
20		m[pack.Name] = pack
21	}
22	return m
23}
24
25// MergeNewVersion merges candidate version information to the receiver struct
26func (ps Packages) MergeNewVersion(as Packages) {
27	for name, pack := range ps {
28		pack.NewVersion = pack.Version
29		pack.NewRelease = pack.Release
30		ps[name] = pack
31	}
32
33	for _, a := range as {
34		if pack, ok := ps[a.Name]; ok {
35			pack.NewVersion = a.NewVersion
36			pack.NewRelease = a.NewRelease
37			pack.Repository = a.Repository
38			ps[a.Name] = pack
39		}
40	}
41}
42
43// Merge returns merged map (immutable)
44func (ps Packages) Merge(other Packages) Packages {
45	merged := Packages{}
46	for k, v := range ps {
47		merged[k] = v
48	}
49	for k, v := range other {
50		merged[k] = v
51	}
52	return merged
53}
54
55// FindOne search a element
56func (ps Packages) FindOne(f func(Package) bool) (string, Package, bool) {
57	for key, p := range ps {
58		if f(p) {
59			return key, p, true
60		}
61	}
62	return "", Package{}, false
63}
64
65// FindByFQPN search a package by Fully-Qualified-Package-Name
66func (ps Packages) FindByFQPN(nameVerRelArc string) (*Package, error) {
67	for _, p := range ps {
68		if nameVerRelArc == p.FQPN() {
69			return &p, nil
70		}
71	}
72	return nil, xerrors.Errorf("Failed to find the package: %s", nameVerRelArc)
73}
74
75// Package has installed binary packages.
76type Package struct {
77	Name             string               `json:"name"`
78	Version          string               `json:"version"`
79	Release          string               `json:"release"`
80	NewVersion       string               `json:"newVersion"`
81	NewRelease       string               `json:"newRelease"`
82	Arch             string               `json:"arch"`
83	Repository       string               `json:"repository"`
84	Changelog        Changelog            `json:"changelog"`
85	AffectedProcs    []AffectedProcess    `json:",omitempty"`
86	NeedRestartProcs []NeedRestartProcess `json:",omitempty"`
87}
88
89// FQPN returns Fully-Qualified-Package-Name
90// name-version-release.arch
91func (p Package) FQPN() string {
92	fqpn := p.Name
93	if p.Version != "" {
94		fqpn += fmt.Sprintf("-%s", p.Version)
95	}
96	if p.Release != "" {
97		fqpn += fmt.Sprintf("-%s", p.Release)
98	}
99	if p.Arch != "" {
100		fqpn += fmt.Sprintf(".%s", p.Arch)
101	}
102	return fqpn
103}
104
105// FormatVer returns package version-release
106func (p Package) FormatVer() string {
107	ver := p.Version
108	if 0 < len(p.Release) {
109		ver = fmt.Sprintf("%s-%s", ver, p.Release)
110	}
111	return ver
112}
113
114// FormatNewVer returns package version-release
115func (p Package) FormatNewVer() string {
116	ver := p.NewVersion
117	if 0 < len(p.NewRelease) {
118		ver = fmt.Sprintf("%s-%s", ver, p.NewRelease)
119	}
120	return ver
121}
122
123// FormatVersionFromTo formats installed and new package version
124func (p Package) FormatVersionFromTo(stat PackageFixStatus) string {
125	to := p.FormatNewVer()
126	if stat.NotFixedYet {
127		if stat.FixState != "" {
128			to = stat.FixState
129		} else {
130			to = "Not Fixed Yet"
131		}
132	} else if p.NewVersion == "" {
133		to = "Unknown"
134	}
135	var fixedIn string
136	if stat.FixedIn != "" {
137		fixedIn = fmt.Sprintf(" (FixedIn: %s)", stat.FixedIn)
138	}
139	return fmt.Sprintf("%s-%s -> %s%s",
140		p.Name, p.FormatVer(), to, fixedIn)
141}
142
143// FormatChangelog formats the changelog
144func (p Package) FormatChangelog() string {
145	buf := []string{}
146	packVer := fmt.Sprintf("%s-%s -> %s",
147		p.Name, p.FormatVer(), p.FormatNewVer())
148	var delim bytes.Buffer
149	for i := 0; i < len(packVer); i++ {
150		delim.WriteString("-")
151	}
152
153	clog := p.Changelog.Contents
154	if lines := strings.Split(clog, "\n"); len(lines) != 0 {
155		clog = strings.Join(lines[0:len(lines)-1], "\n")
156	}
157
158	switch p.Changelog.Method {
159	case FailedToGetChangelog:
160		clog = "No changelogs"
161	case FailedToFindVersionInChangelog:
162		clog = "Failed to parse changelogs. For details, check yourself"
163	}
164	buf = append(buf, packVer, delim.String(), clog)
165	return strings.Join(buf, "\n")
166}
167
168// Changelog has contents of changelog and how to get it.
169// Method: models.detectionMethodStr
170type Changelog struct {
171	Contents string          `json:"contents"`
172	Method   DetectionMethod `json:"method"`
173}
174
175// AffectedProcess keep a processes information affected by software update
176type AffectedProcess struct {
177	PID             string     `json:"pid,omitempty"`
178	Name            string     `json:"name,omitempty"`
179	ListenPorts     []string   `json:"listenPorts,omitempty"`
180	ListenPortStats []PortStat `json:"listenPortStats,omitempty"`
181}
182
183// PortStat has the result of parsing the port information to the address and port.
184type PortStat struct {
185	BindAddress     string   `json:"bindAddress"`
186	Port            string   `json:"port"`
187	PortReachableTo []string `json:"portReachableTo"`
188}
189
190func NewPortStat(ipPort string) (*PortStat, error) {
191	if ipPort == "" {
192		return &PortStat{}, nil
193	}
194	sep := strings.LastIndex(ipPort, ":")
195	if sep == -1 {
196		return nil, xerrors.Errorf("Failed to parse IP:Port: %s", ipPort)
197	}
198	return &PortStat{
199		BindAddress: ipPort[:sep],
200		Port:        ipPort[sep+1:],
201	}, nil
202}
203
204// HasReachablePort checks if Package.AffectedProcs has PortReachableTo
205func (p Package) HasReachablePort() bool {
206	for _, ap := range p.AffectedProcs {
207		for _, lp := range ap.ListenPortStats {
208			if len(lp.PortReachableTo) > 0 {
209				return true
210			}
211		}
212	}
213	return false
214}
215
216// NeedRestartProcess keep a processes information affected by software update
217type NeedRestartProcess struct {
218	PID         string `json:"pid"`
219	Path        string `json:"path"`
220	ServiceName string `json:"serviceName"`
221	InitSystem  string `json:"initSystem"`
222	HasInit     bool   `json:"-"`
223}
224
225// SrcPackage has installed source package information.
226// Debian based Linux has both of package and source information in dpkg.
227// OVAL database often includes a source version (Not a binary version),
228// so it is also needed to capture source version for OVAL version comparison.
229// https://github.com/future-architect/vuls/issues/504
230type SrcPackage struct {
231	Name        string   `json:"name"`
232	Version     string   `json:"version"`
233	Arch        string   `json:"arch"`
234	BinaryNames []string `json:"binaryNames"`
235}
236
237// AddBinaryName add the name if not exists
238func (s *SrcPackage) AddBinaryName(name string) {
239	found := false
240	for _, n := range s.BinaryNames {
241		if n == name {
242			return
243		}
244	}
245	if !found {
246		s.BinaryNames = append(s.BinaryNames, name)
247	}
248}
249
250// SrcPackages is Map of SrcPackage
251// { "package-name": SrcPackage }
252type SrcPackages map[string]SrcPackage
253
254// FindByBinName finds by bin-package-name
255func (s SrcPackages) FindByBinName(name string) (*SrcPackage, bool) {
256	for _, p := range s {
257		for _, binName := range p.BinaryNames {
258			if binName == name {
259				return &p, true
260			}
261		}
262	}
263	return nil, false
264}
265
266// raspiPackNamePattern is a regular expression pattern to detect the Raspberry Pi specific package from the package name.
267// e.g. libraspberrypi-dev, rpi-eeprom, python3-rpi.gpio, pi-bluetooth
268var raspiPackNamePattern = regexp.MustCompile(`(.*raspberry.*|^rpi.*|.*-rpi.*|^pi-.*)`)
269
270// raspiPackNamePattern is a regular expression pattern to detect the Raspberry Pi specific package from the version.
271// e.g. ffmpeg 7:4.1.4-1+rpt7~deb10u1, vlc 3.0.10-0+deb10u1+rpt2
272var raspiPackVersionPattern = regexp.MustCompile(`.+\+rp(t|i)\d+`)
273
274// raspiPackNameList is a package name array of Raspberry Pi specific packages that are difficult to detect with regular expressions.
275var raspiPackNameList = []string{"piclone", "pipanel", "pishutdown", "piwiz", "pixflat-icons"}
276
277// IsRaspbianPackage judges whether it is a package related to Raspberry Pi from the package name and version
278func IsRaspbianPackage(name, version string) bool {
279	if raspiPackNamePattern.MatchString(name) || raspiPackVersionPattern.MatchString(version) {
280		return true
281	}
282	for _, n := range raspiPackNameList {
283		if n == name {
284			return true
285		}
286	}
287
288	return false
289}
290