1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 package org.owasp.dependencycheck.data.nvdcve;
19
20 import com.google.common.io.Resources;
21 import java.io.File;
22 import java.io.IOException;
23 import java.io.InputStream;
24 import java.net.URL;
25 import java.nio.charset.StandardCharsets;
26 import java.sql.PreparedStatement;
27 import java.sql.Connection;
28 import java.sql.Driver;
29 import java.sql.DriverManager;
30 import java.sql.ResultSet;
31 import java.sql.SQLException;
32 import java.sql.Statement;
33 import java.util.Locale;
34 import java.util.ResourceBundle;
35 import javax.annotation.concurrent.ThreadSafe;
36 import org.anarres.jdiagnostics.DefaultQuery;
37 import org.apache.commons.dbcp2.BasicDataSource;
38 import org.apache.commons.io.IOUtils;
39 import org.owasp.dependencycheck.utils.DBUtils;
40 import org.owasp.dependencycheck.utils.DependencyVersion;
41 import org.owasp.dependencycheck.utils.DependencyVersionUtil;
42 import org.owasp.dependencycheck.utils.FileUtils;
43 import org.owasp.dependencycheck.utils.Settings;
44 import org.slf4j.Logger;
45 import org.slf4j.LoggerFactory;
46
47
48
49
50
51
52
53
54
55 @ThreadSafe
56 public final class DatabaseManager {
57
58
59
60
61 private static final Logger LOGGER = LoggerFactory.getLogger(DatabaseManager.class);
62
63
64
65 public static final String DB_STRUCTURE_RESOURCE = "data/initialize.sql";
66
67
68
69 public static final String DB_STRUCTURE_UPDATE_RESOURCE = "data/upgrade_%s.sql";
70
71
72
73 public static final String UPGRADE_HELP_URL = "https://jeremylong.github.io/DependencyCheck/data/upgrade.html";
74
75
76
77 private Driver driver = null;
78
79
80
81 private String connectionString = null;
82
83
84
85 private String userName = null;
86
87
88
89 private String password = null;
90
91
92
93
94 private int callDepth = 0;
95
96
97
98 private final Settings settings;
99
100
101
102 private boolean isH2;
103
104
105
106 private boolean isOracle;
107
108
109
110 private String databaseProductName;
111
112
113
114 private BasicDataSource connectionPool;
115
116
117
118
119
120
121
122
123 public DatabaseManager(Settings settings) throws DatabaseException {
124 this.settings = settings;
125 initialize();
126 }
127
128
129
130
131
132
133
134
135 private void initialize() throws DatabaseException {
136 final boolean autoUpdate = settings.getBoolean(Settings.KEYS.AUTO_UPDATE, true);
137 Connection conn = null;
138 try {
139
140 final String driverName = settings.getString(Settings.KEYS.DB_DRIVER_NAME, "");
141 if (!driverName.isEmpty()) {
142 final String driverPath = settings.getString(Settings.KEYS.DB_DRIVER_PATH, "");
143 LOGGER.debug("Loading driver '{}'", driverName);
144 try {
145 if (!driverPath.isEmpty()) {
146 LOGGER.debug("Loading driver from: {}", driverPath);
147 driver = DriverLoader.load(driverName, driverPath);
148 } else {
149 driver = DriverLoader.load(driverName);
150 }
151 } catch (DriverLoadException ex) {
152 LOGGER.debug("Unable to load database driver", ex);
153 throw new DatabaseException("Unable to load database driver", ex);
154 }
155 }
156 userName = settings.getString(Settings.KEYS.DB_USER, "dcuser");
157
158 password = settings.getString(Settings.KEYS.DB_PASSWORD, "DC-Pass1337!");
159 try {
160 connectionString = settings.getConnectionString(
161 Settings.KEYS.DB_CONNECTION_STRING,
162 Settings.KEYS.DB_FILE_NAME);
163 } catch (IOException ex) {
164 LOGGER.debug("Unable to retrieve the database connection string", ex);
165 throw new DatabaseException("Unable to retrieve the database connection string", ex);
166 }
167 isH2 = isH2Connection(connectionString);
168 boolean shouldCreateSchema = false;
169 try {
170 if (autoUpdate && isH2) {
171 shouldCreateSchema = !h2DataFileExists();
172 LOGGER.debug("Need to create DB Structure: {}", shouldCreateSchema);
173 }
174 } catch (IOException ioex) {
175 LOGGER.debug("Unable to verify database exists", ioex);
176 throw new DatabaseException("Unable to verify database exists", ioex);
177 }
178 LOGGER.debug("Loading database connection");
179 LOGGER.debug("Connection String: {}", connectionString);
180 LOGGER.debug("Database User: {}", userName);
181
182 try {
183 if (connectionString.toLowerCase().contains("integrated security=true")
184 || connectionString.toLowerCase().contains("trusted_connection=true")) {
185 conn = DriverManager.getConnection(connectionString);
186 } else {
187 conn = DriverManager.getConnection(connectionString, userName, password);
188 }
189 } catch (SQLException ex) {
190 if (ex.getMessage().contains("java.net.UnknownHostException") && connectionString.contains("AUTO_SERVER=TRUE;")) {
191 connectionString = connectionString.replace("AUTO_SERVER=TRUE;", "");
192 try {
193 conn = DriverManager.getConnection(connectionString, userName, password);
194 settings.setString(Settings.KEYS.DB_CONNECTION_STRING, connectionString);
195 LOGGER.debug("Unable to start the database in server mode; reverting to single user mode");
196 } catch (SQLException sqlex) {
197 LOGGER.debug("Unable to connect to the database", ex);
198 throw new DatabaseException("Unable to connect to the database", ex);
199 }
200 } else if (isH2 && ex.getMessage().contains("file version or invalid file header")) {
201 LOGGER.error("Incompatible or corrupt database found. To resolve this issue please remove the existing "
202 + "database by running purge");
203 throw new DatabaseException("Incompatible or corrupt database found; run the purge command to resolve the issue");
204 } else {
205 LOGGER.debug("Unable to connect to the database", ex);
206 throw new DatabaseException("Unable to connect to the database", ex);
207 }
208 }
209 databaseProductName = determineDatabaseProductName(conn);
210 isOracle = "oracle".equals(databaseProductName);
211 if (shouldCreateSchema) {
212 try {
213 createTables(conn);
214 } catch (DatabaseException dex) {
215 LOGGER.debug("", dex);
216 throw new DatabaseException("Unable to create the database structure", dex);
217 }
218 }
219 try {
220 ensureSchemaVersion(conn);
221 } catch (DatabaseException dex) {
222 LOGGER.debug("", dex);
223 throw new DatabaseException("Database schema does not match this version of dependency-check", dex);
224 }
225 } finally {
226 if (conn != null) {
227 try {
228 conn.close();
229 } catch (SQLException ex) {
230 LOGGER.debug("An error occurred closing the connection", ex);
231 }
232 }
233 }
234 }
235
236
237
238
239
240
241
242 private String determineDatabaseProductName(Connection conn) {
243 try {
244 final String databaseProductName = conn.getMetaData().getDatabaseProductName().toLowerCase();
245 LOGGER.debug("Database product: {}", databaseProductName);
246 return databaseProductName;
247 } catch (SQLException se) {
248 LOGGER.warn("Problem determining database product!", se);
249 return null;
250 }
251 }
252
253
254
255
256
257
258
259 public void cleanup() {
260 if (driver != null) {
261 DriverLoader.cleanup(driver);
262 driver = null;
263 }
264 connectionString = null;
265 userName = null;
266 password = null;
267 }
268
269
270
271
272
273
274
275
276
277 public boolean h2DataFileExists() throws IOException {
278 return h2DataFileExists(settings);
279 }
280
281
282
283
284
285
286
287
288
289
290 public static boolean h2DataFileExists(Settings configuration) throws IOException {
291 final File file = getH2DataFile(configuration);
292 return file.exists();
293 }
294
295
296
297
298
299
300
301
302 public static File getH2DataFile(Settings configuration) throws IOException {
303 final File dir = configuration.getH2DataDirectory();
304 final String fileName = configuration.getString(Settings.KEYS.DB_FILE_NAME);
305 return new File(dir, fileName);
306 }
307
308
309
310
311
312
313 public String getDatabaseProductName() {
314 return databaseProductName;
315 }
316
317
318
319
320
321
322 public boolean isH2Connection() {
323 return isH2;
324 }
325
326
327
328
329
330
331 public boolean isOracle() {
332 return isOracle;
333 }
334
335
336
337
338
339
340
341 public static boolean isH2Connection(Settings configuration) {
342 final String connStr;
343 try {
344 connStr = configuration.getConnectionString(
345 Settings.KEYS.DB_CONNECTION_STRING,
346 Settings.KEYS.DB_FILE_NAME);
347 } catch (IOException ex) {
348 LOGGER.debug("Unable to get connectionn string", ex);
349 return false;
350 }
351 return isH2Connection(connStr);
352 }
353
354
355
356
357
358
359
360 public static boolean isH2Connection(String connectionString) {
361 return connectionString.startsWith("jdbc:h2:file:");
362 }
363
364
365
366
367
368
369
370
371 private void createTables(Connection conn) throws DatabaseException {
372 LOGGER.debug("Creating database structure");
373 final String dbStructure;
374 try {
375 dbStructure = getResource(DB_STRUCTURE_RESOURCE);
376
377 Statement statement = null;
378 try {
379 statement = conn.createStatement();
380 statement.execute(dbStructure);
381 } catch (SQLException ex) {
382 LOGGER.debug("", ex);
383 throw new DatabaseException("Unable to create database statement", ex);
384 } finally {
385 DBUtils.closeStatement(statement);
386 }
387 } catch (IOException ex) {
388 throw new DatabaseException("Unable to create database schema", ex);
389 } catch (LinkageError ex) {
390 LOGGER.debug(new DefaultQuery(ex).call().toString());
391 }
392 }
393
394 private String getResource(String resource) throws IOException {
395 String dbStructure;
396 try {
397 final URL url = Resources.getResource(resource);
398 dbStructure = Resources.toString(url, StandardCharsets.UTF_8);
399 } catch (IllegalArgumentException ex) {
400 LOGGER.debug("Resources.getResource(String) failed to find the DB Structure Resource", ex);
401 try (InputStream is = FileUtils.getResourceAsStream(resource)) {
402 dbStructure = IOUtils.toString(is, StandardCharsets.UTF_8);
403 }
404 }
405 return dbStructure;
406 }
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421 private void updateSchema(Connection conn, DependencyVersion appExpectedVersion, DependencyVersion currentDbVersion)
422 throws DatabaseException {
423
424 if (connectionString.startsWith("jdbc:h2:file:")) {
425 LOGGER.debug("Updating database structure");
426 final String updateFile = String.format(DB_STRUCTURE_UPDATE_RESOURCE, currentDbVersion.toString());
427 if ("data/upgrade_4.2.sql".equals(updateFile) && !FileUtils.getResourceAsFile(updateFile).exists()) {
428 throw new DatabaseException("unable to upgrade the database schema - please run the dependency-check "
429 + "purge command to remove the existing database");
430 }
431 try {
432 final String dbStructureUpdate = getResource(updateFile);
433 Statement statement = null;
434 try {
435 statement = conn.createStatement();
436 statement.execute(dbStructureUpdate);
437 } catch (SQLException ex) {
438 throw new DatabaseException(String.format("Unable to upgrade the database schema from %s to %s",
439 currentDbVersion, appExpectedVersion.toString()), ex);
440 } finally {
441 DBUtils.closeStatement(statement);
442 }
443 } catch (IllegalArgumentException | IOException ex) {
444 final String msg = String.format("Upgrade SQL file does not exist: %s", updateFile);
445 throw new DatabaseException(msg, ex);
446 }
447 } else {
448 final int e0 = Integer.parseInt(appExpectedVersion.getVersionParts().get(0));
449 final int c0 = Integer.parseInt(currentDbVersion.getVersionParts().get(0));
450 final int e1 = Integer.parseInt(appExpectedVersion.getVersionParts().get(1));
451 final int c1 = Integer.parseInt(currentDbVersion.getVersionParts().get(1));
452
453 if (e0 == c0 && e1 < c1) {
454 LOGGER.warn("A new version of dependency-check is available; consider upgrading");
455 settings.setBoolean(Settings.KEYS.AUTO_UPDATE, false);
456 } else if (e0 == c0 && e1 == c1) {
457
458 } else {
459 LOGGER.error("The database schema must be upgraded to use this version of dependency-check. Please see {} for more information.",
460 UPGRADE_HELP_URL);
461 throw new DatabaseException("Database schema is out of date");
462 }
463
464 }
465 }
466
467
468
469
470
471
472
473 public ResourceBundle getSqlStatements() {
474 final ResourceBundle statementBundle = getDatabaseProductName() != null
475 ? ResourceBundle.getBundle("data/dbStatements", new Locale(getDatabaseProductName()))
476 : ResourceBundle.getBundle("data/dbStatements");
477 return statementBundle;
478 }
479
480
481
482
483
484
485
486
487
488 private void ensureSchemaVersion(Connection conn) throws DatabaseException {
489 ResultSet rs = null;
490 PreparedStatement ps = null;
491 final ResourceBundle statementBundle = getSqlStatements();
492 final String sql = statementBundle.getString("SELECT_SCHEMA_VERSION");
493 try {
494 ps = conn.prepareStatement(sql);
495 rs = ps.executeQuery();
496 if (rs.next()) {
497 final String dbSchemaVersion = settings.getString(Settings.KEYS.DB_VERSION);
498 final DependencyVersion appDbVersion = DependencyVersionUtil.parseVersion(dbSchemaVersion);
499 if (appDbVersion == null) {
500 throw new DatabaseException("Invalid application database schema");
501 }
502 final DependencyVersion db = DependencyVersionUtil.parseVersion(rs.getString(1));
503 if (db == null) {
504 throw new DatabaseException("Invalid database schema");
505 }
506 LOGGER.debug("DC Schema: {}", appDbVersion);
507 LOGGER.debug("DB Schema: {}", db);
508 if (appDbVersion.compareTo(db) > 0) {
509 final boolean autoUpdate = settings.getBoolean(Settings.KEYS.AUTO_UPDATE, true);
510 if (autoUpdate) {
511 updateSchema(conn, appDbVersion, db);
512 if (++callDepth < 10) {
513 ensureSchemaVersion(conn);
514 }
515 } else {
516 throw new DatabaseException("Old database schema identified - please execute "
517 + "dependency-check without the no-update configuration to continue");
518 }
519 }
520 } else {
521 throw new DatabaseException("Database schema is missing");
522 }
523 } catch (SQLException ex) {
524 LOGGER.debug("", ex);
525 throw new DatabaseException("Unable to check the database schema version", ex);
526 } finally {
527 DBUtils.closeResultSet(rs);
528 DBUtils.closeStatement(ps);
529 }
530 }
531
532
533
534
535 public void open() {
536 connectionPool = new BasicDataSource();
537 if (driver != null) {
538 connectionPool.setDriver(driver);
539 }
540 connectionPool.setUrl(connectionString);
541 connectionPool.setUsername(userName);
542 connectionPool.setPassword(password);
543 }
544
545
546
547
548 public void close() {
549 try {
550 connectionPool.close();
551 } catch (SQLException ex) {
552 LOGGER.debug("Error closing the connection pool", ex);
553 }
554 connectionPool = null;
555 }
556
557
558
559
560
561
562 public boolean isOpen() {
563 return connectionPool != null;
564 }
565
566
567
568
569
570
571
572
573
574 public Connection getConnection() throws DatabaseException {
575 try {
576 return connectionPool.getConnection();
577 } catch (SQLException ex) {
578 throw new DatabaseException("Error connecting to the database", ex);
579 }
580 }
581 }