elm_history_tools 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -3
- data/README.md +36 -7
- data/lib/elm_history_tools.rb +2 -0
- data/lib/elm_history_tools/history_formatter.rb +16 -16
- data/lib/elm_history_tools/history_sanitizer.rb +68 -0
- data/lib/elm_history_tools/utils.rb +22 -0
- data/lib/elm_history_tools/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 280b6456a5473cc4af85ebe96dd303466bf1341d
|
4
|
+
data.tar.gz: d18f4ff0a50be7ddd539b0198b51d05415812151
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b9e621128a84df9e1ee1c84369cff2bb46c6fe7800a664b40ac3f64dc58efa028b140229f2b20693ab7390e3e3e02c319d980416316c0acc0f832c52283a460e
|
7
|
+
data.tar.gz: 9e7c57afa830d958f2fcce36d9894227605d4afb810a32f6ff679211068327e4c29cb37dc99e60651e8139bacaeadd8912bf1655f316fc7e5c37bc5a8fc3bf75
|
data/Gemfile.lock
CHANGED
@@ -1,12 +1,11 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
elm_history_tools (0.
|
4
|
+
elm_history_tools (0.2.0)
|
5
5
|
|
6
6
|
GEM
|
7
7
|
remote: https://rubygems.org/
|
8
8
|
specs:
|
9
|
-
awesome_print (1.8.0)
|
10
9
|
byebug (10.0.2)
|
11
10
|
diff-lcs (1.3)
|
12
11
|
rake (10.5.0)
|
@@ -28,7 +27,6 @@ PLATFORMS
|
|
28
27
|
ruby
|
29
28
|
|
30
29
|
DEPENDENCIES
|
31
|
-
awesome_print
|
32
30
|
bundler (~> 1.16)
|
33
31
|
byebug
|
34
32
|
elm_history_tools!
|
data/README.md
CHANGED
@@ -38,17 +38,46 @@ _Sanitizing History_
|
|
38
38
|
|
39
39
|
How do you actually do this?
|
40
40
|
|
41
|
-
|
41
|
+
`ElmHistoryTools::HistorySanitizer` allows you to sanitize Elm records and objects. Because these
|
42
|
+
objects are so individual to your application, there's no universal code that can sanitize the data
|
43
|
+
without breaking the structure. (This is why we can't, for instance, just use something like Rails'
|
44
|
+
ParameterFilters.)
|
45
|
+
|
46
|
+
Instead, you tell the HistorySanitizer which terms to look for and provide a block that can do the
|
47
|
+
sanitizing:
|
48
|
+
|
49
|
+
```ruby
|
50
|
+
# let's say you have an Elm object like
|
51
|
+
# MySecretData "access_token" {password = "myP@ssw0rd", expiration = "tomorrow"}
|
52
|
+
ElmHistoryTools::HistorySanitizer.sanitize_history(history_data, ["MySecretData", "password"]) do |elm_object_or_record|
|
53
|
+
if elm_object_or_record["ctor"] == "MySecretData"
|
54
|
+
# replace the access token
|
55
|
+
# make sure to return the updated object!
|
56
|
+
elm_object_or_record.merge("_0" =>"[FILTERED]")
|
57
|
+
else
|
58
|
+
# we have the record
|
59
|
+
elm_object_or_record.merge("password" => "[FILTERED]")
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# before: {"ctor" => "MySecretData", "_0" => "access_token", "_1" => {"password" => "myP@ssw0rd", "expiration" => "tomorrow"}}
|
64
|
+
# after: {"ctor" => "MySecretData", "_0" => "[FILTERED]", "_1" => {"password" => "[FILTERED]", "expiration" => "tomorrow"}}
|
65
|
+
```
|
66
|
+
|
67
|
+
As you can see, this is neither flexible nor elegant -- unfortunately, that's the nature of the
|
68
|
+
game when you're manipulating the state generated by an Elm program. Elm types may store sensitive data
|
69
|
+
as basic data in the object, which leaves no automated way to detect and replace that information.
|
42
70
|
|
43
71
|
_Format a History into a Hash_
|
44
72
|
|
45
|
-
One approach we've found useful for user issues at eSpark Learning has been to skim the
|
46
|
-
|
47
|
-
server, if they were able to they answer any quiz questions, etc., providing a
|
48
|
-
further debugging.
|
73
|
+
One approach we've found useful for user issues at eSpark Learning has been to skim the history and
|
74
|
+
see if anything looks amiss. Eyeballing the user's actions shows if a student get an unhappy
|
75
|
+
response back from a server, if they were able to they answer any quiz questions, etc., providing a
|
76
|
+
useful context for further debugging.
|
49
77
|
|
50
|
-
In order to make sense of data, it has to be presented in a
|
51
|
-
|
78
|
+
In order to make sense of data, it has to be presented in a compact and (relatively) human-readable
|
79
|
+
format, which the raw history export is not. (See below for more information on that.) Such cleanup
|
80
|
+
is what the `ElmHistoryTools::HistoryFormatter` object can do:
|
52
81
|
|
53
82
|
```ruby
|
54
83
|
# given a JSON history file like
|
data/lib/elm_history_tools.rb
CHANGED
@@ -20,23 +20,23 @@ module ElmHistoryTools::HistoryFormatter
|
|
20
20
|
# alternative approach would be to use nil. While that would clearly distinguish between those
|
21
21
|
# cases, it would make working with the results more complicated.
|
22
22
|
def self.simplify_history_entry(entry)
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
}
|
36
|
-
else
|
37
|
-
entry.each_with_object({}) do |(key, value), hash|
|
38
|
-
hash[key] = simplify_history_entry(value)
|
23
|
+
ElmHistoryTools::Utils.transform_object(entry) do |object_hash|
|
24
|
+
if object_hash["ctor"] == "::"
|
25
|
+
# Elm lists are represented as nested entries with the contructor ::. (See the readme for
|
26
|
+
# more detail.)
|
27
|
+
# We collapse those into a proper Ruby array via flatten.
|
28
|
+
# The last entry of the list will have no nested entry, so we use compact to remove the nil.
|
29
|
+
[simplify_history_entry(object_hash["_0"]), simplify_history_entry(object_hash["_1"])].compact.flatten
|
30
|
+
elsif object_hash["ctor"]
|
31
|
+
# we have an Elm object type (we know this because non-objects aren't passed to the block)
|
32
|
+
{
|
33
|
+
object_hash["ctor"] => object_hash.reject {|k, _v| k == "ctor"}.values.map {|val| simplify_history_entry(val) }
|
34
|
+
}
|
39
35
|
end
|
40
36
|
end
|
41
37
|
end
|
38
|
+
|
39
|
+
class << self
|
40
|
+
private :simplify_history_entry
|
41
|
+
end
|
42
42
|
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module ElmHistoryTools::HistorySanitizer
|
2
|
+
# We don't include the object received because it might have sensitive information
|
3
|
+
# This might be reconsidered in the future.
|
4
|
+
class ElmObjectExpected < StandardError; end
|
5
|
+
|
6
|
+
# Given a raw Elm history file parsed to JSON, a set of key terms to watch for (password, token,
|
7
|
+
# etc.) , sanitize a
|
8
|
+
# history so it doesn't contain sensitive information.
|
9
|
+
#
|
10
|
+
# The message sanitizers should be in the format of
|
11
|
+
# "HistoryConstructor" => block_taking_history_entry_and_returning_sanitized_version
|
12
|
+
#
|
13
|
+
# You can optionally specify a set of watch words to ensure you don't accidentally
|
14
|
+
# introduce unsafe data. If specified and any unsanitized entry contains a constructor or record
|
15
|
+
# property matching one of those words, an error will be raised. (So for instance, if you add a
|
16
|
+
# User object with an Password property to a message, a watch word for "password" will catch
|
17
|
+
# that -- though it would also catch password_reset_requested.)
|
18
|
+
#
|
19
|
+
# Each app will have such different entry structures that there's no easy to generalize this
|
20
|
+
# further.
|
21
|
+
def self.sanitize_history(history_data, watch_words, &sanitizing_block)
|
22
|
+
matchers = watch_words.map {|word| word.is_a?(Regexp) ? word : Regexp.new(word, true)}
|
23
|
+
history_data.dup.merge(
|
24
|
+
"history" => history_data["history"].map {|entry| sanitize(entry, matchers, &sanitizing_block)}
|
25
|
+
)
|
26
|
+
end
|
27
|
+
|
28
|
+
# For each entry, we sanitize it if there's a matching handler. If there is no sanitizer, see if
|
29
|
+
# any data in the entry merit flagging against a watch word.
|
30
|
+
def self.sanitize(entry, matchers, &sanitizing_block)
|
31
|
+
ElmHistoryTools::Utils.transform_object(entry) do |elm_object|
|
32
|
+
constructor = elm_object["ctor"]
|
33
|
+
raise ElmObjectExpected unless constructor
|
34
|
+
|
35
|
+
# replace any records that need it
|
36
|
+
cleaned_object = elm_object.each_with_object({}) do |(key, value), hash|
|
37
|
+
if value.is_a?(Hash) && value["ctor"]
|
38
|
+
# we have an elm object -- sanitize that
|
39
|
+
hash[key] = sanitize(value, matchers, &sanitizing_block)
|
40
|
+
elsif value.is_a?(Hash)
|
41
|
+
# We have an Elm record, represented as a raw hash
|
42
|
+
# This is inefficient, but should hopefully be acceptable for the relatively low number of records
|
43
|
+
# being processed. If not, we can rewrite it.
|
44
|
+
if value.keys.any? {|field| matchers.any? {|matcher| field.match(matcher)}}
|
45
|
+
hash[key] = yield value
|
46
|
+
else
|
47
|
+
hash[key] = value
|
48
|
+
end
|
49
|
+
else
|
50
|
+
# raw values
|
51
|
+
hash[key] = value
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# now that we've sanitized any stored records or objects, sanitize the overall record
|
56
|
+
if matchers.any? {|matcher| constructor.match(matcher)}
|
57
|
+
yield cleaned_object
|
58
|
+
else
|
59
|
+
cleaned_object
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
class << self
|
65
|
+
private :sanitize
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module ElmHistoryTools::Utils
|
2
|
+
# This method gives us a tool to walk down an Elm history entry, processing all the Elm objects contained within per the provided block. Whatever the block returns will become the value at that point.
|
3
|
+
#
|
4
|
+
# To simplify matters, this doesn't process primitive values. Additionally, while each value an
|
5
|
+
# Elm record (which are stored as plain JSON objects) is examined, the overall record itself is
|
6
|
+
# never passed to the block -- to examine/alter an Elm record, do so in the callback for
|
7
|
+
# whichever Elm object contains it. (All history entries are Message objects, so there's always
|
8
|
+
# an entry point.)
|
9
|
+
def self.transform_object(entry, &block)
|
10
|
+
if !entry.is_a?(Hash)
|
11
|
+
entry
|
12
|
+
elsif entry["ctor"]
|
13
|
+
# If you want to walk down sub-entries, call transform_object within your block.
|
14
|
+
yield entry
|
15
|
+
else
|
16
|
+
# Each Elm record should be
|
17
|
+
entry.each_with_object({}) do |(key, value), hash|
|
18
|
+
hash[key] = transform_object(value, &block)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: elm_history_tools
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Alex Koppel
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-
|
11
|
+
date: 2018-06-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -57,6 +57,8 @@ files:
|
|
57
57
|
- elm_history_tools.gemspec
|
58
58
|
- lib/elm_history_tools.rb
|
59
59
|
- lib/elm_history_tools/history_formatter.rb
|
60
|
+
- lib/elm_history_tools/history_sanitizer.rb
|
61
|
+
- lib/elm_history_tools/utils.rb
|
60
62
|
- lib/elm_history_tools/version.rb
|
61
63
|
- readme-images/elm-history-dashboard.png
|
62
64
|
homepage: http://github.com/arsduo/elm-history-tools
|