fastlane-plugin-google_data_safety 0.1.2 → 0.3.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.
- checksums.yaml +4 -4
- data/README.md +53 -16
- data/lib/fastlane/plugin/google_data_safety/actions/prompt_create_data_safety_csv_action.rb +585 -0
- data/lib/fastlane/plugin/google_data_safety/actions/upload_google_data_safety_action.rb +20 -2
- data/lib/fastlane/plugin/google_data_safety/helper/prompt_create_data_safety_csv_helper.rb +17 -0
- data/lib/fastlane/plugin/google_data_safety/helper/upload_google_data_safety_helper.rb +0 -6
- data/lib/fastlane/plugin/google_data_safety/version.rb +1 -1
- metadata +13 -11
|
@@ -0,0 +1,585 @@
|
|
|
1
|
+
require 'fastlane/action'
|
|
2
|
+
require 'googleauth'
|
|
3
|
+
require_relative '../helper/prompt_create_data_safety_csv_helper'
|
|
4
|
+
require 'net/http'
|
|
5
|
+
require 'json'
|
|
6
|
+
require 'csv'
|
|
7
|
+
|
|
8
|
+
module Fastlane
|
|
9
|
+
module Actions
|
|
10
|
+
class PromptCreateDataSafetyCsvAction < Action
|
|
11
|
+
FORMULA_CHARS = %w[= + - @].freeze
|
|
12
|
+
|
|
13
|
+
# Run the prompt_create_data_safety_csv action.
|
|
14
|
+
#
|
|
15
|
+
# @param param [hash] Fastlane parameters
|
|
16
|
+
def self.run(params)
|
|
17
|
+
if !UI.interactive?
|
|
18
|
+
UI.error("Prompts questions can only display in interactive.")
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
csv_file_path = params[:csv_file]
|
|
22
|
+
csv_file_path = File.expand_path(csv_file_path)
|
|
23
|
+
|
|
24
|
+
hash = {}
|
|
25
|
+
|
|
26
|
+
self.headerDisplay(csv_file: csv_file_path)
|
|
27
|
+
self.startingQuestion(hash: hash)
|
|
28
|
+
|
|
29
|
+
template_data = Helper::PromptCreateDataSafetyHelper.template_array()
|
|
30
|
+
|
|
31
|
+
safety_data = template_data.each_with_index do | row |
|
|
32
|
+
hash_key = "#{row[0]},#{row[1]}"
|
|
33
|
+
if hash.key?(hash_key)
|
|
34
|
+
row[2] = hash[hash_key]
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
self.saveCsvFile(data: safety_data, csv_file_path: csv_file_path)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Array of valid answer strings for true.
|
|
42
|
+
def self.valid_true_answers = ["y", "yes", "true", "t"]
|
|
43
|
+
# Array of valid answer strings for false.
|
|
44
|
+
def self.valid_false_answers = ["n", "no", "false", "f"]
|
|
45
|
+
# Array of valid answer strings for ninety days selection (delete account data).
|
|
46
|
+
def self.valid_90_day_answers = ["90", "ninety"]
|
|
47
|
+
|
|
48
|
+
# Displays terminal header of Fastlane action and about the upcoming prompts.
|
|
49
|
+
#
|
|
50
|
+
# @param csv_file [String] The CSV file location that display to user where the file will be saved in
|
|
51
|
+
def self.headerDisplay(csv_file: nil)
|
|
52
|
+
UI.important("")
|
|
53
|
+
UI.important(" ____ _ ____ _ ____ __ _ ")
|
|
54
|
+
UI.important(" / ___| ___ ___ __ _| | ___ | _ \\ __ _| |_ __ _ / ___| __ _ / _| ___| |_ _ _ ")
|
|
55
|
+
UI.important(" | | _ / _ \\ / _ \\ / _` | |/ _ \\ | | | |/ _` | __/ _` | \\___ \\ / _` | |_ / _ | __| | | |")
|
|
56
|
+
UI.important(" | |_| | (_) | (_) | (_| | | __/ | |_| | (_| | || (_| | ___) | (_| | _| __| |_| |_| |")
|
|
57
|
+
UI.important(" \\____|\\___/ \\___/ \\__, |_|\\___| |____/ \\__,_|\\__\\__,_| |____/ \\__,_|_| \\___|\\__|\\__, |")
|
|
58
|
+
UI.important(" |___/ |___/ ")
|
|
59
|
+
UI.important("")
|
|
60
|
+
UI.important("By: Owen Bean")
|
|
61
|
+
UI.important("Report problems at https://github.com/owenbean400/fastlane-plugin-google_data_safety/issues")
|
|
62
|
+
UI.important("")
|
|
63
|
+
UI.important("Answer the following prompts below to generate CSV file to upload at #{csv_file}")
|
|
64
|
+
UI.important("")
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Prompt questions for Google Data Safety sheet.
|
|
68
|
+
#
|
|
69
|
+
# @param hash [hash] The hash to save data of answers to questions
|
|
70
|
+
def self.startingQuestion(hash: nil)
|
|
71
|
+
isDataInputAppInfo = self.promptForAnswerBoolean(
|
|
72
|
+
question: "Does your app collect or share any of the required user data types?",
|
|
73
|
+
hash: hash,
|
|
74
|
+
question_id: "PSL_DATA_COLLECTION_COLLECTS_PERSONAL_DATA",
|
|
75
|
+
response_id: "",
|
|
76
|
+
is_false_blank: false)
|
|
77
|
+
|
|
78
|
+
if isDataInputAppInfo
|
|
79
|
+
|
|
80
|
+
self.promptForAnswerBoolean(
|
|
81
|
+
question: "Is all of the user data collected by your app encrypted in transit?",
|
|
82
|
+
hash: hash,
|
|
83
|
+
question_id: "PSL_DATA_COLLECTION_ENCRYPTED_IN_TRANSIT",
|
|
84
|
+
response_id: "",
|
|
85
|
+
is_false_blank: false)
|
|
86
|
+
|
|
87
|
+
isDataInputAccountCreate = self.promptForAnswerBoolean(
|
|
88
|
+
question: "Does the app allow for account creation from within the app?",
|
|
89
|
+
hash: nil,
|
|
90
|
+
question_id: nil,
|
|
91
|
+
response_id: nil,
|
|
92
|
+
is_false_blank: false)
|
|
93
|
+
|
|
94
|
+
if isDataInputAccountCreate
|
|
95
|
+
self.promptForAnswerBoolean(
|
|
96
|
+
question: "Does the app allow for account creation from username and password?",
|
|
97
|
+
hash: hash,
|
|
98
|
+
question_id: "PSL_SUPPORTED_ACCOUNT_CREATION_METHODS",
|
|
99
|
+
response_id: "PSL_ACM_USER_ID_PASSWORD",
|
|
100
|
+
is_false_blank: true)
|
|
101
|
+
|
|
102
|
+
self.promptForAnswerBoolean(
|
|
103
|
+
question: "Does the app allow for account creation from username and other authentication?",
|
|
104
|
+
hash: hash,
|
|
105
|
+
question_id: "PSL_SUPPORTED_ACCOUNT_CREATION_METHODS",
|
|
106
|
+
response_id: "PSL_ACM_USER_ID_OTHER_AUTH",
|
|
107
|
+
is_false_blank: true)
|
|
108
|
+
|
|
109
|
+
self.promptForAnswerBoolean(
|
|
110
|
+
question: "Does the app allow for account creation from username, password, and other authentication?",
|
|
111
|
+
hash: hash,
|
|
112
|
+
question_id: "PSL_SUPPORTED_ACCOUNT_CREATION_METHODS",
|
|
113
|
+
response_id: "PSL_ACM_USER_ID_PASSWORD_OTHER_AUTH",
|
|
114
|
+
is_false_blank: true)
|
|
115
|
+
|
|
116
|
+
self.promptForAnswerBoolean(
|
|
117
|
+
question: "Does the app allow for account creation from OAuth?",
|
|
118
|
+
hash: hash,
|
|
119
|
+
question_id: "PSL_SUPPORTED_ACCOUNT_CREATION_METHODS",
|
|
120
|
+
response_id: "PSL_ACM_OAUTH",
|
|
121
|
+
is_false_blank: true)
|
|
122
|
+
|
|
123
|
+
isDataInputCreateOther = self.promptForAnswerBoolean(
|
|
124
|
+
question: "Does the app allow for other account creation method?",
|
|
125
|
+
hash: hash,
|
|
126
|
+
question_id: "PSL_SUPPORTED_ACCOUNT_CREATION_METHODS",
|
|
127
|
+
response_id: "PSL_ACM_OTHER",
|
|
128
|
+
is_false_blank: true)
|
|
129
|
+
|
|
130
|
+
if isDataInputCreateOther
|
|
131
|
+
self.promptForAnswer(
|
|
132
|
+
question: "Describe the method of account creation that your app supports",
|
|
133
|
+
hash: hash,
|
|
134
|
+
question_id: "PSL_ACM_SPECIFY",
|
|
135
|
+
response_id: "")
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
self.promptForUrl(
|
|
139
|
+
question: "Add a valid URL for account deletion",
|
|
140
|
+
hash: hash,
|
|
141
|
+
question_id: "PSL_ACCOUNT_DELETION_URL",
|
|
142
|
+
response_id: "")
|
|
143
|
+
else
|
|
144
|
+
isDataInputLoginOutside = self.promptForAnswerBoolean(
|
|
145
|
+
question: "Can users login to your app with accounts created outside of the app?",
|
|
146
|
+
hash: hash,
|
|
147
|
+
question_id: "PSL_HAS_OUTSIDE_APP_ACCOUNTS",
|
|
148
|
+
response_id: "",
|
|
149
|
+
is_false_blank: true)
|
|
150
|
+
|
|
151
|
+
if isDataInputLoginOutside
|
|
152
|
+
self.promptForAnswerBoolean(
|
|
153
|
+
question: "Can users create account outside of app by app identification (e.g. SIM binding, service subscription)?",
|
|
154
|
+
hash: hash,
|
|
155
|
+
question_id: "PSL_OUTSIDE_APP_ACCOUNT_TYPES",
|
|
156
|
+
response_id: "PSL_LOGIN_WITH_OUTSIDE_APP_ID",
|
|
157
|
+
is_false_blank: true)
|
|
158
|
+
|
|
159
|
+
self.promptForAnswerBoolean(
|
|
160
|
+
question: "Can users create account outside of app by employment or enterprise account?",
|
|
161
|
+
hash: hash,
|
|
162
|
+
question_id: "PSL_OUTSIDE_APP_ACCOUNT_TYPES",
|
|
163
|
+
response_id: "PSL_LOGIN_THROUGH_EMPLOYMENT_OR_ENTERPRISE_ACCOUNT",
|
|
164
|
+
is_false_blank: true)
|
|
165
|
+
|
|
166
|
+
isDataInputLoginOutsideOther = self.promptForAnswerBoolean(
|
|
167
|
+
question: "Is there other way users can create account outside of app?",
|
|
168
|
+
hash: hash,
|
|
169
|
+
question_id: "PSL_OUTSIDE_APP_ACCOUNT_TYPES",
|
|
170
|
+
response_id: "PSL_OUTSIDE_APP_ACCOUNT_TYPE_OTHER",
|
|
171
|
+
is_false_blank: true)
|
|
172
|
+
|
|
173
|
+
if isDataInputLoginOutsideOther
|
|
174
|
+
self.promptForAnswer(
|
|
175
|
+
question: "Describe how these accounts are created",
|
|
176
|
+
hash: hash,
|
|
177
|
+
question_id: "PSL_OUTSIDE_APP_ACCOUNT_TYPE_SPECIFY",
|
|
178
|
+
response_id: "")
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
dataInputDeleteData = UI.input("Do you provide a way for users to request that their data is deleted? Type 90 for deletion after 90 days. Y/N/90")
|
|
184
|
+
|
|
185
|
+
while !self.valid_true_answers.include?(dataInputDeleteData.downcase) &&
|
|
186
|
+
!self.valid_false_answers.include?(dataInputDeleteData.downcase) &&
|
|
187
|
+
!self.valid_90_day_answers.include?(dataInputDeleteData.downcase)
|
|
188
|
+
UI.important("Please provide an answer of either yes, no, or 90")
|
|
189
|
+
dataInputDeleteData = UI.input("Do you provide a way for users to request that their data is deleted? Type 90 for deletion after 90 days. Y/N/90")
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
if self.valid_true_answers.include?(dataInputDeleteData.downcase)
|
|
193
|
+
self.addToHash(hash: hash, question_id: "PSL_SUPPORT_DATA_DELETION_BY_USER", response_id: "DATA_DELETION_YES", value: "true")
|
|
194
|
+
|
|
195
|
+
self.promptForUrl(
|
|
196
|
+
question: "What URL link can users request to delete data?",
|
|
197
|
+
hash: hash,
|
|
198
|
+
question_id: "PSL_DATA_DELETION_URL",
|
|
199
|
+
response_id: "")
|
|
200
|
+
elsif self.valid_90_day_answers.include?(dataInputDeleteData.downcase)
|
|
201
|
+
self.addToHash(hash: hash, question_id: "PSL_SUPPORT_DATA_DELETION_BY_USER", response_id: "DATA_DELETION_NO_AUTO_DELETED", value: "true")
|
|
202
|
+
else
|
|
203
|
+
self.addToHash(hash: hash, question_id: "PSL_SUPPORT_DATA_DELETION_BY_USER", response_id: "DATA_DELETION_NO", value: "true")
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
self.questionDataInfo(hash: hash)
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Prompt question data type group question for Google Data Safety sheet.
|
|
211
|
+
#
|
|
212
|
+
# @param hash [hash] The hash to save data of answers to questions
|
|
213
|
+
def self.questionDataInfo(hash: nil)
|
|
214
|
+
UI.important("*** Below questions will be asked about location data ***")
|
|
215
|
+
self.questionDataType(title: "approximate location", code: "PSL_APPROX_LOCATION", group_code: "PSL_DATA_TYPES_LOCATION", hash: hash)
|
|
216
|
+
self.questionDataType(title: "precise location", code: "PSL_PRECISE_LOCATION", group_code: "PSL_DATA_TYPES_LOCATION", hash: hash)
|
|
217
|
+
|
|
218
|
+
UI.important("*** Below questions will be asked about personal information data ***")
|
|
219
|
+
self.questionDataType(title: "name", code: "PSL_NAME", group_code: "PSL_DATA_TYPES_PERSONAL", hash: hash)
|
|
220
|
+
self.questionDataType(title: "email", code: "PSL_EMAIL", group_code: "PSL_DATA_TYPES_PERSONAL", hash: hash)
|
|
221
|
+
self.questionDataType(title: "user ID", code: "PSL_USER_ACCOUNT", group_code: "PSL_DATA_TYPES_PERSONAL", hash: hash)
|
|
222
|
+
self.questionDataType(title: "address", code: "PSL_ADDRESS", group_code: "PSL_DATA_TYPES_PERSONAL", hash: hash)
|
|
223
|
+
self.questionDataType(title: "phone", code: "PSL_PHONE", group_code: "PSL_DATA_TYPES_PERSONAL", hash: hash)
|
|
224
|
+
self.questionDataType(title: "race and ethnicity", code: "PSL_RACE_ETHNICITY", group_code: "PSL_DATA_TYPES_PERSONAL", hash: hash)
|
|
225
|
+
self.questionDataType(title: "political or religious beliefs", code: "PSL_POLITICAL_RELIGIOUS", group_code: "PSL_DATA_TYPES_PERSONAL", hash: hash)
|
|
226
|
+
self.questionDataType(title: "sexual orientation", code: "PSL_SEXUAL_ORIENTATION_GENDER_IDENTITY", group_code: "PSL_DATA_TYPES_PERSONAL", hash: hash)
|
|
227
|
+
self.questionDataType(title: "other personal information", code: "PSL_OTHER_PERSONAL", group_code: "PSL_DATA_TYPES_PERSONAL", hash: hash)
|
|
228
|
+
|
|
229
|
+
UI.important("*** Below questions will be asked about financial data ***")
|
|
230
|
+
self.questionDataType(title: "user payment information", code: "PSL_CREDIT_DEBIT_BANK_ACCOUNT_NUMBER", group_code: "PSL_DATA_TYPES_FINANCIAL", hash: hash)
|
|
231
|
+
self.questionDataType(title: "purchase history", code: "PSL_PURCHASE_HISTORY", group_code: "PSL_DATA_TYPES_FINANCIAL", hash: hash)
|
|
232
|
+
self.questionDataType(title: "credit score", code: "PSL_CREDIT_SCORE", group_code: "PSL_DATA_TYPES_FINANCIAL", hash: hash)
|
|
233
|
+
self.questionDataType(title: "other financial info", code: "PSL_OTHER", group_code: "PSL_DATA_TYPES_FINANCIAL", hash: hash)
|
|
234
|
+
|
|
235
|
+
UI.important("\n*** Below questions will be asked about audio and music data ***")
|
|
236
|
+
self.questionDataType(title: "health info", code: "PSL_HEALTH", group_code: "PSL_DATA_TYPES_HEALTH_AND_FITNESS", hash: hash)
|
|
237
|
+
self.questionDataType(title: "fitness info", code: "PSL_FITNESS", group_code: "PSL_DATA_TYPES_HEALTH_AND_FITNESS", hash: hash)
|
|
238
|
+
|
|
239
|
+
UI.important("*** Below questions will be asked about email and text data ***")
|
|
240
|
+
self.questionDataType(title: "email", code: "PSL_EMAILS", group_code: "PSL_DATA_TYPES_EMAIL_AND_TEXT", hash: hash)
|
|
241
|
+
self.questionDataType(title: "SMS or MMS", code: "PSL_SMS_CALL_LOG", group_code: "PSL_DATA_TYPES_EMAIL_AND_TEXT", hash: hash)
|
|
242
|
+
self.questionDataType(title: "Other in-app messages", code: "PSL_OTHER_MESSAGES", group_code: "PSL_DATA_TYPES_EMAIL_AND_TEXT", hash: hash)
|
|
243
|
+
|
|
244
|
+
UI.important("*** Below questions will be asked about photo and video data ***")
|
|
245
|
+
self.questionDataType(title: "photo", code: "PSL_PHOTOS", group_code: "PSL_DATA_TYPES_PHOTOS_AND_VIDEOS", hash: hash)
|
|
246
|
+
self.questionDataType(title: "videos", code: "PSL_VIDEOS", group_code: "PSL_DATA_TYPES_PHOTOS_AND_VIDEOS", hash: hash)
|
|
247
|
+
|
|
248
|
+
UI.important("*** Below questions will be asked about audio and music data ***")
|
|
249
|
+
self.questionDataType(title: "voice or sound recordings", code: "PSL_AUDIO", group_code: "PSL_DATA_TYPES_AUDIO", hash: hash)
|
|
250
|
+
self.questionDataType(title: "music files", code: "PSL_MUSIC", group_code: "PSL_DATA_TYPES_AUDIO", hash: hash)
|
|
251
|
+
self.questionDataType(title: "Other audio files", code: "PSL_OTHER_AUDIO", group_code: "PSL_DATA_TYPES_AUDIO", hash: hash)
|
|
252
|
+
|
|
253
|
+
UI.important("*** Below questions will be asked about files and documents data ***")
|
|
254
|
+
self.questionDataType(title: "files and docs", code: "PSL_FILES_AND_DOCS", group_code: "PSL_DATA_TYPES_FILES_AND_DOCS", hash: hash)
|
|
255
|
+
|
|
256
|
+
UI.important("*** Below questions will be asked about calendar data ***")
|
|
257
|
+
self.questionDataType(title: "calendar events", code: "PSL_CALENDAR", group_code: "PSL_DATA_TYPES_CALENDAR", hash: hash)
|
|
258
|
+
|
|
259
|
+
UI.important("*** Below questions will be asked about contact data ***")
|
|
260
|
+
self.questionDataType(title: "contacts", code: "PSL_CONTACTS", group_code: "PSL_DATA_TYPES_CONTACTS", hash: hash)
|
|
261
|
+
|
|
262
|
+
UI.important("*** Below questions will be asked about app activity data ***")
|
|
263
|
+
self.questionDataType(title: "app interactions", code: "PSL_USER_INTERACTION", group_code: "PSL_DATA_TYPES_APP_ACTIVITY", hash: hash)
|
|
264
|
+
self.questionDataType(title: "in-app search history", code: "PSL_IN_APP_SEARCH_HISTORY", group_code: "PSL_DATA_TYPES_APP_ACTIVITY", hash: hash)
|
|
265
|
+
self.questionDataType(title: "installed apps", code: "PSL_APPS_ON_DEVICE", group_code: "PSL_DATA_TYPES_APP_ACTIVITY", hash: hash)
|
|
266
|
+
self.questionDataType(title: "other user-generated content", code: "PSL_USER_GENERATED_CONTENT", group_code: "PSL_DATA_TYPES_APP_ACTIVITY", hash: hash)
|
|
267
|
+
self.questionDataType(title: "other actions", code: "PSL_OTHER_APP_ACTIVITY", group_code: "PSL_DATA_TYPES_APP_ACTIVITY", hash: hash)
|
|
268
|
+
|
|
269
|
+
UI.important("*** Below questions will be asked about location data ***")
|
|
270
|
+
self.questionDataType(title: "web browser history", code: "PSL_WEB_BROWSING_HISTORY", group_code: "PSL_DATA_TYPES_SEARCH_AND_BROWSING", hash: hash)
|
|
271
|
+
|
|
272
|
+
UI.important("*** Below questions will be asked about app performance data ***")
|
|
273
|
+
self.questionDataType(title: "crash log", code: "PSL_CRASH_LOGS", group_code: "PSL_DATA_TYPES_APP_PERFORMANCE", hash: hash)
|
|
274
|
+
self.questionDataType(title: "performance diagnostics", code: "PSL_PERFORMANCE_DIAGNOSTICS", group_code: "PSL_DATA_TYPES_APP_PERFORMANCE", hash: hash)
|
|
275
|
+
self.questionDataType(title: "other app performance data", code: "PSL_OTHER_PERFORMANCE", group_code: "PSL_DATA_TYPES_APP_PERFORMANCE", hash: hash)
|
|
276
|
+
|
|
277
|
+
UI.important("*** Below questions will be asked about identifier data ***")
|
|
278
|
+
self.questionDataType(title: "device or other IDs", code: "PSL_DEVICE_ID", group_code: "PSL_DATA_TYPES_IDENTIFIERS", hash: hash)
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# Prompt question of a specific data type about data sharing and data collection for Google Data Safety sheet.
|
|
282
|
+
#
|
|
283
|
+
# @param title [String] The data type title
|
|
284
|
+
# @param code [String] Google Data Safety code name for CSV file format
|
|
285
|
+
# @param group_code [String] Google Data Safety group code name for CSV file format
|
|
286
|
+
# @param hash [hash] The hash to save data of answers to questions
|
|
287
|
+
def self.questionDataType(title: "", code: "", group_code: "", hash: nil)
|
|
288
|
+
isDataInputCollectOrShare = self.promptForAnswerBoolean(
|
|
289
|
+
question: "Is #{title} data being collected or shared?",
|
|
290
|
+
hash: hash,
|
|
291
|
+
question_id: group_code,
|
|
292
|
+
response_id: code,
|
|
293
|
+
is_false_blank: true)
|
|
294
|
+
|
|
295
|
+
if isDataInputCollectOrShare
|
|
296
|
+
isDataInputShared = self.promptForAnswerBoolean(
|
|
297
|
+
question: "Is #{title} data being shared?",
|
|
298
|
+
hash: hash,
|
|
299
|
+
question_id: "PSL_DATA_USAGE_RESPONSES:#{code}:PSL_DATA_USAGE_COLLECTION_AND_SHARING",
|
|
300
|
+
response_id: "PSL_DATA_USAGE_ONLY_SHARED",
|
|
301
|
+
is_false_blank: true)
|
|
302
|
+
|
|
303
|
+
if isDataInputShared
|
|
304
|
+
self.promptForAnswerBoolean(
|
|
305
|
+
question: "Is #{title} shared for app functionality?",
|
|
306
|
+
hash: hash,
|
|
307
|
+
question_id: "PSL_DATA_USAGE_RESPONSES:#{code}:DATA_USAGE_SHARING_PURPOSE",
|
|
308
|
+
response_id: "PSL_APP_FUNCTIONALITY",
|
|
309
|
+
is_false_blank: true)
|
|
310
|
+
|
|
311
|
+
self.promptForAnswerBoolean(
|
|
312
|
+
question: "Is #{title} shared for analytics?",
|
|
313
|
+
hash: hash,
|
|
314
|
+
question_id: "PSL_DATA_USAGE_RESPONSES:#{code}:DATA_USAGE_SHARING_PURPOSE",
|
|
315
|
+
response_id: "PSL_ANALYTICS",
|
|
316
|
+
is_false_blank: true)
|
|
317
|
+
|
|
318
|
+
self.promptForAnswerBoolean(
|
|
319
|
+
question: "Is #{title} shared for developer communications?",
|
|
320
|
+
hash: hash,
|
|
321
|
+
question_id: "PSL_DATA_USAGE_RESPONSES:#{code}:DATA_USAGE_SHARING_PURPOSE",
|
|
322
|
+
response_id: "PSL_DEVELOPER_COMMUNICATIONS",
|
|
323
|
+
is_false_blank: true)
|
|
324
|
+
|
|
325
|
+
self.promptForAnswerBoolean(
|
|
326
|
+
question: "Is #{title} shared for advertising or marketing?",
|
|
327
|
+
hash: hash,
|
|
328
|
+
question_id: "PSL_DATA_USAGE_RESPONSES:#{code}:DATA_USAGE_SHARING_PURPOSE",
|
|
329
|
+
response_id: "PSL_ADVERTISING",
|
|
330
|
+
is_false_blank: true)
|
|
331
|
+
|
|
332
|
+
self.promptForAnswerBoolean(
|
|
333
|
+
question: "Is #{title} shared for fraud prevention, security, or compliance?",
|
|
334
|
+
hash: hash,
|
|
335
|
+
question_id: "PSL_DATA_USAGE_RESPONSES:#{code}:DATA_USAGE_SHARING_PURPOSE",
|
|
336
|
+
response_id: "PSL_FRAUD_PREVENTION_SECURITY",
|
|
337
|
+
is_false_blank: true)
|
|
338
|
+
|
|
339
|
+
self.promptForAnswerBoolean(
|
|
340
|
+
question: "Is #{title} shared for personalizing the app?",
|
|
341
|
+
hash: hash,
|
|
342
|
+
question_id: "PSL_DATA_USAGE_RESPONSES:#{code}:DATA_USAGE_SHARING_PURPOSE",
|
|
343
|
+
response_id: "PSL_PERSONALIZATION",
|
|
344
|
+
is_false_blank: true)
|
|
345
|
+
|
|
346
|
+
self.promptForAnswerBoolean(
|
|
347
|
+
question: "Is #{title} shared to manage account?",
|
|
348
|
+
hash: hash,
|
|
349
|
+
question_id: "PSL_DATA_USAGE_RESPONSES:#{code}:DATA_USAGE_SHARING_PURPOSE",
|
|
350
|
+
response_id: "PSL_ACCOUNT_MANAGEMENT",
|
|
351
|
+
is_false_blank: true)
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
isDataInputCollected = self.promptForAnswerBoolean(
|
|
355
|
+
question: "Is #{title} data being collected?",
|
|
356
|
+
hash: hash,
|
|
357
|
+
question_id: "PSL_DATA_USAGE_RESPONSES:#{code}:PSL_DATA_USAGE_COLLECTION_AND_SHARING",
|
|
358
|
+
response_id: "PSL_DATA_USAGE_ONLY_COLLECTED",
|
|
359
|
+
is_false_blank: true)
|
|
360
|
+
|
|
361
|
+
if isDataInputCollected
|
|
362
|
+
self.promptForAnswerBoolean(
|
|
363
|
+
question: "Is #{title} data processed ephemerally?",
|
|
364
|
+
hash: hash,
|
|
365
|
+
question_id: "PSL_DATA_USAGE_RESPONSES:#{code}:PSL_DATA_USAGE_EPHEMERAL",
|
|
366
|
+
response_id: "",
|
|
367
|
+
is_false_blank: false)
|
|
368
|
+
|
|
369
|
+
isDataInputRequired = self.promptForAnswerBoolean(
|
|
370
|
+
question: "Is #{title} data required for your app?",
|
|
371
|
+
hash: nil,
|
|
372
|
+
question_id: nil,
|
|
373
|
+
response_id: nil,
|
|
374
|
+
is_false_blank: false)
|
|
375
|
+
|
|
376
|
+
if isDataInputRequired
|
|
377
|
+
self.addToHash(hash: hash, question_id: "PSL_DATA_USAGE_RESPONSES:#{code}:DATA_USAGE_USER_CONTROL", response_id: "PSL_DATA_USAGE_USER_CONTROL_REQUIRED", value: "true")
|
|
378
|
+
else
|
|
379
|
+
self.addToHash(hash: hash, question_id: "PSL_DATA_USAGE_RESPONSES:#{code}:DATA_USAGE_USER_CONTROL", response_id: "PSL_DATA_USAGE_USER_CONTROL_OPTIONAL", value: "true")
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
self.promptForAnswerBoolean(
|
|
383
|
+
question: "Is #{title} collected for app functionality?",
|
|
384
|
+
hash: hash,
|
|
385
|
+
question_id: "PSL_DATA_USAGE_RESPONSES:#{code}:DATA_USAGE_COLLECTION_PURPOSE",
|
|
386
|
+
response_id: "PSL_APP_FUNCTIONALITY",
|
|
387
|
+
is_false_blank: true)
|
|
388
|
+
|
|
389
|
+
self.promptForAnswerBoolean(
|
|
390
|
+
question: "Is #{title} collected for analytics?",
|
|
391
|
+
hash: hash,
|
|
392
|
+
question_id: "PSL_DATA_USAGE_RESPONSES:#{code}:DATA_USAGE_COLLECTION_PURPOSE",
|
|
393
|
+
response_id: "PSL_ANALYTICS",
|
|
394
|
+
is_false_blank: true)
|
|
395
|
+
|
|
396
|
+
self.promptForAnswerBoolean(
|
|
397
|
+
question: "Is #{title} collected for developer communications?",
|
|
398
|
+
hash: hash,
|
|
399
|
+
question_id: "PSL_DATA_USAGE_RESPONSES:#{code}:DATA_USAGE_COLLECTION_PURPOSE",
|
|
400
|
+
response_id: "PSL_DEVELOPER_COMMUNICATIONS",
|
|
401
|
+
is_false_blank: true)
|
|
402
|
+
|
|
403
|
+
self.promptForAnswerBoolean(
|
|
404
|
+
question: "Is #{title} collected for advertising or marketing?",
|
|
405
|
+
hash: hash,
|
|
406
|
+
question_id: "PSL_DATA_USAGE_RESPONSES:#{code}:DATA_USAGE_COLLECTION_PURPOSE",
|
|
407
|
+
response_id: "PSL_ADVERTISING",
|
|
408
|
+
is_false_blank: true)
|
|
409
|
+
|
|
410
|
+
self.promptForAnswerBoolean(
|
|
411
|
+
question: "Is #{title} collected for fraud prevention, security, or compliance?",
|
|
412
|
+
hash: hash,
|
|
413
|
+
question_id: "PSL_DATA_USAGE_RESPONSES:#{code}:DATA_USAGE_COLLECTION_PURPOSE",
|
|
414
|
+
response_id: "PSL_FRAUD_PREVENTION_SECURITY",
|
|
415
|
+
is_false_blank: true)
|
|
416
|
+
|
|
417
|
+
self.promptForAnswerBoolean(
|
|
418
|
+
question: "Is #{title} collected for personalizing the app?",
|
|
419
|
+
hash: hash,
|
|
420
|
+
question_id: "PSL_DATA_USAGE_RESPONSES:#{code}:DATA_USAGE_COLLECTION_PURPOSE",
|
|
421
|
+
response_id: "PSL_PERSONALIZATION",
|
|
422
|
+
is_false_blank: true)
|
|
423
|
+
|
|
424
|
+
self.promptForAnswerBoolean(
|
|
425
|
+
question: "Is #{title} collected to manage account?",
|
|
426
|
+
hash: hash,
|
|
427
|
+
question_id: "PSL_DATA_USAGE_RESPONSES:#{code}:DATA_USAGE_COLLECTION_PURPOSE",
|
|
428
|
+
response_id: "PSL_ACCOUNT_MANAGEMENT",
|
|
429
|
+
is_false_blank: true)
|
|
430
|
+
end
|
|
431
|
+
end
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
# Save CSV file with data sanitation.
|
|
435
|
+
#
|
|
436
|
+
# @param data [hash] Google Data Safety hash of CSV content
|
|
437
|
+
# @param csv_file_path [String] CSV file path to save file at
|
|
438
|
+
def self.saveCsvFile(data: nil, csv_file_path: "")
|
|
439
|
+
FileUtils.mkdir_p(File.dirname(csv_file_path))
|
|
440
|
+
|
|
441
|
+
CSV.open(csv_file_path, "w") do |csv|
|
|
442
|
+
data.each do |row|
|
|
443
|
+
sanitized_row = row.map do |cell|
|
|
444
|
+
if cell.is_a?(String)
|
|
445
|
+
cleaned = cell.gsub(/[\t\r\n]/, '').strip
|
|
446
|
+
|
|
447
|
+
if cleaned.lstrip.start_with?(*FORMULA_CHARS)
|
|
448
|
+
cleaned = "'#{cleaned}"
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
cleaned
|
|
452
|
+
else
|
|
453
|
+
cell
|
|
454
|
+
end
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
csv << sanitized_row
|
|
458
|
+
end
|
|
459
|
+
end
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
# Prompt question to terminal for a yes or no answer. Reprompt user if answer is not a valid yes or no.
|
|
463
|
+
#
|
|
464
|
+
# @param question [String] The question that displays in terminal
|
|
465
|
+
# @param hash [hash] The hash data for Google Data Safety.
|
|
466
|
+
# @param question_id [String] The question id to add in the hash data. Review CSV file from Google Data Safety sheet for more details.
|
|
467
|
+
# @param response_id [String] The response id to add in the hash data. Review CSV file from Google Data Safety sheet for more details.
|
|
468
|
+
# @param is_false_blank [Boolean] Whether the false answer should be false or blank in CSV file. It is dependent on the question ID and response ID for Google Data Safety sheet format.
|
|
469
|
+
# @return [Boolean] The user's answer to the question
|
|
470
|
+
def self.promptForAnswerBoolean(question: "", hash: nil, question_id: nil, response_id: "", is_false_blank: false)
|
|
471
|
+
answer = UI.input(question + " Y/N")
|
|
472
|
+
|
|
473
|
+
while !self.valid_true_answers.include?(answer.downcase) && !self.valid_false_answers.include?(answer.downcase)
|
|
474
|
+
UI.important("Please provide \"y\", \"yes\", \"true\", or \"t\" for yes; and \"n\", \"no\", \"false\", \"f\" for no")
|
|
475
|
+
answer = UI.input(question + " Y/N")
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
if self.valid_true_answers.include?(answer.downcase)
|
|
479
|
+
self.addToHash(hash: hash, question_id: question_id, response_id: response_id, value: "true")
|
|
480
|
+
return true
|
|
481
|
+
else
|
|
482
|
+
self.addToHash(hash: hash, question_id: question_id, response_id: response_id, value: is_false_blank ? "" : "false")
|
|
483
|
+
return false
|
|
484
|
+
end
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
# Prompt question to terminal for a string answer. Reprompt user if answer is empty string.
|
|
488
|
+
#
|
|
489
|
+
# @param question [String] The question that displays in terminal
|
|
490
|
+
# @param hash [hash] The hash data for Google Data Safety
|
|
491
|
+
# @param question_id [String] The question id to add in the hash data. Review CSV file from Google Data Safety sheet for more details.
|
|
492
|
+
# @param response_id [String] The response id to add in the hash data. Review CSV file from Google Data Safety sheet for more details.
|
|
493
|
+
# @return [String] The user's answer to question
|
|
494
|
+
def self.promptForAnswer(question: "", hash: nil, question_id: nil, response_id: "")
|
|
495
|
+
answer = UI.input(question)
|
|
496
|
+
|
|
497
|
+
while answer.empty?
|
|
498
|
+
UI.important("Please provide an answer")
|
|
499
|
+
answer = UI.input(question)
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
self.addToHash(hash: hash, question_id: question_id, response_id: response_id, value: answer)
|
|
503
|
+
return answer
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
# Prompt question to terminal for URL answer. Reprompt user if answer is not a valid URL.
|
|
507
|
+
#
|
|
508
|
+
# @param question [String] The question that displays in terminal
|
|
509
|
+
# @param hash [hash] The hash data for Google Data Safety
|
|
510
|
+
# @param question_id [String] The question id to add in the hash data. Review CSV file from Google Data Safety sheet for more details.
|
|
511
|
+
# @param response_id [String] The response id to add in the hash data. Review CSV file from Google Data Safety sheet for more details.
|
|
512
|
+
# @return [String] The user's answer to question
|
|
513
|
+
def self.promptForUrl(question: "", hash: nil, question_id: nil, response_id: "")
|
|
514
|
+
answer = UI.input(question)
|
|
515
|
+
|
|
516
|
+
while !self.validUrl?(answer)
|
|
517
|
+
UI.important("Please provide a proper URL")
|
|
518
|
+
answer = UI.input(question)
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
self.addToHash(hash: hash, question_id: question_id, response_id: response_id, value: answer)
|
|
522
|
+
return answer
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
# Detect if URL is valid format.
|
|
526
|
+
#
|
|
527
|
+
# @param url [String] The url to validate
|
|
528
|
+
# @return [Boolean] The url is valid
|
|
529
|
+
def self.validUrl?(url)
|
|
530
|
+
uri = URI.parse(url)
|
|
531
|
+
uri.is_a?(URI::HTTP) && !uri.host.nil?
|
|
532
|
+
rescue URI::InvalidURIError
|
|
533
|
+
false
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
# Prompt question and answer to Google Data Safety hash
|
|
537
|
+
#
|
|
538
|
+
# @param hash [hash] The hash data for Google Data Safety
|
|
539
|
+
# @param question_id [String] The question id to add in the hash data. Review CSV file from Google Data Safety sheet for more details.
|
|
540
|
+
# @param response_id [String] The response id to add in the hash data. Review CSV file from Google Data Safety sheet for more details.
|
|
541
|
+
# @param value [String] The answer value
|
|
542
|
+
def self.addToHash(hash: nil, question_id: nil, response_id: "", value: nil)
|
|
543
|
+
if !value.nil? && !hash.nil?
|
|
544
|
+
hash["#{question_id},#{response_id}"] = value
|
|
545
|
+
end
|
|
546
|
+
end
|
|
547
|
+
|
|
548
|
+
def self.description
|
|
549
|
+
"Prompt user questions to generate Google data safety sheet"
|
|
550
|
+
end
|
|
551
|
+
|
|
552
|
+
def self.authors
|
|
553
|
+
["Owen Bean"]
|
|
554
|
+
end
|
|
555
|
+
|
|
556
|
+
def self.return_value
|
|
557
|
+
# If your method provides a return value, you can describe here what it does
|
|
558
|
+
end
|
|
559
|
+
|
|
560
|
+
def self.details
|
|
561
|
+
"Command line question prompts for information about Android app data collection"
|
|
562
|
+
end
|
|
563
|
+
|
|
564
|
+
def self.available_options
|
|
565
|
+
[
|
|
566
|
+
FastlaneCore::ConfigItem.new(
|
|
567
|
+
key: :csv_file,
|
|
568
|
+
env_name: "CSV_FILE",
|
|
569
|
+
description: "File location to save csv file for Google data safety sheet",
|
|
570
|
+
optional: false,
|
|
571
|
+
type: String
|
|
572
|
+
)
|
|
573
|
+
]
|
|
574
|
+
end
|
|
575
|
+
|
|
576
|
+
def self.is_supported?(platform)
|
|
577
|
+
# Adjust this if your plugin only works for a particular platform (iOS vs. Android, for example)
|
|
578
|
+
# See: https://docs.fastlane.tools/advanced/#control-configuration-by-lane-and-by-platform
|
|
579
|
+
#
|
|
580
|
+
# [:ios, :mac, :android].include?(platform)
|
|
581
|
+
true
|
|
582
|
+
end
|
|
583
|
+
end
|
|
584
|
+
end
|
|
585
|
+
end
|
|
@@ -17,7 +17,13 @@ module Fastlane
|
|
|
17
17
|
|
|
18
18
|
access_token = body["access_token"]
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
debug = false
|
|
21
|
+
|
|
22
|
+
if !params[:debug].nil?
|
|
23
|
+
debug = params[:debug]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
self.send_data_safety_sheet(package_name: params[:package_name], auth_token: access_token, csv_content: csv_content, debug: debug)
|
|
21
27
|
|
|
22
28
|
end
|
|
23
29
|
|
|
@@ -79,7 +85,7 @@ module Fastlane
|
|
|
79
85
|
token
|
|
80
86
|
end
|
|
81
87
|
|
|
82
|
-
def self.send_data_safety_sheet(package_name: nil, auth_token: nil, csv_content: nil)
|
|
88
|
+
def self.send_data_safety_sheet(package_name: nil, auth_token: nil, csv_content: nil, debug: false)
|
|
83
89
|
if package_name.nil?
|
|
84
90
|
if UI.interactive?
|
|
85
91
|
UI.important("To not be asked package name, you can specify it using 'package_name'.")
|
|
@@ -111,6 +117,11 @@ module Fastlane
|
|
|
111
117
|
else
|
|
112
118
|
UI.error("Google Data Safety sheet upload error with API")
|
|
113
119
|
end
|
|
120
|
+
|
|
121
|
+
if debug
|
|
122
|
+
UI.important("Google Data Safety API response body")
|
|
123
|
+
UI.important(response.body)
|
|
124
|
+
end
|
|
114
125
|
end
|
|
115
126
|
|
|
116
127
|
end
|
|
@@ -169,6 +180,13 @@ module Fastlane
|
|
|
169
180
|
optional: true,
|
|
170
181
|
type: Hash
|
|
171
182
|
),
|
|
183
|
+
FastlaneCore::ConfigItem.new(
|
|
184
|
+
key: :debug,
|
|
185
|
+
env_name: "DEBUG",
|
|
186
|
+
description: "Set to debug mode for troubleshooting",
|
|
187
|
+
optional: true,
|
|
188
|
+
type: Boolean
|
|
189
|
+
),
|
|
172
190
|
]
|
|
173
191
|
end
|
|
174
192
|
|