Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,16 +105,23 @@ Retrieves the recorded history of installed updates.

### Hide

Hides or unhides an update from installation.
Hides or unhides an update from installation. Updates can be selected by KB
article ID (`--kbs`), by Update ID (`--update-ids`), or both.

Hide a KB:

`cabbie hide --kbs="1234513"`

Hide an update by its Update ID:

`cabbie hide --update-ids="1234ccd5-1234-456f-78gh-ij2911553881"`

Make an update available for install:

`cabbie hide --unhide --kbs="1234513"`

`cabbie hide --unhide --update-ids="1234ccd5-1234-456f-78gh-ij2911553881"`

### Reboot

Manage the pending reboot state of the machine.
Expand Down Expand Up @@ -199,6 +206,18 @@ string under the `hidden` key or use the client-side Windows Update Agent (WUA)
> represents a server-side package container GUID that does not match the
> client-side `UpdateID`.

Removing an update from the `hidden` or `hidden-UpdateID` keys does not make it
visible again, because Cabbie has no record of why an update was hidden and will
not undo hides performed by an administrator or another tool. To make a
previously hidden update visible again, place the KB article string under the
`unhide` key or the Update ID under the `unhide-UpdateID` key. Unhidden updates
become eligible for installation during the next update cycle.

Enforcements are aggregated across every json file in the enforcement directory.
If the same update is listed as both hidden and unhidden, hiding wins and the
conflict is logged. This holds even when the two entries use different
identifiers, such as hiding an update by KB ID and unhiding it by Update ID.

Example:

