json-schema-diff 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,108 @@
1
+ [
2
+ {
3
+ "ident": "template-injection",
4
+ "desc": "Template injection in workflow",
5
+ "url": "https://docs.zizmor.sh/audits/template-injection/",
6
+ "determinations": {
7
+ "confidence": "High",
8
+ "severity": "High",
9
+ "persona": "Regular"
10
+ },
11
+ "locations": [
12
+ {
13
+ "symbolic": {
14
+ "key": {
15
+ "Local": {
16
+ "prefix": null,
17
+ "given_path": ".github/workflows/ci.yml"
18
+ }
19
+ },
20
+ "annotation": "workflow step uses uncontrolled template expression",
21
+ "route": {
22
+ "route": [
23
+ {"Key": "jobs"},
24
+ {"Key": "test"},
25
+ {"Key": "steps"},
26
+ {"Index": 2},
27
+ {"Key": "run"}
28
+ ]
29
+ },
30
+ "feature_kind": "Normal",
31
+ "kind": "Primary"
32
+ },
33
+ "concrete": {
34
+ "location": {
35
+ "start_point": {
36
+ "row": 42,
37
+ "column": 12
38
+ },
39
+ "end_point": {
40
+ "row": 42,
41
+ "column": 45
42
+ },
43
+ "offset_span": {
44
+ "start": 1250,
45
+ "end": 1283
46
+ }
47
+ },
48
+ "feature": "echo \"Hello ${{ github.event.issue.title }}\"",
49
+ "comments": []
50
+ }
51
+ }
52
+ ],
53
+ "ignored": false
54
+ },
55
+ {
56
+ "ident": "dangerous-actions",
57
+ "desc": "Use of dangerous or deprecated action",
58
+ "url": "https://docs.zizmor.sh/audits/dangerous-actions/",
59
+ "determinations": {
60
+ "confidence": "Medium",
61
+ "severity": "Medium",
62
+ "persona": "Regular"
63
+ },
64
+ "locations": [
65
+ {
66
+ "symbolic": {
67
+ "key": {
68
+ "Local": {
69
+ "prefix": null,
70
+ "given_path": ".github/workflows/deploy.yml"
71
+ }
72
+ },
73
+ "annotation": "potentially dangerous action with write permissions",
74
+ "route": {
75
+ "route": [
76
+ {"Key": "jobs"},
77
+ {"Key": "deploy"},
78
+ {"Key": "steps"},
79
+ {"Index": 0},
80
+ {"Key": "uses"}
81
+ ]
82
+ },
83
+ "feature_kind": "Normal",
84
+ "kind": "Primary"
85
+ },
86
+ "concrete": {
87
+ "location": {
88
+ "start_point": {
89
+ "row": 25,
90
+ "column": 8
91
+ },
92
+ "end_point": {
93
+ "row": 25,
94
+ "column": 35
95
+ },
96
+ "offset_span": {
97
+ "start": 780,
98
+ "end": 807
99
+ }
100
+ },
101
+ "feature": "uses: suspicious/action@v1",
102
+ "comments": []
103
+ }
104
+ }
105
+ ],
106
+ "ignored": false
107
+ }
108
+ ]
@@ -0,0 +1,160 @@
1
+ [
2
+ {
3
+ "ident": "template-injection",
4
+ "desc": "Template injection in workflow",
5
+ "url": "https://docs.zizmor.sh/audits/template-injection/",
6
+ "determinations": {
7
+ "confidence": "High",
8
+ "severity": "High",
9
+ "persona": "Regular"
10
+ },
11
+ "locations": [
12
+ {
13
+ "symbolic": {
14
+ "key": {
15
+ "Local": {
16
+ "prefix": null,
17
+ "given_path": ".github/workflows/ci.yml"
18
+ }
19
+ },
20
+ "annotation": "workflow step uses uncontrolled template expression",
21
+ "route": {
22
+ "route": [
23
+ {"Key": "jobs"},
24
+ {"Key": "test"},
25
+ {"Key": "steps"},
26
+ {"Index": 2},
27
+ {"Key": "run"}
28
+ ]
29
+ },
30
+ "feature_kind": "Normal",
31
+ "kind": "Primary"
32
+ },
33
+ "concrete": {
34
+ "location": {
35
+ "start_point": {
36
+ "row": 42,
37
+ "column": 12
38
+ },
39
+ "end_point": {
40
+ "row": 42,
41
+ "column": 45
42
+ },
43
+ "offset_span": {
44
+ "start": 1250,
45
+ "end": 1283
46
+ }
47
+ },
48
+ "feature": "echo \"Hello ${{ github.event.issue.title }}\"",
49
+ "comments": []
50
+ }
51
+ }
52
+ ],
53
+ "ignored": false
54
+ },
55
+ {
56
+ "ident": "dangerous-actions",
57
+ "desc": "Use of dangerous or deprecated action",
58
+ "url": "https://docs.zizmor.sh/audits/dangerous-actions/",
59
+ "determinations": {
60
+ "confidence": "High",
61
+ "severity": "High",
62
+ "persona": "Regular"
63
+ },
64
+ "locations": [
65
+ {
66
+ "symbolic": {
67
+ "key": {
68
+ "Local": {
69
+ "prefix": null,
70
+ "given_path": ".github/workflows/deploy.yml"
71
+ }
72
+ },
73
+ "annotation": "potentially dangerous action with write permissions",
74
+ "route": {
75
+ "route": [
76
+ {"Key": "jobs"},
77
+ {"Key": "deploy"},
78
+ {"Key": "steps"},
79
+ {"Index": 0},
80
+ {"Key": "uses"}
81
+ ]
82
+ },
83
+ "feature_kind": "Normal",
84
+ "kind": "Primary"
85
+ },
86
+ "concrete": {
87
+ "location": {
88
+ "start_point": {
89
+ "row": 25,
90
+ "column": 8
91
+ },
92
+ "end_point": {
93
+ "row": 25,
94
+ "column": 35
95
+ },
96
+ "offset_span": {
97
+ "start": 780,
98
+ "end": 807
99
+ }
100
+ },
101
+ "feature": "uses: suspicious/action@v1",
102
+ "comments": []
103
+ }
104
+ }
105
+ ],
106
+ "ignored": false
107
+ },
108
+ {
109
+ "ident": "hardcoded-credentials",
110
+ "desc": "Hardcoded credentials detected",
111
+ "url": "https://docs.zizmor.sh/audits/hardcoded-credentials/",
112
+ "determinations": {
113
+ "confidence": "High",
114
+ "severity": "High",
115
+ "persona": "Regular"
116
+ },
117
+ "locations": [
118
+ {
119
+ "symbolic": {
120
+ "key": {
121
+ "Local": {
122
+ "prefix": null,
123
+ "given_path": ".github/workflows/release.yml"
124
+ }
125
+ },
126
+ "annotation": "hardcoded API key in environment variable",
127
+ "route": {
128
+ "route": [
129
+ {"Key": "jobs"},
130
+ {"Key": "publish"},
131
+ {"Key": "env"},
132
+ {"Key": "API_KEY"}
133
+ ]
134
+ },
135
+ "feature_kind": "Normal",
136
+ "kind": "Primary"
137
+ },
138
+ "concrete": {
139
+ "location": {
140
+ "start_point": {
141
+ "row": 18,
142
+ "column": 4
143
+ },
144
+ "end_point": {
145
+ "row": 18,
146
+ "column": 35
147
+ },
148
+ "offset_span": {
149
+ "start": 560,
150
+ "end": 591
151
+ }
152
+ },
153
+ "feature": "API_KEY: sk-1234567890abcdef",
154
+ "comments": []
155
+ }
156
+ }
157
+ ],
158
+ "ignored": false
159
+ }
160
+ ]
@@ -0,0 +1,300 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://docs.zizmor.sh/schemas/output-v1.json",
4
+ "title": "Zizmor JSON Output Schema (v1)",
5
+ "description": "Schema for zizmor's v1 JSON output format - an array of security findings",
6
+ "type": "array",
7
+ "items": {
8
+ "$ref": "#/$defs/Finding"
9
+ },
10
+ "$defs": {
11
+ "Finding": {
12
+ "type": "object",
13
+ "description": "A security finding identified by zizmor",
14
+ "required": ["ident", "desc", "url", "determinations", "locations", "ignored"],
15
+ "properties": {
16
+ "ident": {
17
+ "type": "string",
18
+ "description": "The audit ID for this finding (e.g., 'template-injection')"
19
+ },
20
+ "desc": {
21
+ "type": "string",
22
+ "description": "A short description of the finding"
23
+ },
24
+ "url": {
25
+ "type": "string",
26
+ "format": "uri",
27
+ "description": "A URL linking to the documentation for this finding's audit"
28
+ },
29
+ "determinations": {
30
+ "$ref": "#/$defs/Determinations"
31
+ },
32
+ "locations": {
33
+ "type": "array",
34
+ "description": "Array of locations where this finding occurs",
35
+ "items": {
36
+ "$ref": "#/$defs/Location"
37
+ },
38
+ "minItems": 1
39
+ },
40
+ "ignored": {
41
+ "type": "boolean",
42
+ "description": "Whether this finding is ignored via inline comments or configuration"
43
+ }
44
+ },
45
+ "additionalProperties": false
46
+ },
47
+ "Determinations": {
48
+ "type": "object",
49
+ "description": "Classifications for the finding's confidence, severity, and target persona",
50
+ "required": ["confidence", "severity", "persona"],
51
+ "properties": {
52
+ "confidence": {
53
+ "$ref": "#/$defs/Confidence"
54
+ },
55
+ "severity": {
56
+ "$ref": "#/$defs/Severity"
57
+ },
58
+ "persona": {
59
+ "$ref": "#/$defs/Persona"
60
+ }
61
+ },
62
+ "additionalProperties": false
63
+ },
64
+ "Confidence": {
65
+ "type": "string",
66
+ "enum": ["Unknown", "Low", "Medium", "High"],
67
+ "description": "Confidence level of the finding"
68
+ },
69
+ "Severity": {
70
+ "type": "string",
71
+ "enum": ["Unknown", "Informational", "Low", "Medium", "High"],
72
+ "description": "Severity level of the finding"
73
+ },
74
+ "Persona": {
75
+ "type": "string",
76
+ "enum": ["Auditor", "Pedantic", "Regular"],
77
+ "description": "Target persona for the finding (affects false positive tolerance)"
78
+ },
79
+ "Location": {
80
+ "type": "object",
81
+ "description": "A location within a GitHub Actions workflow with symbolic and concrete components",
82
+ "required": ["symbolic", "concrete"],
83
+ "properties": {
84
+ "symbolic": {
85
+ "$ref": "#/$defs/SymbolicLocation"
86
+ },
87
+ "concrete": {
88
+ "$ref": "#/$defs/Feature"
89
+ }
90
+ },
91
+ "additionalProperties": false
92
+ },
93
+ "SymbolicLocation": {
94
+ "type": "object",
95
+ "description": "Symbolic representation of a location in the workflow",
96
+ "required": ["key", "annotation", "route", "feature_kind", "kind"],
97
+ "properties": {
98
+ "key": {
99
+ "$ref": "#/$defs/InputKey"
100
+ },
101
+ "annotation": {
102
+ "type": "string",
103
+ "description": "Human-readable annotation for this location"
104
+ },
105
+ "route": {
106
+ "$ref": "#/$defs/Route"
107
+ },
108
+ "feature_kind": {
109
+ "$ref": "#/$defs/SymbolicFeature"
110
+ },
111
+ "kind": {
112
+ "$ref": "#/$defs/LocationKind"
113
+ }
114
+ },
115
+ "additionalProperties": false
116
+ },
117
+ "InputKey": {
118
+ "type": "object",
119
+ "description": "Identifier for the input file",
120
+ "oneOf": [
121
+ {
122
+ "type": "object",
123
+ "required": ["Local"],
124
+ "properties": {
125
+ "Local": {
126
+ "type": "object",
127
+ "required": ["prefix", "given_path"],
128
+ "properties": {
129
+ "prefix": {
130
+ "type": ["string", "null"],
131
+ "description": "Optional prefix for the path"
132
+ },
133
+ "given_path": {
134
+ "type": "string",
135
+ "description": "The file path"
136
+ }
137
+ },
138
+ "additionalProperties": false
139
+ }
140
+ },
141
+ "additionalProperties": false
142
+ }
143
+ ]
144
+ },
145
+ "Route": {
146
+ "type": "object",
147
+ "description": "Path through the YAML structure to reach the location",
148
+ "required": ["route"],
149
+ "properties": {
150
+ "route": {
151
+ "type": "array",
152
+ "items": {
153
+ "$ref": "#/$defs/RouteComponent"
154
+ }
155
+ }
156
+ },
157
+ "additionalProperties": false
158
+ },
159
+ "RouteComponent": {
160
+ "oneOf": [
161
+ {
162
+ "type": "object",
163
+ "required": ["Key"],
164
+ "properties": {
165
+ "Key": {
166
+ "type": "string"
167
+ }
168
+ },
169
+ "additionalProperties": false
170
+ },
171
+ {
172
+ "type": "object",
173
+ "required": ["Index"],
174
+ "properties": {
175
+ "Index": {
176
+ "type": "integer",
177
+ "minimum": 0
178
+ }
179
+ },
180
+ "additionalProperties": false
181
+ }
182
+ ]
183
+ },
184
+ "SymbolicFeature": {
185
+ "oneOf": [
186
+ {
187
+ "type": "string",
188
+ "enum": ["Normal", "KeyOnly"],
189
+ "description": "Normal feature (whole YAML feature) or KeyOnly (key without value)"
190
+ },
191
+ {
192
+ "type": "object",
193
+ "required": ["Subfeature"],
194
+ "properties": {
195
+ "Subfeature": {
196
+ "type": "object",
197
+ "required": ["after", "fragment"],
198
+ "properties": {
199
+ "after": {
200
+ "type": "integer",
201
+ "minimum": 0,
202
+ "description": "Byte offset within the parent feature"
203
+ },
204
+ "fragment": {
205
+ "type": "object",
206
+ "required": ["Raw"],
207
+ "properties": {
208
+ "Raw": {
209
+ "type": "string",
210
+ "description": "The raw text fragment"
211
+ }
212
+ },
213
+ "additionalProperties": false
214
+ }
215
+ },
216
+ "additionalProperties": false
217
+ }
218
+ },
219
+ "additionalProperties": false
220
+ }
221
+ ]
222
+ },
223
+ "LocationKind": {
224
+ "type": "string",
225
+ "enum": ["Primary", "Related", "Hidden"],
226
+ "description": "Type of location: Primary (main issue), Related (supporting), or Hidden (not displayed)"
227
+ },
228
+ "Feature": {
229
+ "type": "object",
230
+ "description": "Concrete extracted feature with location and content",
231
+ "required": ["location", "feature", "comments"],
232
+ "properties": {
233
+ "location": {
234
+ "$ref": "#/$defs/ConcreteLocation"
235
+ },
236
+ "feature": {
237
+ "type": "string",
238
+ "description": "The extracted textual content of the feature"
239
+ },
240
+ "comments": {
241
+ "type": "array",
242
+ "items": {
243
+ "type": "string"
244
+ },
245
+ "description": "Any comments within the feature's line span"
246
+ }
247
+ },
248
+ "additionalProperties": false
249
+ },
250
+ "ConcreteLocation": {
251
+ "type": "object",
252
+ "description": "Concrete location with line/column and byte offset information",
253
+ "required": ["start_point", "end_point", "offset_span"],
254
+ "properties": {
255
+ "start_point": {
256
+ "$ref": "#/$defs/Point"
257
+ },
258
+ "end_point": {
259
+ "$ref": "#/$defs/Point"
260
+ },
261
+ "offset_span": {
262
+ "type": "object",
263
+ "required": ["start", "end"],
264
+ "properties": {
265
+ "start": {
266
+ "type": "integer",
267
+ "minimum": 0,
268
+ "description": "Starting byte offset"
269
+ },
270
+ "end": {
271
+ "type": "integer",
272
+ "minimum": 0,
273
+ "description": "Ending byte offset"
274
+ }
275
+ },
276
+ "additionalProperties": false
277
+ }
278
+ },
279
+ "additionalProperties": false
280
+ },
281
+ "Point": {
282
+ "type": "object",
283
+ "description": "A (row, column) point within a file",
284
+ "required": ["row", "column"],
285
+ "properties": {
286
+ "row": {
287
+ "type": "integer",
288
+ "minimum": 0,
289
+ "description": "Zero-indexed row number"
290
+ },
291
+ "column": {
292
+ "type": "integer",
293
+ "minimum": 0,
294
+ "description": "Zero-indexed column number"
295
+ }
296
+ },
297
+ "additionalProperties": false
298
+ }
299
+ }
300
+ }
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "json/schema/diff"
5
+
6
+ Json::Schema::Diff::CLI.start(ARGV)
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Json
4
+ module Schema
5
+ module Diff
6
+ class CLI
7
+ def self.start(args)
8
+ new.run(args)
9
+ end
10
+
11
+ def run(args)
12
+ options = parse_options(args)
13
+
14
+ if args.length != 3
15
+ puts "Usage: json-schema-diff [OPTIONS] SCHEMA OLD_JSON NEW_JSON"
16
+ puts "Try 'json-schema-diff --help' for more information."
17
+ exit 1
18
+ end
19
+
20
+ schema_file, old_file, new_file = args
21
+
22
+ begin
23
+ schema = SchemaParser.new(schema_file)
24
+ comparer = Comparer.new(schema, options[:ignore_fields] || [])
25
+
26
+ old_json = JSON.parse(File.read(old_file))
27
+ new_json = JSON.parse(File.read(new_file))
28
+
29
+ diff_result = comparer.compare(old_json, new_json)
30
+
31
+ formatter = Formatter.new(options[:format], options[:color])
32
+ puts formatter.format(diff_result)
33
+ rescue JSON::ParserError => e
34
+ puts "Error parsing JSON: #{e.message}"
35
+ exit 1
36
+ rescue Errno::ENOENT => e
37
+ puts "File not found: #{e.message}"
38
+ exit 1
39
+ rescue Error => e
40
+ puts "Error: #{e.message}"
41
+ exit 1
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ def parse_options(args)
48
+ options = {
49
+ format: "pretty",
50
+ color: true,
51
+ ignore_fields: []
52
+ }
53
+
54
+ parser = OptionParser.new do |opts|
55
+ opts.banner = "Usage: json-schema-diff [OPTIONS] SCHEMA OLD_JSON NEW_JSON"
56
+ opts.separator ""
57
+ opts.separator "Compare two JSON files using a JSON Schema to guide and annotate the diff output."
58
+ opts.separator ""
59
+ opts.separator "Arguments:"
60
+ opts.separator " SCHEMA Path to JSON Schema file"
61
+ opts.separator " OLD_JSON Path to first JSON file (baseline)"
62
+ opts.separator " NEW_JSON Path to second JSON file (comparison)"
63
+ opts.separator ""
64
+ opts.separator "Options:"
65
+
66
+ opts.on("-f", "--format FORMAT", ["pretty", "json"],
67
+ "Output format (pretty, json)") do |format|
68
+ options[:format] = format
69
+ end
70
+
71
+ opts.on("-i", "--ignore-fields FIELDS", Array,
72
+ "Comma-separated list of field paths to ignore") do |fields|
73
+ options[:ignore_fields] = fields
74
+ end
75
+
76
+ opts.on("--[no-]color", "Enable/disable colored output (default: enabled)") do |color|
77
+ options[:color] = color
78
+ end
79
+
80
+ opts.on("-h", "--help", "Show this help message") do
81
+ puts opts
82
+ exit 0
83
+ end
84
+
85
+ opts.on("-v", "--version", "Show version") do
86
+ puts "json-schema-diff #{VERSION}"
87
+ exit 0
88
+ end
89
+ end
90
+
91
+ parser.parse!(args)
92
+ options
93
+ rescue OptionParser::InvalidOption => e
94
+ puts "Error: #{e.message}"
95
+ puts "Try 'json-schema-diff --help' for more information."
96
+ exit 1
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end