cmd/csv2cpe/csv2cpe.go (252 lines of code) (raw):
// Copyright (c) Facebook, Inc. and its affiliates.
//
// 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.
package main
import (
"encoding/csv"
"flag"
"fmt"
"io"
"os"
"sort"
"strconv"
"strings"
"github.com/facebookincubator/flog"
"github.com/facebookincubator/nvdtools/wfn"
)
func main() {
acm := &AttributeColumnMap{}
acm.AddFlags(flag.CommandLine)
idx := flag.Int("i", 0, "column index (after erases) to insert cpe, zero means last")
idelim := flag.String("d", ",", "input column delimiter")
odelim := flag.String("o", "", "output column delimiter, optional")
erase := flag.String("e", "", "comma separated list of columns to erase, optional")
unmap := flag.Bool("x", false, "erase all columns mapped from -cpe_{field}, optional")
lower := flag.Bool("lower", false, "force cpe output to be lower case, optional")
defaultNA := flag.Bool("na", false, "if set, unknown CPE attributes are set to N/A, otherwise to ANY")
flag.Parse()
switch {
case len(*idelim) != 1:
fmt.Fprintln(os.Stderr, "input delimiter must be a single character")
os.Exit(1)
case len(*odelim) > 1:
fmt.Fprintln(os.Stderr, "output delimiter must be a single character")
os.Exit(1)
case len(*odelim) == 0:
*odelim = *idelim
}
var err error
eraseCols := make(IntSet)
if len(*erase) > 0 {
eraseCols, err = NewIntSetFromString(strings.Split(*erase, ",")...)
if err != nil {
fmt.Fprintf(os.Stderr, "invalid column index passed to -e: %v", err)
}
}
if *unmap {
eraseCols.Merge(NewIntSet(acm.Columns()...))
}
p := &Processor{
InputComma: rune((*idelim)[0]),
OutputComma: rune((*odelim)[0]),
CPEToLower: *lower,
DefaultNA: *defaultNA,
CPEOutputColumn: *idx,
EraseInputColumns: eraseCols,
}
err = p.Process(acm, os.Stdin, os.Stdout)
if err != nil {
flog.Fatalln(err)
}
}
// Processor is a CSV processor.
type Processor struct {
InputComma rune // input comma character
OutputComma rune // output comma character
CPEToLower bool // whether the output cpe should be forced lower case
DefaultNA bool // true -> default attribute value is NA, false -> ANY
CPEOutputColumn int // index to add cpe column in the output, after erases
EraseInputColumns IntSet // input columns to erase before output
}
// Process reads CSV from r and writes CSV + CPE to w.
func (p *Processor) Process(acm *AttributeColumnMap, r io.Reader, w io.Writer) error {
reader := csv.NewReader(r)
reader.Comma = p.InputComma
writer := csv.NewWriter(w)
writer.Comma = p.OutputComma
defer writer.Flush()
line := 0
for {
line++
cols, err := reader.Read()
if err != nil {
if err == io.EOF {
break
}
return fmt.Errorf("error parsing line %d: %v", line, err)
}
cpe, err := acm.CPE(cols, p.CPEToLower, p.DefaultNA)
if err != nil {
return fmt.Errorf("error parsing columns in line %d: %v", line, err)
}
cols = RemoveColumns(cols, p.EraseInputColumns)
cols = InsertColumn(cols, cpe, p.CPEOutputColumn)
writer.Write(cols)
}
return nil
}
// AttributeColumnMap maps CSV columns to WFN Attribute fields.
type AttributeColumnMap struct {
Part int
Vendor int
Product int
Version int
Update int
Edition int
SWEdition int
TargetSW int
TargetHW int
Other int
Language int
}
// AddFlags adds configuration flags to the given FlagSet.
func (acm *AttributeColumnMap) AddFlags(fs *flag.FlagSet) {
fs.IntVar(&acm.Part, "cpe_part", 0, "part cpe column index")
fs.IntVar(&acm.Vendor, "cpe_vendor", 0, "vendor cpe column index")
fs.IntVar(&acm.Product, "cpe_product", 0, "product cpe column index")
fs.IntVar(&acm.Version, "cpe_version", 0, "version cpe column index")
fs.IntVar(&acm.Update, "cpe_update", 0, "update cpe column index")
fs.IntVar(&acm.Edition, "cpe_edition", 0, "edition cpe column index")
fs.IntVar(&acm.SWEdition, "cpe_swedition", 0, "swedition cpe column index")
fs.IntVar(&acm.TargetSW, "cpe_targetsw", 0, "targetsw cpe column index")
fs.IntVar(&acm.TargetHW, "cpe_targethw", 0, "targethw cpe column index")
fs.IntVar(&acm.Other, "cpe_other", 0, "other cpe column index")
fs.IntVar(&acm.Language, "cpe_language", 0, "language cpe column index")
}
// CPE returns a CPE by mapping cols to the configured column indices.
func (acm *AttributeColumnMap) CPE(cols []string, lower, na bool) (string, error) {
var err error
var attr *wfn.Attributes
if na {
attr = wfn.NewAttributesWithNA()
} else {
attr = wfn.NewAttributesWithAny()
}
m := map[int]*string{
acm.Part: &attr.Part,
acm.Vendor: &attr.Vendor,
acm.Product: &attr.Product,
acm.Version: &attr.Version,
acm.Update: &attr.Update,
acm.Edition: &attr.Edition,
acm.SWEdition: &attr.SWEdition,
acm.TargetSW: &attr.TargetSW,
acm.TargetHW: &attr.TargetHW,
acm.Other: &attr.Other,
acm.Language: &attr.Language,
}
delete(m, 0)
for i, v := range m {
j := i - 1
if j >= len(cols) {
continue
}
col := cols[j]
if lower {
col = strings.ToLower(col)
}
if i == acm.Version {
for strings.HasSuffix(col, ".") {
col = strings.TrimSuffix(col, ".")
}
}
*v, err = wfn.WFNize(col)
if err != nil {
return "", err
}
}
return attr.BindToURI(), nil
}
// Columns returns a list of columns configured in the map, sorted descending.
func (acm *AttributeColumnMap) Columns() []int {
s := NewIntSet(
acm.Part,
acm.Vendor,
acm.Product,
acm.Version,
acm.Update,
acm.Edition,
acm.SWEdition,
acm.TargetSW,
acm.TargetHW,
acm.Other,
acm.Language,
).ReverseSortedSet()
for i, v := range s {
if v == 0 {
return s[:i]
}
}
return s
}
// IntSet is a set of integers.
type IntSet map[int]struct{}
// NewIntSet creates and initializes a new IntSet.
func NewIntSet(s ...int) IntSet {
m := make(IntSet, len(s))
for _, v := range s {
m[v] = struct{}{}
}
return m
}
// NewIntSetFromString creates and initializes a new IntSet from
// indices and ranges in s. Example: 1-3,7,9 expands to 1,2,3,7,9.
func NewIntSetFromString(s ...string) (IntSet, error) {
var err error
islice := make([]int, 0, len(s))
for _, str := range s {
var start, end int
p := strings.SplitN(str, "-", 2)
if len(p) == 2 && p[1] != "" {
end, err = strconv.Atoi(p[1])
if err != nil {
return nil, fmt.Errorf("failed to parse range %q: %v", str, err)
}
}
start, err = strconv.Atoi(p[0])
if err != nil {
return nil, fmt.Errorf("failed to parse int: %q: %v", str, err)
}
if end == 0 {
islice = append(islice, start)
continue
}
if end <= start {
return nil, fmt.Errorf("range end <= start: %q (%d <= %d)", str, end, start)
}
for i := start; i <= end; i++ {
islice = append(islice, i)
}
}
return NewIntSet(islice...), nil
}
// Merge merges ms into is.
func (is IntSet) Merge(ms IntSet) {
for k, v := range ms {
is[k] = v
}
}
// ReverseSortedSet returns a reverse sorted set.
func (is IntSet) ReverseSortedSet() []int {
s := make([]int, 0, len(is))
for v := range is {
s = append(s, v)
}
sort.Sort(sort.Reverse(sort.IntSlice(s)))
return s
}
// InsertColumn inserts column c to s at position idx.
// Invalid idx causes c to be appended to s.
func InsertColumn(s []string, c string, idx int) []string {
if idx <= 0 || idx-1 >= len(s) {
return append(s, c)
}
i := idx - 1
return append(s[:i], append([]string{c}, s[i:]...)...)
}
// RemoveColumns returns a copy of cols with columns idx removed.
func RemoveColumns(cols []string, idx IntSet) []string {
if len(cols) == 0 || len(idx) == 0 {
return cols
}
nc := make([]string, 0, len(cols))
for i, col := range cols {
if _, exists := idx[i+1]; exists {
continue
}
nc = append(nc, col)
}
return nc
}