```json
Expand All @@ -211,6 +230,12 @@ Example:
],
"hidden-UpdateID": [
"1234ccd5-1234-456f-78gh-ij2911553881"
],
"unhide": [
"789012"
],
"unhide-UpdateID": [
"5678ccd5-1234-456f-78gh-ij2911553882"
]
}
```
Expand Down
11 changes: 11 additions & 0 deletions cabbie.go
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,17 @@ func enforce() error {
deck.ErrorA(failures).With(eventID(cablib.EvtErrInstallFailure)).Go()
}
}
if len(updates.Conflicts) > 0 {
deck.ErrorfA("Ignoring unhide enforcement for updates that are also explicitly hidden: %v", updates.Conflicts).With(eventID(cablib.EvtErrEnforcement)).Go()
}
if len(updates.Unhide) > 0 || len(updates.UnhideUpdateID) > 0 {
want := updateSet{kbs: NewKBSetFromSlice(updates.Unhide), uuids: updates.UnhideUpdateID}
hidden := updateSet{kbs: NewKBSetFromSlice(updates.Hidden), uuids: updates.HiddenUpdateID}
if err := unhide(want, hidden); err != nil {
failures = fmt.Errorf("error unhiding updates: %v", err)
deck.ErrorA(failures).With(eventID(cablib.EvtErrUnhide)).Go()
}
}
if len(updates.Hidden) > 0 {
if err := hide(NewKBSetFromSlice(updates.Hidden)); err != nil {
failures = fmt.Errorf("error hiding updates: %v", err)
Expand Down
46 changes: 46 additions & 0 deletions enforcement/enforcement.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"fmt"
"os"
"path/filepath"
"strings"

"github.com/google/cabbie/cablib"

Expand All @@ -45,6 +46,9 @@ type Enforcements struct {
ExcludedDrivers []DriverExclude `json:"excluded-drivers"`
Hidden []string `json:"hidden"`
HiddenUpdateID []string `json:"hidden-UpdateID"`
Unhide []string `json:"unhide"`
UnhideUpdateID []string `json:"unhide-UpdateID"`
Conflicts []string `json:"-"`
}

// DriverExclude specifies criteria to exclude certain driver updates.
Expand Down Expand Up @@ -95,8 +99,11 @@ func Get() (Enforcements, error) {
ret.Hidden = append(ret.Hidden, e.Hidden...)
ret.ExcludedDrivers = append(ret.ExcludedDrivers, e.ExcludedDrivers...)
ret.HiddenUpdateID = append(ret.HiddenUpdateID, e.HiddenUpdateID...)
ret.Unhide = append(ret.Unhide, e.Unhide...)
ret.UnhideUpdateID = append(ret.UnhideUpdateID, e.UnhideUpdateID...)
}
ret.dedupe()
ret.reconcile()
return ret, nil
}

Expand Down Expand Up @@ -131,9 +138,48 @@ func (e *Enforcements) dedupe() {
e.Required = uniqueStrings(e.Required)
e.Hidden = uniqueStrings(e.Hidden)
e.HiddenUpdateID = uniqueStrings(e.HiddenUpdateID)
e.Unhide = uniqueStrings(e.Unhide)
e.UnhideUpdateID = uniqueStrings(e.UnhideUpdateID)
e.ExcludedDrivers = uniqueDriverExclude(e.ExcludedDrivers)
}

func normalizeKB(kb string) string {
return strings.ReplaceAll(strings.ToLower(kb), "kb", "")
}

// reconcile resolves entries that are requested to be both hidden and
// unhidden. If in both, hiding wins and we log it.
func (e *Enforcements) reconcile() {
hidden := make(map[string]bool, len(e.Hidden))
for _, kb := range e.Hidden {
hidden[normalizeKB(kb)] = true
}
hiddenID := make(map[string]bool, len(e.HiddenUpdateID))
for _, id := range e.HiddenUpdateID {
hiddenID[strings.ToLower(id)] = true
}

unhide := make([]string, 0, len(e.Unhide))
for _, kb := range e.Unhide {
if hidden[normalizeKB(kb)] {
e.Conflicts = append(e.Conflicts, kb)
continue
}
unhide = append(unhide, kb)
}
e.Unhide = unhide

unhideID := make([]string, 0, len(e.UnhideUpdateID))
for _, id := range e.UnhideUpdateID {
if hiddenID[strings.ToLower(id)] {
e.Conflicts = append(e.Conflicts, id)
continue
}
unhideID = append(unhideID, id)
}
e.UnhideUpdateID = unhideID
}

// Watcher runs a filesystem watcher for required updates. This is meant to install required updates as soon as they are configured.
// All configured required updates are read on a schedule (see cabbie.go t.Enforcement ticker usage) to ensure required
// updates are installed even if a filesystem event is missed.
Expand Down
94 changes: 94 additions & 0 deletions enforcement/enforcement_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,16 @@ func TestDedupe(t *testing.T) {
Enforcements{Hidden: []string{"4018073", "67891011", "4018073", "4018073"}},
Enforcements{Hidden: []string{"4018073", "67891011"}},
},
{
"with dup unhide",
Enforcements{Unhide: []string{"4018073", "67891011", "4018073", "4018073"}},
Enforcements{Unhide: []string{"4018073", "67891011"}},
},
{
"with dup unhide update ids",
Enforcements{UnhideUpdateID: []string{"8870bdb3", "245bd515", "8870bdb3"}},
Enforcements{UnhideUpdateID: []string{"8870bdb3", "245bd515"}},
},
{
"with dup excluded drivers",
Enforcements{ExcludedDrivers: []DriverExclude{
Expand Down Expand Up @@ -82,6 +92,83 @@ func TestDedupe(t *testing.T) {
}
}

func TestReconcile(t *testing.T) {
tests := []struct {
desc string
in Enforcements
want Enforcements
}{
{
"no conflicts",
Enforcements{
Hidden: []string{"4018073"},
HiddenUpdateID: []string{"8870bdb3-95f9-43d9-9ba4-1f0e7cf13db2"},
Unhide: []string{"67891011"},
UnhideUpdateID: []string{"245bd515-7b95-496e-acac-344881833263"},
},
Enforcements{
Hidden: []string{"4018073"},
HiddenUpdateID: []string{"8870bdb3-95f9-43d9-9ba4-1f0e7cf13db2"},
Unhide: []string{"67891011"},
UnhideUpdateID: []string{"245bd515-7b95-496e-acac-344881833263"},
},
},
{
"hidden wins over unhide",
Enforcements{
Hidden: []string{"4018073"},
Unhide: []string{"4018073", "67891011"},
},
Enforcements{
Hidden: []string{"4018073"},
Unhide: []string{"67891011"},
Conflicts: []string{"4018073"},
},
},
{
"kb prefix is normalized",
Enforcements{
Hidden: []string{"KB4018073"},
Unhide: []string{"4018073"},
},
Enforcements{
Hidden: []string{"KB4018073"},
Conflicts: []string{"4018073"},
},
},
{
"hidden wins over unhide update id",
Enforcements{
HiddenUpdateID: []string{"8870BDB3-95F9-43D9-9BA4-1F0E7CF13DB2"},
UnhideUpdateID: []string{"8870bdb3-95f9-43d9-9ba4-1f0e7cf13db2"},
},
Enforcements{
HiddenUpdateID: []string{"8870BDB3-95F9-43D9-9BA4-1F0E7CF13DB2"},
Conflicts: []string{"8870bdb3-95f9-43d9-9ba4-1f0e7cf13db2"},
},
},
{
"unhide by update id is unaffected by a hidden kb",
Enforcements{
Hidden: []string{"4018073"},
UnhideUpdateID: []string{"8870bdb3-95f9-43d9-9ba4-1f0e7cf13db2"},
},
Enforcements{
Hidden: []string{"4018073"},
UnhideUpdateID: []string{"8870bdb3-95f9-43d9-9ba4-1f0e7cf13db2"},
},
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
tt.in.reconcile()
if diff := cmp.Diff(tt.want, tt.in, cmpopts.EquateEmpty()); diff != "" {
t.Errorf("reconcile(%s) returned unexpected diff (-want +got):\n%s", tt.desc, diff)
}
})
}
}

func TestEnforcements(t *testing.T) {
tests := []struct {
in string
Expand All @@ -96,6 +183,13 @@ func TestEnforcements(t *testing.T) {
Enforcements{Hidden: []string{"4018073", "67891011"}},
nil,
},
{"unhide.json",
Enforcements{
Unhide: []string{"4018073", "67891011"},
UnhideUpdateID: []string{"8870bdb3-95f9-43d9-9ba4-1f0e7cf13db2", "245bd515-7b95-496e-acac-344881833263"},
},
nil,
},
{"excluded-drivers.json",
Enforcements{ExcludedDrivers: []DriverExclude{
{DriverClass: "UnitTest"},
Expand Down
10 changes: 10 additions & 0 deletions enforcement/testdata/unhide.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"unhide": [
"4018073",
"67891011"
],
"unhide-UpdateID": [
"8870bdb3-95f9-43d9-9ba4-1f0e7cf13db2",
"245bd515-7b95-496e-acac-344881833263"
]
}
Loading
Loading