Autopsy  4.12.0
Graphical digital forensics platform for The Sleuth Kit and other tools.
SqliteEamDbSettings.java
Go to the documentation of this file.
1 /*
2  * Central Repository
3  *
4  * Copyright 2015-2019 Basis Technology Corp.
5  * Contact: carrier <at> sleuthkit <dot> org
6  *
7  * Licensed under the Apache License, Version 2.0 (the "License");
8  * you may not use this file except in compliance with the License.
9  * You may obtain a copy of the License at
10  *
11  * http://www.apache.org/licenses/LICENSE-2.0
12  *
13  * Unless required by applicable law or agreed to in writing, software
14  * distributed under the License is distributed on an "AS IS" BASIS,
15  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16  * See the License for the specific language governing permissions and
17  * limitations under the License.
18  */
19 package org.sleuthkit.autopsy.centralrepository.datamodel;
20 
21 import java.io.File;
22 import java.io.IOException;
23 import java.nio.file.Files;
24 import java.nio.file.InvalidPathException;
25 import java.sql.Connection;
26 import java.sql.DriverManager;
27 import java.sql.SQLException;
28 import java.sql.Statement;
29 import java.util.List;
30 import java.util.logging.Level;
31 import java.util.regex.Pattern;
35 import static org.sleuthkit.autopsy.centralrepository.datamodel.AbstractSqlEamDb.SOFTWARE_CR_DB_SCHEMA_VERSION;
36 
43 public final class SqliteEamDbSettings {
44 
45  private final static Logger LOGGER = Logger.getLogger(SqliteEamDbSettings.class.getName());
46  private final static String DEFAULT_DBNAME = "central_repository.db"; // NON-NLS
47  private final static String DEFAULT_DBDIRECTORY = PlatformUtil.getUserDirectory() + File.separator + "central_repository"; // NON-NLS
48  private final static String JDBC_DRIVER = "org.sqlite.JDBC"; // NON-NLS
49  private final static String JDBC_BASE_URI = "jdbc:sqlite:"; // NON-NLS
50  private final static String VALIDATION_QUERY = "SELECT count(*) from sqlite_master"; // NON-NLS
51  private final static String PRAGMA_SYNC_OFF = "PRAGMA synchronous = OFF";
52  private final static String PRAGMA_SYNC_NORMAL = "PRAGMA synchronous = NORMAL";
53  private final static String PRAGMA_JOURNAL_WAL = "PRAGMA journal_mode = WAL";
54  private final static String PRAGMA_READ_UNCOMMITTED_TRUE = "PRAGMA read_uncommitted = True";
55  private final static String PRAGMA_ENCODING_UTF8 = "PRAGMA encoding = 'UTF-8'";
56  private final static String PRAGMA_PAGE_SIZE_4096 = "PRAGMA page_size = 4096";
57  private final static String PRAGMA_FOREIGN_KEYS_ON = "PRAGMA foreign_keys = ON";
58  private final static String DB_NAMES_REGEX = "[a-z][a-z0-9_]*(\\.db)?";
59  private String dbName;
60  private String dbDirectory;
61  private int bulkThreshold;
62 
64  loadSettings();
65  }
66 
67  public void loadSettings() {
68  dbName = ModuleSettings.getConfigSetting("CentralRepository", "db.sqlite.dbName"); // NON-NLS
69  if (dbName == null || dbName.isEmpty()) {
70  dbName = DEFAULT_DBNAME;
71  }
72 
73  dbDirectory = ModuleSettings.getConfigSetting("CentralRepository", "db.sqlite.dbDirectory"); // NON-NLS
74  if (dbDirectory == null || dbDirectory.isEmpty()) {
75  dbDirectory = DEFAULT_DBDIRECTORY;
76  }
77 
78  try {
79  String bulkThresholdString = ModuleSettings.getConfigSetting("CentralRepository", "db.sqlite.bulkThreshold"); // NON-NLS
80  if (bulkThresholdString == null || bulkThresholdString.isEmpty()) {
81  this.bulkThreshold = AbstractSqlEamDb.DEFAULT_BULK_THRESHHOLD;
82  } else {
83  this.bulkThreshold = Integer.parseInt(bulkThresholdString);
84  if (getBulkThreshold() <= 0) {
85  this.bulkThreshold = AbstractSqlEamDb.DEFAULT_BULK_THRESHHOLD;
86  }
87  }
88  } catch (NumberFormatException ex) {
89  this.bulkThreshold = AbstractSqlEamDb.DEFAULT_BULK_THRESHHOLD;
90  }
91  }
92 
93  public void saveSettings() {
95 
96  ModuleSettings.setConfigSetting("CentralRepository", "db.sqlite.dbName", getDbName()); // NON-NLS
97  ModuleSettings.setConfigSetting("CentralRepository", "db.sqlite.dbDirectory", getDbDirectory()); // NON-NLS
98  ModuleSettings.setConfigSetting("CentralRepository", "db.sqlite.bulkThreshold", Integer.toString(getBulkThreshold())); // NON-NLS
99  }
100 
106  public boolean dbFileExists() {
107  File dbFile = new File(getFileNameWithPath());
108  if (!dbFile.exists()) {
109  return false;
110  }
111  // It's unlikely, but make sure the file isn't actually a directory
112  return (!dbFile.isDirectory());
113  }
114 
120  public boolean dbDirectoryExists() {
121  // Ensure dbDirectory is a valid directory
122  File dbDir = new File(getDbDirectory());
123 
124  if (!dbDir.exists()) {
125  return false;
126  } else if (!dbDir.isDirectory()) {
127  return false;
128  }
129 
130  return true;
131 
132  }
133 
139  public boolean createDbDirectory() {
140  if (!dbDirectoryExists()) {
141  try {
142  File dbDir = new File(getDbDirectory());
143  Files.createDirectories(dbDir.toPath());
144  LOGGER.log(Level.INFO, "sqlite directory did not exist, created it at {0}.", getDbDirectory()); // NON-NLS
145  } catch (IOException | InvalidPathException | SecurityException ex) {
146  LOGGER.log(Level.SEVERE, "Failed to create sqlite database directory.", ex); // NON-NLS
147  return false;
148  }
149  }
150 
151  return true;
152  }
153 
159  public boolean deleteDatabase() {
160  File dbFile = new File(this.getFileNameWithPath());
161  return dbFile.delete();
162  }
163 
169  String getConnectionURL() {
170  StringBuilder url = new StringBuilder();
171  url.append(getJDBCBaseURI());
172  url.append(getFileNameWithPath());
173 
174  return url.toString();
175  }
176 
185  private Connection getEphemeralConnection() {
186  if (!dbDirectoryExists()) {
187  return null;
188  }
189 
190  Connection conn;
191  try {
192  String url = getConnectionURL();
193  Class.forName(getDriver());
194  conn = DriverManager.getConnection(url);
195  } catch (ClassNotFoundException | SQLException ex) {
196  LOGGER.log(Level.SEVERE, "Failed to acquire ephemeral connection to sqlite.", ex); // NON-NLS
197  conn = null;
198  }
199  return conn;
200  }
201 
208  public boolean verifyConnection() {
209  Connection conn = getEphemeralConnection();
210  if (null == conn) {
211  return false;
212  }
213 
214  boolean result = EamDbUtil.executeValidationQuery(conn, VALIDATION_QUERY);
216  return result;
217  }
218 
225  public boolean verifyDatabaseSchema() {
226  Connection conn = getEphemeralConnection();
227  if (null == conn) {
228  return false;
229  }
230 
231  boolean result = EamDbUtil.schemaVersionIsSet(conn);
233  return result;
234  }
235 
247  public boolean initializeDatabaseSchema() {
248  // The "id" column is an alias for the built-in 64-bit int "rowid" column.
249  // It is autoincrementing by default and must be of type "integer primary key".
250  // We've omitted the autoincrement argument because we are not currently
251  // using the id value to search for specific rows, so we do not care
252  // if a rowid is re-used after an existing rows was previously deleted.
253  StringBuilder createOrganizationsTable = new StringBuilder();
254  createOrganizationsTable.append("CREATE TABLE IF NOT EXISTS organizations (");
255  createOrganizationsTable.append("id integer primary key autoincrement NOT NULL,");
256  createOrganizationsTable.append("org_name text NOT NULL,");
257  createOrganizationsTable.append("poc_name text NOT NULL,");
258  createOrganizationsTable.append("poc_email text NOT NULL,");
259  createOrganizationsTable.append("poc_phone text NOT NULL,");
260  createOrganizationsTable.append("CONSTRAINT org_name_unique UNIQUE (org_name)");
261  createOrganizationsTable.append(")");
262 
263  // NOTE: The organizations will only have a small number of rows, so
264  // an index is probably not worthwhile.
265  StringBuilder createCasesTable = new StringBuilder();
266  createCasesTable.append("CREATE TABLE IF NOT EXISTS cases (");
267  createCasesTable.append("id integer primary key autoincrement NOT NULL,");
268  createCasesTable.append("case_uid text NOT NULL,");
269  createCasesTable.append("org_id integer,");
270  createCasesTable.append("case_name text NOT NULL,");
271  createCasesTable.append("creation_date text NOT NULL,");
272  createCasesTable.append("case_number text,");
273  createCasesTable.append("examiner_name text,");
274  createCasesTable.append("examiner_email text,");
275  createCasesTable.append("examiner_phone text,");
276  createCasesTable.append("notes text,");
277  createCasesTable.append("CONSTRAINT case_uid_unique UNIQUE(case_uid) ON CONFLICT IGNORE,");
278  createCasesTable.append("foreign key (org_id) references organizations(id) ON UPDATE SET NULL ON DELETE SET NULL");
279  createCasesTable.append(")");
280 
281  // NOTE: when there are few cases in the cases table, these indices may not be worthwhile
282  String casesIdx1 = "CREATE INDEX IF NOT EXISTS cases_org_id ON cases (org_id)";
283  String casesIdx2 = "CREATE INDEX IF NOT EXISTS cases_case_uid ON cases (case_uid)";
284 
285  StringBuilder createReferenceSetsTable = new StringBuilder();
286  createReferenceSetsTable.append("CREATE TABLE IF NOT EXISTS reference_sets (");
287  createReferenceSetsTable.append("id integer primary key autoincrement NOT NULL,");
288  createReferenceSetsTable.append("org_id integer NOT NULL,");
289  createReferenceSetsTable.append("set_name text NOT NULL,");
290  createReferenceSetsTable.append("version text NOT NULL,");
291  createReferenceSetsTable.append("known_status integer NOT NULL,");
292  createReferenceSetsTable.append("read_only boolean NOT NULL,");
293  createReferenceSetsTable.append("type integer NOT NULL,");
294  createReferenceSetsTable.append("import_date text NOT NULL,");
295  createReferenceSetsTable.append("foreign key (org_id) references organizations(id) ON UPDATE SET NULL ON DELETE SET NULL,");
296  createReferenceSetsTable.append("CONSTRAINT hash_set_unique UNIQUE (set_name, version)");
297  createReferenceSetsTable.append(")");
298 
299  String referenceSetsIdx1 = "CREATE INDEX IF NOT EXISTS reference_sets_org_id ON reference_sets (org_id)";
300 
301  // Each "%s" will be replaced with the relevant reference_TYPE table name.
302  StringBuilder createReferenceTypesTableTemplate = new StringBuilder();
303  createReferenceTypesTableTemplate.append("CREATE TABLE IF NOT EXISTS %s (");
304  createReferenceTypesTableTemplate.append("id integer primary key autoincrement NOT NULL,");
305  createReferenceTypesTableTemplate.append("reference_set_id integer,");
306  createReferenceTypesTableTemplate.append("value text NOT NULL,");
307  createReferenceTypesTableTemplate.append("known_status integer NOT NULL,");
308  createReferenceTypesTableTemplate.append("comment text,");
309  createReferenceTypesTableTemplate.append("CONSTRAINT %s_multi_unique UNIQUE(reference_set_id, value) ON CONFLICT IGNORE,");
310  createReferenceTypesTableTemplate.append("foreign key (reference_set_id) references reference_sets(id) ON UPDATE SET NULL ON DELETE SET NULL");
311  createReferenceTypesTableTemplate.append(")");
312 
313  // Each "%s" will be replaced with the relevant reference_TYPE table name.
314  String referenceTypesIdx1 = "CREATE INDEX IF NOT EXISTS %s_value ON %s (value)";
315  String referenceTypesIdx2 = "CREATE INDEX IF NOT EXISTS %s_value_known_status ON %s (value, known_status)";
316 
317  StringBuilder createCorrelationTypesTable = new StringBuilder();
318  createCorrelationTypesTable.append("CREATE TABLE IF NOT EXISTS correlation_types (");
319  createCorrelationTypesTable.append("id integer primary key autoincrement NOT NULL,");
320  createCorrelationTypesTable.append("display_name text NOT NULL,");
321  createCorrelationTypesTable.append("db_table_name text NOT NULL,");
322  createCorrelationTypesTable.append("supported integer NOT NULL,");
323  createCorrelationTypesTable.append("enabled integer NOT NULL,");
324  createCorrelationTypesTable.append("CONSTRAINT correlation_types_names UNIQUE (display_name, db_table_name)");
325  createCorrelationTypesTable.append(")");
326 
327  String createArtifactInstancesTableTemplate = getCreateArtifactInstancesTableTemplate();
328 
329  String instancesCaseIdIdx = getAddCaseIdIndexTemplate();
330  String instancesDatasourceIdIdx = getAddDataSourceIdIndexTemplate();
331  String instancesValueIdx = getAddValueIndexTemplate();
332  String instancesKnownStatusIdx = getAddKnownStatusIndexTemplate();
333  String instancesObjectIdIdx = getAddObjectIdIndexTemplate();
334 
335  // NOTE: the db_info table currenly only has 1 row, so having an index
336  // provides no benefit.
337  Connection conn = null;
338  try {
339  conn = getEphemeralConnection();
340  if (null == conn) {
341  return false;
342  }
343  Statement stmt = conn.createStatement();
344  stmt.execute(PRAGMA_JOURNAL_WAL);
345  stmt.execute(PRAGMA_SYNC_OFF);
346  stmt.execute(PRAGMA_READ_UNCOMMITTED_TRUE);
347  stmt.execute(PRAGMA_ENCODING_UTF8);
348  stmt.execute(PRAGMA_PAGE_SIZE_4096);
349  stmt.execute(PRAGMA_FOREIGN_KEYS_ON);
350 
351  stmt.execute(createOrganizationsTable.toString());
352 
353  stmt.execute(createCasesTable.toString());
354  stmt.execute(casesIdx1);
355  stmt.execute(casesIdx2);
356 
357  stmt.execute(getCreateDataSourcesTableStatement());
358  stmt.execute(getAddDataSourcesNameIndexStatement());
359  stmt.execute(getAddDataSourcesObjectIdIndexStatement());
360 
361  stmt.execute(createReferenceSetsTable.toString());
362  stmt.execute(referenceSetsIdx1);
363 
364  stmt.execute(createCorrelationTypesTable.toString());
365 
366  /*
367  * Note that the essentially useless id column in the following
368  * table is required for backwards compatibility. Otherwise, the
369  * name column could be the primary key.
370  */
371  stmt.execute("CREATE TABLE db_info (id INTEGER PRIMARY KEY, name TEXT UNIQUE NOT NULL, value TEXT NOT NULL)");
372  stmt.execute("INSERT INTO db_info (name, value) VALUES ('" + AbstractSqlEamDb.SCHEMA_MAJOR_VERSION_KEY + "', '" + SOFTWARE_CR_DB_SCHEMA_VERSION.getMajor() + "')");
373  stmt.execute("INSERT INTO db_info (name, value) VALUES ('" + AbstractSqlEamDb.SCHEMA_MINOR_VERSION_KEY + "', '" + SOFTWARE_CR_DB_SCHEMA_VERSION.getMinor() + "')");
374  stmt.execute("INSERT INTO db_info (name, value) VALUES ('" + AbstractSqlEamDb.CREATION_SCHEMA_MAJOR_VERSION_KEY + "', '" + SOFTWARE_CR_DB_SCHEMA_VERSION.getMajor() + "')");
375  stmt.execute("INSERT INTO db_info (name, value) VALUES ('" + AbstractSqlEamDb.CREATION_SCHEMA_MINOR_VERSION_KEY + "', '" + SOFTWARE_CR_DB_SCHEMA_VERSION.getMinor() + "')");
376 
377  // Create a separate instance and reference table for each artifact type
379 
380  String reference_type_dbname;
381  String instance_type_dbname;
382  for (CorrelationAttributeInstance.Type type : DEFAULT_CORRELATION_TYPES) {
383  reference_type_dbname = EamDbUtil.correlationTypeToReferenceTableName(type);
384  instance_type_dbname = EamDbUtil.correlationTypeToInstanceTableName(type);
385 
386  stmt.execute(String.format(createArtifactInstancesTableTemplate, instance_type_dbname, instance_type_dbname));
387  stmt.execute(String.format(instancesCaseIdIdx, instance_type_dbname, instance_type_dbname));
388  stmt.execute(String.format(instancesDatasourceIdIdx, instance_type_dbname, instance_type_dbname));
389  stmt.execute(String.format(instancesValueIdx, instance_type_dbname, instance_type_dbname));
390  stmt.execute(String.format(instancesKnownStatusIdx, instance_type_dbname, instance_type_dbname));
391  stmt.execute(String.format(instancesObjectIdIdx, instance_type_dbname, instance_type_dbname));
392 
393  // FUTURE: allow more than the FILES type
394  if (type.getId() == CorrelationAttributeInstance.FILES_TYPE_ID) {
395  stmt.execute(String.format(createReferenceTypesTableTemplate.toString(), reference_type_dbname, reference_type_dbname));
396  stmt.execute(String.format(referenceTypesIdx1, reference_type_dbname, reference_type_dbname));
397  stmt.execute(String.format(referenceTypesIdx2, reference_type_dbname, reference_type_dbname));
398  }
399  }
400  } catch (SQLException ex) {
401  LOGGER.log(Level.SEVERE, "Error initializing db schema.", ex); // NON-NLS
402  return false;
403  } catch (EamDbException ex) {
404  LOGGER.log(Level.SEVERE, "Error getting default correlation types. Likely due to one or more Type's with an invalid db table name."); // NON-NLS
405  return false;
406  } finally {
408  }
409  return true;
410  }
411 
419  static String getCreateArtifactInstancesTableTemplate() {
420  // Each "%s" will be replaced with the relevant TYPE_instances table name.
421  return "CREATE TABLE IF NOT EXISTS %s (id integer primary key autoincrement NOT NULL,"
422  + "case_id integer NOT NULL,data_source_id integer NOT NULL,value text NOT NULL,"
423  + "file_path text NOT NULL,known_status integer NOT NULL,comment text,file_obj_id integer,"
424  + "CONSTRAINT %s_multi_unique UNIQUE(data_source_id, value, file_path) ON CONFLICT IGNORE,"
425  + "foreign key (case_id) references cases(id) ON UPDATE SET NULL ON DELETE SET NULL,"
426  + "foreign key (data_source_id) references data_sources(id) ON UPDATE SET NULL ON DELETE SET NULL)";
427  }
428 
436  static String getCreateDataSourcesTableStatement() {
437  return "CREATE TABLE IF NOT EXISTS data_sources (id integer primary key autoincrement NOT NULL,"
438  + "case_id integer NOT NULL,device_id text NOT NULL,name text NOT NULL,datasource_obj_id integer,"
439  + "md5 text DEFAULT NULL,sha1 text DEFAULT NULL,sha256 text DEFAULT NULL,"
440  + "foreign key (case_id) references cases(id) ON UPDATE SET NULL ON DELETE SET NULL,"
441  + "CONSTRAINT datasource_unique UNIQUE (case_id, datasource_obj_id))";
442  }
443 
451  static String getAddDataSourcesNameIndexStatement() {
452  return "CREATE INDEX IF NOT EXISTS data_sources_name ON data_sources (name)";
453  }
454 
462  static String getAddDataSourcesObjectIdIndexStatement() {
463  return "CREATE INDEX IF NOT EXISTS data_sources_object_id ON data_sources (datasource_obj_id)";
464  }
465 
474  static String getAddCaseIdIndexTemplate() {
475  // Each "%s" will be replaced with the relevant TYPE_instances table name.
476  return "CREATE INDEX IF NOT EXISTS %s_case_id ON %s (case_id)";
477  }
478 
487  static String getAddDataSourceIdIndexTemplate() {
488  // Each "%s" will be replaced with the relevant TYPE_instances table name.
489  return "CREATE INDEX IF NOT EXISTS %s_data_source_id ON %s (data_source_id)";
490  }
491 
500  static String getAddValueIndexTemplate() {
501  // Each "%s" will be replaced with the relevant TYPE_instances table name.
502  return "CREATE INDEX IF NOT EXISTS %s_value ON %s (value)";
503  }
504 
513  static String getAddKnownStatusIndexTemplate() {
514  // Each "%s" will be replaced with the relevant TYPE_instances table name.
515  return "CREATE INDEX IF NOT EXISTS %s_value_known_status ON %s (value, known_status)";
516  }
517 
526  static String getAddObjectIdIndexTemplate() {
527  // Each "%s" will be replaced with the relevant TYPE_instances table name.
528  return "CREATE INDEX IF NOT EXISTS %s_file_obj_id ON %s (file_obj_id)";
529  }
530 
531  public boolean insertDefaultDatabaseContent() {
532  Connection conn = getEphemeralConnection();
533  if (null == conn) {
534  return false;
535  }
536 
537  boolean result = EamDbUtil.insertDefaultCorrelationTypes(conn) && EamDbUtil.insertDefaultOrganization(conn);
539  return result;
540  }
541 
542  boolean isChanged() {
543  String dbNameString = ModuleSettings.getConfigSetting("CentralRepository", "db.sqlite.dbName"); // NON-NLS
544  String dbDirectoryString = ModuleSettings.getConfigSetting("CentralRepository", "db.sqlite.dbDirectory"); // NON-NLS
545  String bulkThresholdString = ModuleSettings.getConfigSetting("CentralRepository", "db.sqlite.bulkThreshold"); // NON-NLS
546 
547  return !dbName.equals(dbNameString)
548  || !dbDirectory.equals(dbDirectoryString)
549  || !Integer.toString(bulkThreshold).equals(bulkThresholdString);
550  }
551 
555  public String getDbName() {
556  return dbName;
557  }
558 
564  public void setDbName(String dbName) throws EamDbException {
565  if (dbName == null || dbName.isEmpty()) {
566  throw new EamDbException("Invalid database file name. Cannot be null or empty."); // NON-NLS
567  } else if (!Pattern.matches(DB_NAMES_REGEX, dbName)) {
568  throw new EamDbException("Invalid database file name. Name must start with a lowercase letter and can only contain lowercase letters, numbers, and '_'."); // NON-NLS
569  }
570 
571  this.dbName = dbName;
572  }
573 
577  int getBulkThreshold() {
578  return bulkThreshold;
579  }
580 
584  void setBulkThreshold(int bulkThreshold) throws EamDbException {
585  if (bulkThreshold > 0) {
586  this.bulkThreshold = bulkThreshold;
587  } else {
588  throw new EamDbException("Invalid bulk threshold."); // NON-NLS
589  }
590  }
591 
595  public String getDbDirectory() {
596  return dbDirectory;
597  }
598 
606  public void setDbDirectory(String dbDirectory) throws EamDbException {
607  if (dbDirectory != null && !dbDirectory.isEmpty()) {
608  this.dbDirectory = dbDirectory;
609  } else {
610  throw new EamDbException("Invalid directory for sqlite database. Cannot empty"); // NON-NLS
611  }
612  }
613 
619  public String getFileNameWithPath() {
620  return getDbDirectory() + File.separator + getDbName();
621  }
622 
626  String getDriver() {
627  return JDBC_DRIVER;
628  }
629 
633  String getValidationQuery() {
634  return VALIDATION_QUERY;
635  }
636 
640  String getJDBCBaseURI() {
641  return JDBC_BASE_URI;
642  }
643 
644 }
static String correlationTypeToInstanceTableName(CorrelationAttributeInstance.Type type)
Definition: EamDbUtil.java:353
static boolean executeValidationQuery(Connection conn, String validationQuery)
Definition: EamDbUtil.java:325
static synchronized void setConfigSetting(String moduleName, String settingName, String settingVal)
static boolean insertDefaultCorrelationTypes(Connection conn)
Definition: EamDbUtil.java:103
static String getConfigSetting(String moduleName, String settingName)
synchronized static Logger getLogger(String name)
Definition: Logger.java:124
static String correlationTypeToReferenceTableName(CorrelationAttributeInstance.Type type)
Definition: EamDbUtil.java:364

Copyright © 2012-2018 Basis Technology. Generated on: Wed Sep 18 2019
This work is licensed under a Creative Commons Attribution-Share Alike 3.0 United States License.